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
@@ -0,0 +1,14 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "adapters.moonwell_adapter.adapter.MoonwellAdapter"
3
+ capabilities:
4
+ - "lending.lend"
5
+ - "lending.unlend"
6
+ - "lending.borrow"
7
+ - "lending.repay"
8
+ - "collateral.set"
9
+ - "collateral.remove"
10
+ - "rewards.claim"
11
+ - "position.read"
12
+ - "market.apy"
13
+ - "market.collateral_factor"
14
+ dependencies: []
@@ -2,13 +2,22 @@ from contextlib import asynccontextmanager
2
2
  from unittest.mock import AsyncMock, MagicMock, patch
3
3
 
4
4
  import pytest
5
+ from web3 import Web3
5
6
 
6
7
  from wayfinder_paths.adapters.moonwell_adapter.adapter import (
7
- BASE_CHAIN_ID,
8
+ CHAIN_NAME,
8
9
  MANTISSA,
9
- MOONWELL_DEFAULTS,
10
10
  MoonwellAdapter,
11
11
  )
12
+ from wayfinder_paths.core.constants.contracts import (
13
+ BASE_USDC,
14
+ BASE_WETH,
15
+ MOONWELL_M_USDC,
16
+ MOONWELL_M_WETH,
17
+ MOONWELL_M_WSTETH,
18
+ MOONWELL_REWARD_DISTRIBUTOR,
19
+ MOONWELL_WELL_TOKEN,
20
+ )
12
21
 
13
22
 
14
23
  class TestMoonwellAdapter:
@@ -22,61 +31,98 @@ class TestMoonwellAdapter:
22
31
  def test_adapter_type(self, adapter):
23
32
  assert adapter.adapter_type == "MOONWELL"
24
33
 
25
- def test_default_addresses(self, adapter):
26
- assert (
27
- adapter.comptroller_address.lower()
28
- == MOONWELL_DEFAULTS["comptroller"].lower()
29
- )
30
- assert (
31
- adapter.reward_distributor_address.lower()
32
- == MOONWELL_DEFAULTS["reward_distributor"].lower()
33
- )
34
- assert adapter.m_usdc.lower() == MOONWELL_DEFAULTS["m_usdc"].lower()
35
- assert adapter.m_weth.lower() == MOONWELL_DEFAULTS["m_weth"].lower()
36
- assert adapter.m_wsteth.lower() == MOONWELL_DEFAULTS["m_wsteth"].lower()
37
- assert adapter.well_token.lower() == MOONWELL_DEFAULTS["well_token"].lower()
34
+ def test_chain_name(self):
35
+ assert CHAIN_NAME == "base"
38
36
 
39
- def test_chain_id(self, adapter):
40
- assert adapter.chain_id == BASE_CHAIN_ID
37
+ @pytest.mark.asyncio
38
+ async def test_get_full_user_state_basic(self, adapter):
39
+ w3 = Web3()
41
40
 
42
- def test_chain_name(self, adapter):
43
- assert adapter.chain_name == "base"
41
+ @asynccontextmanager
42
+ async def mock_web3_ctx(_chain_id):
43
+ yield w3
44
+
45
+ m1 = MOONWELL_M_USDC
46
+ m2 = MOONWELL_M_WETH
47
+
48
+ rewards = w3.codec.encode(
49
+ ["(address,(address,uint256,uint256,uint256)[])[]"],
50
+ [[(m1, [(MOONWELL_WELL_TOKEN, 123, 0, 0)])]],
51
+ )
52
+
53
+ stage1 = [
54
+ w3.codec.encode(["address[]"], [[m1, m2]]),
55
+ w3.codec.encode(["address[]"], [[m1]]),
56
+ w3.codec.encode(["uint256", "uint256", "uint256"], [0, 123, 0]),
57
+ rewards,
58
+ ]
59
+
60
+ stage2 = [
61
+ # m1
62
+ w3.codec.encode(["uint256"], [100]),
63
+ w3.codec.encode(["uint256"], [2 * MANTISSA]),
64
+ w3.codec.encode(["uint256"], [50]),
65
+ w3.codec.encode(["address"], [BASE_USDC]),
66
+ w3.codec.encode(["uint8"], [8]),
67
+ w3.codec.encode(["bool", "uint256"], [True, int(0.5 * MANTISSA)]),
68
+ # m2 (all zeros, should be filtered out)
69
+ w3.codec.encode(["uint256"], [0]),
70
+ w3.codec.encode(["uint256"], [0]),
71
+ w3.codec.encode(["uint256"], [0]),
72
+ w3.codec.encode(["address"], [BASE_WETH]),
73
+ w3.codec.encode(["uint8"], [8]),
74
+ w3.codec.encode(["bool", "uint256"], [True, int(0.5 * MANTISSA)]),
75
+ ]
44
76
 
45
- @pytest.mark.asyncio
46
- async def test_health_check(self, adapter):
47
- health = await adapter.health_check()
48
- assert isinstance(health, dict)
49
- assert health.get("status") in {"healthy", "unhealthy", "error"}
50
- assert health.get("adapter") == "MOONWELL"
77
+ with (
78
+ patch(
79
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.web3_from_chain_id",
80
+ mock_web3_ctx,
81
+ ),
82
+ patch.object(
83
+ adapter, "_multicall_chunked", new_callable=AsyncMock
84
+ ) as mock_multicall,
85
+ ):
86
+ mock_multicall.side_effect = [stage1, stage2]
87
+ ok, state = await adapter.get_full_user_state(
88
+ include_rewards=True,
89
+ include_usd=False,
90
+ include_apy=False,
91
+ )
51
92
 
52
- @pytest.mark.asyncio
53
- async def test_connect(self, adapter):
54
- ok = await adapter.connect()
55
- assert isinstance(ok, bool)
56
93
  assert ok is True
94
+ assert state["protocol"] == "moonwell"
95
+ assert state["chainId"] == adapter.chain_id
96
+ assert state["accountLiquidity"]["liquidity"] == 123
97
+ assert len(state["positions"]) == 1
98
+ assert state["positions"][0]["enteredAsCollateral"] is True
99
+ assert state["positions"][0]["suppliedUnderlying"] == 200
100
+ assert state["rewards"][f"base_{MOONWELL_WELL_TOKEN.lower()}"] == 123
57
101
 
58
102
  @pytest.mark.asyncio
59
103
  async def test_lend(self, adapter):
60
104
  mock_tx_hash = {"tx_hash": "0xabc123", "status": "success"}
61
105
  with (
62
- patch.object(
63
- adapter, "_ensure_allowance", new_callable=AsyncMock
106
+ patch(
107
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.ensure_allowance",
108
+ new_callable=AsyncMock,
64
109
  ) as mock_allowance,
65
- patch.object(
66
- adapter, "_encode_call", new_callable=AsyncMock
110
+ patch(
111
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.encode_call",
112
+ new_callable=AsyncMock,
67
113
  ) as mock_encode,
68
- patch.object(adapter, "_send_tx", new_callable=AsyncMock) as mock_send,
114
+ patch(
115
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.send_transaction",
116
+ new_callable=AsyncMock,
117
+ ) as mock_send,
69
118
  ):
70
119
  mock_allowance.return_value = (True, {})
71
- mock_encode.return_value = {
72
- "data": "0x1234",
73
- "to": MOONWELL_DEFAULTS["m_usdc"],
74
- }
75
- mock_send.return_value = (True, mock_tx_hash)
120
+ mock_encode.return_value = {"data": "0x1234", "to": MOONWELL_M_USDC}
121
+ mock_send.return_value = mock_tx_hash
76
122
 
77
123
  success, result = await adapter.lend(
78
- mtoken=MOONWELL_DEFAULTS["m_usdc"],
79
- underlying_token=MOONWELL_DEFAULTS["usdc"],
124
+ mtoken=MOONWELL_M_USDC,
125
+ underlying_token=BASE_USDC,
80
126
  amount=10**6,
81
127
  )
82
128
 
@@ -86,8 +132,8 @@ class TestMoonwellAdapter:
86
132
  @pytest.mark.asyncio
87
133
  async def test_lend_invalid_amount(self, adapter):
88
134
  success, result = await adapter.lend(
89
- mtoken=MOONWELL_DEFAULTS["m_usdc"],
90
- underlying_token=MOONWELL_DEFAULTS["usdc"],
135
+ mtoken=MOONWELL_M_USDC,
136
+ underlying_token=BASE_USDC,
91
137
  amount=0,
92
138
  )
93
139
 
@@ -96,32 +142,22 @@ class TestMoonwellAdapter:
96
142
 
97
143
  @pytest.mark.asyncio
98
144
  async def test_unlend(self, adapter):
99
- # Mock contract encoding
100
- mock_contract = MagicMock()
101
- mock_contract.functions.redeem = MagicMock(
102
- return_value=MagicMock(
103
- build_transaction=AsyncMock(return_value={"data": "0x1234"})
104
- )
105
- )
106
- mock_web3 = MagicMock()
107
- mock_web3.eth.contract = MagicMock(return_value=mock_contract)
108
-
109
- @asynccontextmanager
110
- async def mock_web3_ctx(_chain_id):
111
- yield mock_web3
112
-
113
145
  mock_tx_hash = {"tx_hash": "0xabc123", "status": "success"}
114
146
 
115
147
  with (
116
148
  patch(
117
- "wayfinder_paths.adapters.moonwell_adapter.adapter.web3_from_chain_id",
118
- mock_web3_ctx,
119
- ),
120
- patch.object(adapter, "_send_tx", new_callable=AsyncMock) as mock_send,
149
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.encode_call",
150
+ new_callable=AsyncMock,
151
+ ) as mock_encode,
152
+ patch(
153
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.send_transaction",
154
+ new_callable=AsyncMock,
155
+ ) as mock_send,
121
156
  ):
122
- mock_send.return_value = (True, mock_tx_hash)
157
+ mock_encode.return_value = {"data": "0x1234", "to": MOONWELL_M_USDC}
158
+ mock_send.return_value = mock_tx_hash
123
159
  success, result = await adapter.unlend(
124
- mtoken=MOONWELL_DEFAULTS["m_usdc"],
160
+ mtoken=MOONWELL_M_USDC,
125
161
  amount=10**8,
126
162
  )
127
163
 
@@ -131,7 +167,7 @@ class TestMoonwellAdapter:
131
167
  @pytest.mark.asyncio
132
168
  async def test_unlend_invalid_amount(self, adapter):
133
169
  success, result = await adapter.unlend(
134
- mtoken=MOONWELL_DEFAULTS["m_usdc"],
170
+ mtoken=MOONWELL_M_USDC,
135
171
  amount=-1,
136
172
  )
137
173
 
@@ -140,47 +176,22 @@ class TestMoonwellAdapter:
140
176
 
141
177
  @pytest.mark.asyncio
142
178
  async def test_borrow(self, adapter):
143
- # Track calls to return different values (0 before, 10**6 after)
144
- borrow_balance_calls = [0]
145
-
146
- async def mock_borrow_balance_call(**kwargs):
147
- result = borrow_balance_calls[0]
148
- # Next call returns increased balance
149
- borrow_balance_calls[0] = 10**6
150
- return result
151
-
152
- # Mock mtoken contract for pre-check and verification
153
- mock_mtoken = MagicMock()
154
- mock_mtoken.functions.borrowBalanceStored = MagicMock(
155
- return_value=MagicMock(call=mock_borrow_balance_call)
156
- )
157
- mock_mtoken.functions.borrow = MagicMock(
158
- return_value=MagicMock(
159
- call=AsyncMock(return_value=0),
160
- _encode_transaction_data=MagicMock(return_value="0x1234"),
161
- build_transaction=AsyncMock(return_value={"data": "0x1234"}),
162
- )
163
- )
164
-
165
- mock_web3 = MagicMock()
166
- mock_web3.eth.contract = MagicMock(return_value=mock_mtoken)
167
-
168
- @asynccontextmanager
169
- async def mock_web3_ctx(_chain_id):
170
- yield mock_web3
171
-
172
179
  mock_tx_hash = {"tx_hash": "0xabc123", "status": "success"}
173
180
 
174
181
  with (
175
182
  patch(
176
- "wayfinder_paths.adapters.moonwell_adapter.adapter.web3_from_chain_id",
177
- mock_web3_ctx,
178
- ),
179
- patch.object(adapter, "_send_tx", new_callable=AsyncMock) as mock_send,
183
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.encode_call",
184
+ new_callable=AsyncMock,
185
+ ) as mock_encode,
186
+ patch(
187
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.send_transaction",
188
+ new_callable=AsyncMock,
189
+ ) as mock_send,
180
190
  ):
181
- mock_send.return_value = (True, mock_tx_hash)
191
+ mock_encode.return_value = {"data": "0x1234", "to": MOONWELL_M_USDC}
192
+ mock_send.return_value = mock_tx_hash
182
193
  success, result = await adapter.borrow(
183
- mtoken=MOONWELL_DEFAULTS["m_usdc"],
194
+ mtoken=MOONWELL_M_USDC,
184
195
  amount=10**6,
185
196
  )
186
197
 
@@ -191,24 +202,26 @@ class TestMoonwellAdapter:
191
202
  async def test_repay(self, adapter):
192
203
  mock_tx_hash = {"tx_hash": "0xabc123", "status": "success"}
193
204
  with (
194
- patch.object(
195
- adapter, "_ensure_allowance", new_callable=AsyncMock
205
+ patch(
206
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.ensure_allowance",
207
+ new_callable=AsyncMock,
196
208
  ) as mock_allowance,
197
- patch.object(
198
- adapter, "_encode_call", new_callable=AsyncMock
209
+ patch(
210
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.encode_call",
211
+ new_callable=AsyncMock,
199
212
  ) as mock_encode,
200
- patch.object(adapter, "_send_tx", new_callable=AsyncMock) as mock_send,
213
+ patch(
214
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.send_transaction",
215
+ new_callable=AsyncMock,
216
+ ) as mock_send,
201
217
  ):
202
218
  mock_allowance.return_value = (True, {})
203
- mock_encode.return_value = {
204
- "data": "0x1234",
205
- "to": MOONWELL_DEFAULTS["m_usdc"],
206
- }
207
- mock_send.return_value = (True, mock_tx_hash)
219
+ mock_encode.return_value = {"data": "0x1234", "to": MOONWELL_M_USDC}
220
+ mock_send.return_value = mock_tx_hash
208
221
 
209
222
  success, result = await adapter.repay(
210
- mtoken=MOONWELL_DEFAULTS["m_usdc"],
211
- underlying_token=MOONWELL_DEFAULTS["usdc"],
223
+ mtoken=MOONWELL_M_USDC,
224
+ underlying_token=BASE_USDC,
212
225
  amount=10**6,
213
226
  )
214
227
 
@@ -217,17 +230,10 @@ class TestMoonwellAdapter:
217
230
 
218
231
  @pytest.mark.asyncio
219
232
  async def test_set_collateral(self, adapter):
220
- # Mock comptroller contract for verification
221
233
  mock_comptroller = MagicMock()
222
234
  mock_comptroller.functions.checkMembership = MagicMock(
223
235
  return_value=MagicMock(call=AsyncMock(return_value=True))
224
236
  )
225
- mock_comptroller.functions.enterMarkets = MagicMock(
226
- return_value=MagicMock(
227
- _encode_transaction_data=MagicMock(return_value="0x1234"),
228
- build_transaction=AsyncMock(return_value={"data": "0x1234"}),
229
- )
230
- )
231
237
 
232
238
  mock_web3 = MagicMock()
233
239
  mock_web3.eth.contract = MagicMock(return_value=mock_comptroller)
@@ -239,15 +245,23 @@ class TestMoonwellAdapter:
239
245
  mock_tx_hash = {"tx_hash": "0xabc123", "status": "success"}
240
246
 
241
247
  with (
248
+ patch(
249
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.encode_call",
250
+ new_callable=AsyncMock,
251
+ ) as mock_encode,
252
+ patch(
253
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.send_transaction",
254
+ new_callable=AsyncMock,
255
+ ) as mock_send,
242
256
  patch(
243
257
  "wayfinder_paths.adapters.moonwell_adapter.adapter.web3_from_chain_id",
244
258
  mock_web3_ctx,
245
259
  ),
246
- patch.object(adapter, "_send_tx", new_callable=AsyncMock) as mock_send,
247
260
  ):
248
- mock_send.return_value = (True, mock_tx_hash)
261
+ mock_encode.return_value = {"data": "0x1234", "to": MOONWELL_M_WSTETH}
262
+ mock_send.return_value = mock_tx_hash
249
263
  success, result = await adapter.set_collateral(
250
- mtoken=MOONWELL_DEFAULTS["m_wsteth"],
264
+ mtoken=MOONWELL_M_WSTETH,
251
265
  )
252
266
 
253
267
  assert success is True
@@ -270,7 +284,7 @@ class TestMoonwellAdapter:
270
284
  )
271
285
 
272
286
  def mock_contract(address, abi):
273
- if address.lower() == adapter.reward_distributor_address.lower():
287
+ if address.lower() == MOONWELL_REWARD_DISTRIBUTOR.lower():
274
288
  return mock_reward_contract
275
289
  return mock_comptroller
276
290
 
@@ -308,7 +322,7 @@ class TestMoonwellAdapter:
308
322
  )
309
323
 
310
324
  def mock_contract(address, abi):
311
- if address.lower() == adapter.reward_distributor_address.lower():
325
+ if address.lower() == MOONWELL_REWARD_DISTRIBUTOR.lower():
312
326
  return mock_reward
313
327
  return mock_mtoken
314
328
 
@@ -323,7 +337,7 @@ class TestMoonwellAdapter:
323
337
  "wayfinder_paths.adapters.moonwell_adapter.adapter.web3_from_chain_id",
324
338
  mock_web3_ctx,
325
339
  ):
326
- success, result = await adapter.get_pos(mtoken=MOONWELL_DEFAULTS["m_usdc"])
340
+ success, result = await adapter.get_pos(mtoken=MOONWELL_M_USDC)
327
341
 
328
342
  assert success
329
343
  assert "mtoken_balance" in result
@@ -336,7 +350,7 @@ class TestMoonwellAdapter:
336
350
  @pytest.mark.asyncio
337
351
  async def test_get_collateral_factor_success(self, adapter):
338
352
  # Clear cache to ensure fresh test
339
- adapter._cf_cache.clear()
353
+ await adapter._cache.clear()
340
354
 
341
355
  # Mock contract calls - returns (isListed, collateralFactorMantissa)
342
356
  mock_contract = MagicMock()
@@ -357,7 +371,7 @@ class TestMoonwellAdapter:
357
371
  mock_web3_ctx,
358
372
  ):
359
373
  success, result = await adapter.get_collateral_factor(
360
- mtoken=MOONWELL_DEFAULTS["m_wsteth"]
374
+ mtoken=MOONWELL_M_WSTETH
361
375
  )
362
376
 
363
377
  assert success
@@ -390,7 +404,7 @@ class TestMoonwellAdapter:
390
404
  @pytest.mark.asyncio
391
405
  async def test_get_collateral_factor_caching(self, adapter):
392
406
  # Clear cache to ensure fresh test
393
- adapter._cf_cache.clear()
407
+ await adapter._cache.clear()
394
408
 
395
409
  call_count = 0
396
410
 
@@ -410,7 +424,7 @@ class TestMoonwellAdapter:
410
424
  async def mock_web3_ctx(_chain_id):
411
425
  yield mock_web3
412
426
 
413
- mtoken = MOONWELL_DEFAULTS["m_wsteth"]
427
+ mtoken = MOONWELL_M_WSTETH
414
428
 
415
429
  with patch(
416
430
  "wayfinder_paths.adapters.moonwell_adapter.adapter.web3_from_chain_id",
@@ -435,67 +449,11 @@ class TestMoonwellAdapter:
435
449
  assert call_count == 1
436
450
 
437
451
  success4, result4 = await adapter.get_collateral_factor(
438
- mtoken=MOONWELL_DEFAULTS["m_usdc"]
452
+ mtoken=MOONWELL_M_USDC
439
453
  )
440
454
  assert success4 is True
441
455
  assert call_count == 2
442
456
 
443
- @pytest.mark.asyncio
444
- async def test_get_collateral_factor_cache_expiry(self, adapter):
445
- import time
446
-
447
- from wayfinder_paths.adapters.moonwell_adapter import adapter as adapter_module
448
-
449
- # Clear cache to ensure fresh test
450
- adapter._cf_cache.clear()
451
-
452
- original_ttl = adapter_module.CF_CACHE_TTL
453
-
454
- try:
455
- adapter_module.CF_CACHE_TTL = 0.1
456
-
457
- call_count = 0
458
-
459
- async def mock_markets_call(**kwargs):
460
- nonlocal call_count
461
- call_count += 1
462
- return (True, int(0.75 * MANTISSA))
463
-
464
- mock_contract = MagicMock()
465
- mock_contract.functions.markets = MagicMock(
466
- return_value=MagicMock(call=mock_markets_call)
467
- )
468
- mock_web3 = MagicMock()
469
- mock_web3.eth.contract = MagicMock(return_value=mock_contract)
470
-
471
- @asynccontextmanager
472
- async def mock_web3_ctx(_chain_id):
473
- yield mock_web3
474
-
475
- mtoken = MOONWELL_DEFAULTS["m_wsteth"]
476
-
477
- with patch(
478
- "wayfinder_paths.adapters.moonwell_adapter.adapter.web3_from_chain_id",
479
- mock_web3_ctx,
480
- ):
481
- # First call
482
- await adapter.get_collateral_factor(mtoken=mtoken)
483
- assert call_count == 1
484
-
485
- # Immediate second call should use cache
486
- await adapter.get_collateral_factor(mtoken=mtoken)
487
- assert call_count == 1
488
-
489
- # Wait for cache to expire
490
- time.sleep(0.15)
491
-
492
- await adapter.get_collateral_factor(mtoken=mtoken)
493
- assert call_count == 2
494
-
495
- finally:
496
- # Restore original TTL
497
- adapter_module.CF_CACHE_TTL = original_ttl
498
-
499
457
  @pytest.mark.asyncio
500
458
  async def test_get_apy_supply(self, adapter):
501
459
  rate_per_second = int(1.5e9)
@@ -516,7 +474,7 @@ class TestMoonwellAdapter:
516
474
  )
517
475
 
518
476
  def mock_contract(address, abi):
519
- if address.lower() == adapter.reward_distributor_address.lower():
477
+ if address.lower() == MOONWELL_REWARD_DISTRIBUTOR.lower():
520
478
  return mock_reward
521
479
  return mock_mtoken
522
480
 
@@ -532,7 +490,7 @@ class TestMoonwellAdapter:
532
490
  mock_web3_ctx,
533
491
  ):
534
492
  success, result = await adapter.get_apy(
535
- mtoken=MOONWELL_DEFAULTS["m_usdc"],
493
+ mtoken=MOONWELL_M_USDC,
536
494
  apy_type="supply",
537
495
  include_rewards=False,
538
496
  )
@@ -561,7 +519,7 @@ class TestMoonwellAdapter:
561
519
  )
562
520
 
563
521
  def mock_contract(address, abi):
564
- if address.lower() == adapter.reward_distributor_address.lower():
522
+ if address.lower() == MOONWELL_REWARD_DISTRIBUTOR.lower():
565
523
  return mock_reward
566
524
  return mock_mtoken
567
525
 
@@ -577,7 +535,7 @@ class TestMoonwellAdapter:
577
535
  mock_web3_ctx,
578
536
  ):
579
537
  success, result = await adapter.get_apy(
580
- mtoken=MOONWELL_DEFAULTS["m_usdc"],
538
+ mtoken=MOONWELL_M_USDC,
581
539
  apy_type="borrow",
582
540
  include_rewards=False,
583
541
  )
@@ -632,63 +590,28 @@ class TestMoonwellAdapter:
632
590
 
633
591
  @pytest.mark.asyncio
634
592
  async def test_wrap_eth(self, adapter):
635
- mock_contract = MagicMock()
636
- mock_contract.functions.deposit = MagicMock(
637
- return_value=MagicMock(
638
- build_transaction=AsyncMock(return_value={"data": "0x1234"})
639
- )
640
- )
641
- mock_web3 = MagicMock()
642
- mock_web3.eth.contract = MagicMock(return_value=mock_contract)
643
-
644
- @asynccontextmanager
645
- async def mock_web3_ctx(_chain_id):
646
- yield mock_web3
647
-
648
593
  mock_tx_hash = {"tx_hash": "0xabc123", "status": "success"}
649
594
 
650
595
  with (
651
596
  patch(
652
- "wayfinder_paths.adapters.moonwell_adapter.adapter.web3_from_chain_id",
653
- mock_web3_ctx,
654
- ),
655
- patch.object(adapter, "_send_tx", new_callable=AsyncMock) as mock_send,
597
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.encode_call",
598
+ new_callable=AsyncMock,
599
+ ) as mock_encode,
600
+ patch(
601
+ "wayfinder_paths.adapters.moonwell_adapter.adapter.send_transaction",
602
+ new_callable=AsyncMock,
603
+ ) as mock_send,
656
604
  ):
657
- mock_send.return_value = (True, mock_tx_hash)
605
+ mock_encode.return_value = {"data": "0x1234", "to": BASE_WETH}
606
+ mock_send.return_value = mock_tx_hash
658
607
  success, result = await adapter.wrap_eth(amount=10**18)
659
608
 
660
609
  assert success
661
610
  assert result == mock_tx_hash
662
611
 
663
612
  def test_strategy_address_missing(self):
664
- adapter = MoonwellAdapter(config={})
665
-
666
- with pytest.raises(ValueError, match="strategy_wallet"):
667
- adapter._strategy_address()
668
-
669
- def test_checksum_missing_address(self, adapter):
670
- with pytest.raises(ValueError, match="Missing required"):
671
- adapter._checksum(None)
672
-
673
- def test_config_override(self):
674
- custom_comptroller = "0x1111111111111111111111111111111111111111"
675
- custom_well = "0x2222222222222222222222222222222222222222"
676
- config = {
677
- "strategy_wallet": {
678
- "address": "0x1234567890123456789012345678901234567890"
679
- },
680
- "moonwell_adapter": {
681
- "comptroller": custom_comptroller,
682
- "well_token": custom_well,
683
- "chain_id": 1,
684
- },
685
- }
686
-
687
- adapter = MoonwellAdapter(config=config)
688
-
689
- assert adapter.comptroller_address.lower() == custom_comptroller.lower()
690
- assert adapter.well_token.lower() == custom_well.lower()
691
- assert adapter.chain_id == 1
613
+ with pytest.raises(KeyError):
614
+ MoonwellAdapter(config={})
692
615
 
693
616
  @pytest.mark.asyncio
694
617
  async def test_max_withdrawable_mtoken_zero_balance(self, adapter):
@@ -707,9 +630,7 @@ class TestMoonwellAdapter:
707
630
  return_value=MagicMock(call=AsyncMock(return_value=8))
708
631
  )
709
632
  mock_mtoken.functions.underlying = MagicMock(
710
- return_value=MagicMock(
711
- call=AsyncMock(return_value=MOONWELL_DEFAULTS["usdc"])
712
- )
633
+ return_value=MagicMock(call=AsyncMock(return_value=BASE_USDC))
713
634
  )
714
635
 
715
636
  mock_web3 = MagicMock()
@@ -724,7 +645,7 @@ class TestMoonwellAdapter:
724
645
  mock_web3_ctx,
725
646
  ):
726
647
  success, result = await adapter.max_withdrawable_mtoken(
727
- mtoken=MOONWELL_DEFAULTS["m_usdc"]
648
+ mtoken=MOONWELL_M_USDC
728
649
  )
729
650
 
730
651
  assert success
@@ -0,0 +1,7 @@
1
+ """
2
+ Multicall Adapter
3
+ """
4
+
5
+ from wayfinder_paths.adapters.multicall_adapter.adapter import MulticallAdapter
6
+
7
+ __all__ = ["MulticallAdapter"]