wayfinder-paths 0.1.9__py3-none-any.whl → 0.1.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/adapters/balance_adapter/README.md +1 -2
- wayfinder_paths/adapters/balance_adapter/adapter.py +4 -4
- wayfinder_paths/adapters/brap_adapter/adapter.py +139 -23
- wayfinder_paths/adapters/moonwell_adapter/README.md +174 -0
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +1226 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +635 -0
- wayfinder_paths/core/clients/AuthClient.py +3 -0
- wayfinder_paths/core/clients/WayfinderClient.py +2 -2
- wayfinder_paths/core/constants/__init__.py +0 -2
- wayfinder_paths/core/constants/base.py +6 -2
- wayfinder_paths/core/constants/moonwell_abi.py +411 -0
- wayfinder_paths/core/engine/StrategyJob.py +3 -0
- wayfinder_paths/core/services/local_evm_txn.py +182 -217
- wayfinder_paths/core/services/local_token_txn.py +46 -26
- wayfinder_paths/core/strategies/descriptors.py +1 -1
- wayfinder_paths/core/utils/evm_helpers.py +0 -27
- wayfinder_paths/run_strategy.py +34 -74
- wayfinder_paths/scripts/create_strategy.py +2 -27
- wayfinder_paths/scripts/run_strategy.py +37 -7
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +1 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -15
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +108 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/examples.json +11 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2975 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +886 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -1
- wayfinder_paths/templates/adapter/README.md +5 -21
- wayfinder_paths/templates/adapter/adapter.py +1 -2
- wayfinder_paths/templates/adapter/test_adapter.py +1 -1
- wayfinder_paths/templates/strategy/README.md +4 -21
- wayfinder_paths/tests/test_smoke_manifest.py +17 -2
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/METADATA +60 -187
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/RECORD +38 -45
- wayfinder_paths/CONFIG_GUIDE.md +0 -390
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/token_adapter/manifest.yaml +0 -6
- wayfinder_paths/config.example.json +0 -22
- wayfinder_paths/core/engine/manifest.py +0 -97
- wayfinder_paths/scripts/validate_manifests.py +0 -213
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +0 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +0 -17
- wayfinder_paths/templates/adapter/manifest.yaml +0 -6
- wayfinder_paths/templates/strategy/manifest.yaml +0 -8
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.adapters.moonwell_adapter.adapter import (
|
|
6
|
+
BASE_CHAIN_ID,
|
|
7
|
+
MANTISSA,
|
|
8
|
+
MOONWELL_DEFAULTS,
|
|
9
|
+
MoonwellAdapter,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestMoonwellAdapter:
|
|
14
|
+
"""Test cases for MoonwellAdapter"""
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_web3_service(self):
|
|
18
|
+
"""Mock Web3Service for testing"""
|
|
19
|
+
mock_service = MagicMock()
|
|
20
|
+
mock_service.token_transactions = MagicMock()
|
|
21
|
+
mock_service.evm_transactions = MagicMock()
|
|
22
|
+
|
|
23
|
+
# Mock get_web3 to return a mock web3 instance
|
|
24
|
+
mock_web3 = MagicMock()
|
|
25
|
+
mock_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
26
|
+
|
|
27
|
+
return mock_service
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def adapter(self, mock_web3_service):
|
|
31
|
+
"""Create a MoonwellAdapter instance with mocked services"""
|
|
32
|
+
config = {
|
|
33
|
+
"strategy_wallet": {"address": "0x1234567890123456789012345678901234567890"}
|
|
34
|
+
}
|
|
35
|
+
return MoonwellAdapter(
|
|
36
|
+
config=config, web3_service=mock_web3_service, simulation=True
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def test_adapter_type(self, adapter):
|
|
40
|
+
"""Test adapter has correct adapter_type"""
|
|
41
|
+
assert adapter.adapter_type == "MOONWELL"
|
|
42
|
+
|
|
43
|
+
def test_default_addresses(self, adapter):
|
|
44
|
+
"""Test default Moonwell addresses are set correctly"""
|
|
45
|
+
assert (
|
|
46
|
+
adapter.comptroller_address.lower()
|
|
47
|
+
== MOONWELL_DEFAULTS["comptroller"].lower()
|
|
48
|
+
)
|
|
49
|
+
assert (
|
|
50
|
+
adapter.reward_distributor_address.lower()
|
|
51
|
+
== MOONWELL_DEFAULTS["reward_distributor"].lower()
|
|
52
|
+
)
|
|
53
|
+
assert adapter.m_usdc.lower() == MOONWELL_DEFAULTS["m_usdc"].lower()
|
|
54
|
+
assert adapter.m_weth.lower() == MOONWELL_DEFAULTS["m_weth"].lower()
|
|
55
|
+
assert adapter.m_wsteth.lower() == MOONWELL_DEFAULTS["m_wsteth"].lower()
|
|
56
|
+
assert adapter.well_token.lower() == MOONWELL_DEFAULTS["well_token"].lower()
|
|
57
|
+
|
|
58
|
+
def test_chain_id(self, adapter):
|
|
59
|
+
"""Test default chain ID is Base"""
|
|
60
|
+
assert adapter.chain_id == BASE_CHAIN_ID
|
|
61
|
+
|
|
62
|
+
def test_chain_name(self, adapter):
|
|
63
|
+
"""Test chain name is base"""
|
|
64
|
+
assert adapter.chain_name == "base"
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_health_check(self, adapter):
|
|
68
|
+
"""Test adapter health check"""
|
|
69
|
+
health = await adapter.health_check()
|
|
70
|
+
assert isinstance(health, dict)
|
|
71
|
+
assert health.get("status") in {"healthy", "unhealthy", "error"}
|
|
72
|
+
assert health.get("adapter") == "MOONWELL"
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_connect(self, adapter):
|
|
76
|
+
"""Test adapter connection"""
|
|
77
|
+
ok = await adapter.connect()
|
|
78
|
+
assert isinstance(ok, bool)
|
|
79
|
+
assert ok is True
|
|
80
|
+
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def test_lend_simulation(self, adapter, mock_web3_service):
|
|
83
|
+
"""Test lend operation in simulation mode"""
|
|
84
|
+
# Mock the allowance check
|
|
85
|
+
mock_web3_service.token_transactions.read_erc20_allowance = AsyncMock(
|
|
86
|
+
return_value={"allowance": 10**18}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Mock contract encoding
|
|
90
|
+
mock_contract = MagicMock()
|
|
91
|
+
mock_contract.functions.mint = MagicMock(
|
|
92
|
+
return_value=MagicMock(
|
|
93
|
+
build_transaction=AsyncMock(return_value={"data": "0x1234"})
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
mock_web3 = MagicMock()
|
|
97
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
98
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
99
|
+
|
|
100
|
+
success, result = await adapter.lend(
|
|
101
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"],
|
|
102
|
+
underlying_token=MOONWELL_DEFAULTS["usdc"],
|
|
103
|
+
amount=10**6,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
assert success is True
|
|
107
|
+
assert "simulation" in result
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_lend_invalid_amount(self, adapter):
|
|
111
|
+
"""Test lend with invalid amount"""
|
|
112
|
+
success, result = await adapter.lend(
|
|
113
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"],
|
|
114
|
+
underlying_token=MOONWELL_DEFAULTS["usdc"],
|
|
115
|
+
amount=0,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
assert success is False
|
|
119
|
+
assert "positive" in result.lower()
|
|
120
|
+
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
async def test_unlend_simulation(self, adapter, mock_web3_service):
|
|
123
|
+
"""Test unlend operation in simulation mode"""
|
|
124
|
+
# Mock contract encoding
|
|
125
|
+
mock_contract = MagicMock()
|
|
126
|
+
mock_contract.functions.redeem = MagicMock(
|
|
127
|
+
return_value=MagicMock(
|
|
128
|
+
build_transaction=AsyncMock(return_value={"data": "0x1234"})
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
mock_web3 = MagicMock()
|
|
132
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
133
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
134
|
+
|
|
135
|
+
success, result = await adapter.unlend(
|
|
136
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"],
|
|
137
|
+
amount=10**8,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert success is True
|
|
141
|
+
assert "simulation" in result
|
|
142
|
+
|
|
143
|
+
@pytest.mark.asyncio
|
|
144
|
+
async def test_unlend_invalid_amount(self, adapter):
|
|
145
|
+
"""Test unlend with invalid amount"""
|
|
146
|
+
success, result = await adapter.unlend(
|
|
147
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"],
|
|
148
|
+
amount=-1,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
assert success is False
|
|
152
|
+
assert "positive" in result.lower()
|
|
153
|
+
|
|
154
|
+
@pytest.mark.asyncio
|
|
155
|
+
async def test_borrow_simulation(self, adapter, mock_web3_service):
|
|
156
|
+
"""Test borrow operation in simulation mode"""
|
|
157
|
+
# Mock contract encoding
|
|
158
|
+
mock_contract = MagicMock()
|
|
159
|
+
mock_contract.functions.borrow = MagicMock(
|
|
160
|
+
return_value=MagicMock(
|
|
161
|
+
build_transaction=AsyncMock(return_value={"data": "0x1234"})
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
mock_web3 = MagicMock()
|
|
165
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
166
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
167
|
+
|
|
168
|
+
success, result = await adapter.borrow(
|
|
169
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"],
|
|
170
|
+
amount=10**6,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
assert success is True
|
|
174
|
+
assert "simulation" in result
|
|
175
|
+
|
|
176
|
+
@pytest.mark.asyncio
|
|
177
|
+
async def test_repay_simulation(self, adapter, mock_web3_service):
|
|
178
|
+
"""Test repay operation in simulation mode"""
|
|
179
|
+
# Mock the allowance check
|
|
180
|
+
mock_web3_service.token_transactions.read_erc20_allowance = AsyncMock(
|
|
181
|
+
return_value={"allowance": 10**18}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Mock contract encoding
|
|
185
|
+
mock_contract = MagicMock()
|
|
186
|
+
mock_contract.functions.repayBorrow = MagicMock(
|
|
187
|
+
return_value=MagicMock(
|
|
188
|
+
build_transaction=AsyncMock(return_value={"data": "0x1234"})
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
mock_web3 = MagicMock()
|
|
192
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
193
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
194
|
+
|
|
195
|
+
success, result = await adapter.repay(
|
|
196
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"],
|
|
197
|
+
underlying_token=MOONWELL_DEFAULTS["usdc"],
|
|
198
|
+
amount=10**6,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
assert success is True
|
|
202
|
+
assert "simulation" in result
|
|
203
|
+
|
|
204
|
+
@pytest.mark.asyncio
|
|
205
|
+
async def test_set_collateral_simulation(self, adapter, mock_web3_service):
|
|
206
|
+
"""Test set collateral operation in simulation mode"""
|
|
207
|
+
# Mock contract encoding
|
|
208
|
+
mock_contract = MagicMock()
|
|
209
|
+
mock_contract.functions.enterMarkets = MagicMock(
|
|
210
|
+
return_value=MagicMock(
|
|
211
|
+
build_transaction=AsyncMock(return_value={"data": "0x1234"})
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
mock_web3 = MagicMock()
|
|
215
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
216
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
217
|
+
|
|
218
|
+
success, result = await adapter.set_collateral(
|
|
219
|
+
mtoken=MOONWELL_DEFAULTS["m_wsteth"],
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
assert success is True
|
|
223
|
+
assert "simulation" in result
|
|
224
|
+
|
|
225
|
+
@pytest.mark.asyncio
|
|
226
|
+
async def test_claim_rewards_simulation(self, adapter, mock_web3_service):
|
|
227
|
+
"""Test claim rewards operation in simulation mode"""
|
|
228
|
+
# Mock contract for getting outstanding rewards
|
|
229
|
+
mock_reward_contract = MagicMock()
|
|
230
|
+
mock_reward_contract.functions.getOutstandingRewardsForUser = MagicMock(
|
|
231
|
+
return_value=MagicMock(call=AsyncMock(return_value=[]))
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Mock contract for claiming (on comptroller)
|
|
235
|
+
mock_comptroller = MagicMock()
|
|
236
|
+
mock_comptroller.functions.claimReward = MagicMock(
|
|
237
|
+
return_value=MagicMock(
|
|
238
|
+
build_transaction=AsyncMock(return_value={"data": "0x1234"})
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def mock_contract(address, abi):
|
|
243
|
+
if address.lower() == adapter.reward_distributor_address.lower():
|
|
244
|
+
return mock_reward_contract
|
|
245
|
+
return mock_comptroller
|
|
246
|
+
|
|
247
|
+
mock_web3 = MagicMock()
|
|
248
|
+
mock_web3.eth.contract = MagicMock(side_effect=mock_contract)
|
|
249
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
250
|
+
|
|
251
|
+
success, result = await adapter.claim_rewards()
|
|
252
|
+
|
|
253
|
+
assert success is True
|
|
254
|
+
assert isinstance(result, dict)
|
|
255
|
+
|
|
256
|
+
@pytest.mark.asyncio
|
|
257
|
+
async def test_get_pos_success(self, adapter, mock_web3_service):
|
|
258
|
+
"""Test get position data"""
|
|
259
|
+
underlying_addr = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
260
|
+
|
|
261
|
+
# Mock mtoken contract calls
|
|
262
|
+
mock_mtoken = MagicMock()
|
|
263
|
+
mock_mtoken.functions.balanceOf = MagicMock(
|
|
264
|
+
return_value=MagicMock(call=AsyncMock(return_value=10**8))
|
|
265
|
+
)
|
|
266
|
+
mock_mtoken.functions.exchangeRateStored = MagicMock(
|
|
267
|
+
return_value=MagicMock(call=AsyncMock(return_value=2 * MANTISSA))
|
|
268
|
+
)
|
|
269
|
+
mock_mtoken.functions.borrowBalanceStored = MagicMock(
|
|
270
|
+
return_value=MagicMock(call=AsyncMock(return_value=10**6))
|
|
271
|
+
)
|
|
272
|
+
mock_mtoken.functions.underlying = MagicMock(
|
|
273
|
+
return_value=MagicMock(call=AsyncMock(return_value=underlying_addr))
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Mock reward distributor contract
|
|
277
|
+
mock_reward = MagicMock()
|
|
278
|
+
mock_reward.functions.getOutstandingRewardsForUser = MagicMock(
|
|
279
|
+
return_value=MagicMock(call=AsyncMock(return_value=[]))
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def mock_contract(address, abi):
|
|
283
|
+
if address.lower() == adapter.reward_distributor_address.lower():
|
|
284
|
+
return mock_reward
|
|
285
|
+
return mock_mtoken
|
|
286
|
+
|
|
287
|
+
mock_web3 = MagicMock()
|
|
288
|
+
mock_web3.eth.contract = MagicMock(side_effect=mock_contract)
|
|
289
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
290
|
+
|
|
291
|
+
success, result = await adapter.get_pos(mtoken=MOONWELL_DEFAULTS["m_usdc"])
|
|
292
|
+
|
|
293
|
+
assert success is True
|
|
294
|
+
assert "mtoken_balance" in result
|
|
295
|
+
assert "underlying_balance" in result
|
|
296
|
+
assert "borrow_balance" in result
|
|
297
|
+
assert "balances" in result
|
|
298
|
+
assert result["mtoken_balance"] == 10**8
|
|
299
|
+
assert result["borrow_balance"] == 10**6
|
|
300
|
+
|
|
301
|
+
@pytest.mark.asyncio
|
|
302
|
+
async def test_get_collateral_factor_success(self, adapter, mock_web3_service):
|
|
303
|
+
"""Test get collateral factor"""
|
|
304
|
+
# Mock contract calls - returns (isListed, collateralFactorMantissa)
|
|
305
|
+
mock_contract = MagicMock()
|
|
306
|
+
mock_contract.functions.markets = MagicMock(
|
|
307
|
+
return_value=MagicMock(
|
|
308
|
+
call=AsyncMock(return_value=(True, int(0.75 * MANTISSA)))
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
mock_web3 = MagicMock()
|
|
312
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
313
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
314
|
+
|
|
315
|
+
success, result = await adapter.get_collateral_factor(
|
|
316
|
+
mtoken=MOONWELL_DEFAULTS["m_wsteth"]
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
assert success is True
|
|
320
|
+
assert result == 0.75
|
|
321
|
+
|
|
322
|
+
@pytest.mark.asyncio
|
|
323
|
+
async def test_get_collateral_factor_not_listed(self, adapter, mock_web3_service):
|
|
324
|
+
"""Test get collateral factor for unlisted market"""
|
|
325
|
+
mock_contract = MagicMock()
|
|
326
|
+
mock_contract.functions.markets = MagicMock(
|
|
327
|
+
return_value=MagicMock(call=AsyncMock(return_value=(False, 0)))
|
|
328
|
+
)
|
|
329
|
+
mock_web3 = MagicMock()
|
|
330
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
331
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
332
|
+
|
|
333
|
+
success, result = await adapter.get_collateral_factor(
|
|
334
|
+
mtoken="0x0000000000000000000000000000000000000001"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
assert success is False
|
|
338
|
+
assert "not listed" in result.lower()
|
|
339
|
+
|
|
340
|
+
@pytest.mark.asyncio
|
|
341
|
+
async def test_get_collateral_factor_caching(self, adapter, mock_web3_service):
|
|
342
|
+
"""Test that collateral factor is cached and subsequent calls don't hit RPC"""
|
|
343
|
+
call_count = 0
|
|
344
|
+
|
|
345
|
+
async def mock_markets_call():
|
|
346
|
+
nonlocal call_count
|
|
347
|
+
call_count += 1
|
|
348
|
+
return (True, int(0.80 * MANTISSA))
|
|
349
|
+
|
|
350
|
+
mock_contract = MagicMock()
|
|
351
|
+
mock_contract.functions.markets = MagicMock(
|
|
352
|
+
return_value=MagicMock(call=mock_markets_call)
|
|
353
|
+
)
|
|
354
|
+
mock_web3 = MagicMock()
|
|
355
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
356
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
357
|
+
|
|
358
|
+
mtoken = MOONWELL_DEFAULTS["m_wsteth"]
|
|
359
|
+
|
|
360
|
+
# First call should hit RPC
|
|
361
|
+
success1, result1 = await adapter.get_collateral_factor(mtoken=mtoken)
|
|
362
|
+
assert success1 is True
|
|
363
|
+
assert result1 == 0.80
|
|
364
|
+
assert call_count == 1
|
|
365
|
+
|
|
366
|
+
# Second call should use cache (no additional RPC call)
|
|
367
|
+
success2, result2 = await adapter.get_collateral_factor(mtoken=mtoken)
|
|
368
|
+
assert success2 is True
|
|
369
|
+
assert result2 == 0.80
|
|
370
|
+
assert call_count == 1 # Still 1, cache was used
|
|
371
|
+
|
|
372
|
+
# Third call for same mtoken should still use cache
|
|
373
|
+
success3, result3 = await adapter.get_collateral_factor(mtoken=mtoken)
|
|
374
|
+
assert success3 is True
|
|
375
|
+
assert result3 == 0.80
|
|
376
|
+
assert call_count == 1 # Still 1
|
|
377
|
+
|
|
378
|
+
# Call for different mtoken should hit RPC
|
|
379
|
+
success4, result4 = await adapter.get_collateral_factor(
|
|
380
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"]
|
|
381
|
+
)
|
|
382
|
+
assert success4 is True
|
|
383
|
+
assert call_count == 2 # Incremented for new mtoken
|
|
384
|
+
|
|
385
|
+
@pytest.mark.asyncio
|
|
386
|
+
async def test_get_collateral_factor_cache_expiry(self, adapter, mock_web3_service):
|
|
387
|
+
"""Test that collateral factor cache expires after TTL"""
|
|
388
|
+
import time
|
|
389
|
+
|
|
390
|
+
from wayfinder_paths.adapters.moonwell_adapter import adapter as adapter_module
|
|
391
|
+
|
|
392
|
+
# Save original TTL
|
|
393
|
+
original_ttl = adapter_module.CF_CACHE_TTL
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
# Set a very short TTL for testing
|
|
397
|
+
adapter_module.CF_CACHE_TTL = 0.1 # 100ms
|
|
398
|
+
|
|
399
|
+
call_count = 0
|
|
400
|
+
|
|
401
|
+
async def mock_markets_call():
|
|
402
|
+
nonlocal call_count
|
|
403
|
+
call_count += 1
|
|
404
|
+
return (True, int(0.75 * MANTISSA))
|
|
405
|
+
|
|
406
|
+
mock_contract = MagicMock()
|
|
407
|
+
mock_contract.functions.markets = MagicMock(
|
|
408
|
+
return_value=MagicMock(call=mock_markets_call)
|
|
409
|
+
)
|
|
410
|
+
mock_web3 = MagicMock()
|
|
411
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
412
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
413
|
+
|
|
414
|
+
mtoken = MOONWELL_DEFAULTS["m_wsteth"]
|
|
415
|
+
|
|
416
|
+
# First call
|
|
417
|
+
await adapter.get_collateral_factor(mtoken=mtoken)
|
|
418
|
+
assert call_count == 1
|
|
419
|
+
|
|
420
|
+
# Immediate second call should use cache
|
|
421
|
+
await adapter.get_collateral_factor(mtoken=mtoken)
|
|
422
|
+
assert call_count == 1
|
|
423
|
+
|
|
424
|
+
# Wait for cache to expire
|
|
425
|
+
time.sleep(0.15)
|
|
426
|
+
|
|
427
|
+
# Call after expiry should hit RPC again
|
|
428
|
+
await adapter.get_collateral_factor(mtoken=mtoken)
|
|
429
|
+
assert call_count == 2
|
|
430
|
+
|
|
431
|
+
finally:
|
|
432
|
+
# Restore original TTL
|
|
433
|
+
adapter_module.CF_CACHE_TTL = original_ttl
|
|
434
|
+
|
|
435
|
+
@pytest.mark.asyncio
|
|
436
|
+
async def test_get_apy_supply(self, adapter, mock_web3_service):
|
|
437
|
+
"""Test get supply APY"""
|
|
438
|
+
rate_per_second = int(1.5e9)
|
|
439
|
+
|
|
440
|
+
# Mock mtoken contract
|
|
441
|
+
mock_mtoken = MagicMock()
|
|
442
|
+
mock_mtoken.functions.supplyRatePerTimestamp = MagicMock(
|
|
443
|
+
return_value=MagicMock(call=AsyncMock(return_value=rate_per_second))
|
|
444
|
+
)
|
|
445
|
+
mock_mtoken.functions.totalSupply = MagicMock(
|
|
446
|
+
return_value=MagicMock(call=AsyncMock(return_value=10**18))
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Mock reward distributor
|
|
450
|
+
mock_reward = MagicMock()
|
|
451
|
+
mock_reward.functions.getAllMarketConfigs = MagicMock(
|
|
452
|
+
return_value=MagicMock(call=AsyncMock(return_value=[]))
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def mock_contract(address, abi):
|
|
456
|
+
if address.lower() == adapter.reward_distributor_address.lower():
|
|
457
|
+
return mock_reward
|
|
458
|
+
return mock_mtoken
|
|
459
|
+
|
|
460
|
+
mock_web3 = MagicMock()
|
|
461
|
+
mock_web3.eth.contract = MagicMock(side_effect=mock_contract)
|
|
462
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
463
|
+
|
|
464
|
+
success, result = await adapter.get_apy(
|
|
465
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"], apy_type="supply", include_rewards=False
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
assert success is True
|
|
469
|
+
assert isinstance(result, float)
|
|
470
|
+
assert result >= 0
|
|
471
|
+
|
|
472
|
+
@pytest.mark.asyncio
|
|
473
|
+
async def test_get_apy_borrow(self, adapter, mock_web3_service):
|
|
474
|
+
"""Test get borrow APY"""
|
|
475
|
+
rate_per_second = int(2e9)
|
|
476
|
+
|
|
477
|
+
# Mock mtoken contract
|
|
478
|
+
mock_mtoken = MagicMock()
|
|
479
|
+
mock_mtoken.functions.borrowRatePerTimestamp = MagicMock(
|
|
480
|
+
return_value=MagicMock(call=AsyncMock(return_value=rate_per_second))
|
|
481
|
+
)
|
|
482
|
+
mock_mtoken.functions.totalBorrows = MagicMock(
|
|
483
|
+
return_value=MagicMock(call=AsyncMock(return_value=10**18))
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Mock reward distributor
|
|
487
|
+
mock_reward = MagicMock()
|
|
488
|
+
mock_reward.functions.getAllMarketConfigs = MagicMock(
|
|
489
|
+
return_value=MagicMock(call=AsyncMock(return_value=[]))
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def mock_contract(address, abi):
|
|
493
|
+
if address.lower() == adapter.reward_distributor_address.lower():
|
|
494
|
+
return mock_reward
|
|
495
|
+
return mock_mtoken
|
|
496
|
+
|
|
497
|
+
mock_web3 = MagicMock()
|
|
498
|
+
mock_web3.eth.contract = MagicMock(side_effect=mock_contract)
|
|
499
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
500
|
+
|
|
501
|
+
success, result = await adapter.get_apy(
|
|
502
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"], apy_type="borrow", include_rewards=False
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
assert success is True
|
|
506
|
+
assert isinstance(result, float)
|
|
507
|
+
assert result >= 0
|
|
508
|
+
|
|
509
|
+
@pytest.mark.asyncio
|
|
510
|
+
async def test_get_borrowable_amount_success(self, adapter, mock_web3_service):
|
|
511
|
+
"""Test get borrowable amount"""
|
|
512
|
+
mock_contract = MagicMock()
|
|
513
|
+
mock_contract.functions.getAccountLiquidity = MagicMock(
|
|
514
|
+
return_value=MagicMock(
|
|
515
|
+
call=AsyncMock(
|
|
516
|
+
return_value=(0, 10**18, 0)
|
|
517
|
+
) # error, liquidity, shortfall
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
mock_web3 = MagicMock()
|
|
521
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
522
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
523
|
+
|
|
524
|
+
success, result = await adapter.get_borrowable_amount()
|
|
525
|
+
|
|
526
|
+
assert success is True
|
|
527
|
+
assert result == 10**18
|
|
528
|
+
|
|
529
|
+
@pytest.mark.asyncio
|
|
530
|
+
async def test_get_borrowable_amount_shortfall(self, adapter, mock_web3_service):
|
|
531
|
+
"""Test get borrowable amount when account has shortfall"""
|
|
532
|
+
mock_contract = MagicMock()
|
|
533
|
+
mock_contract.functions.getAccountLiquidity = MagicMock(
|
|
534
|
+
return_value=MagicMock(
|
|
535
|
+
call=AsyncMock(return_value=(0, 0, 10**16)) # has shortfall
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
mock_web3 = MagicMock()
|
|
539
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
540
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
541
|
+
|
|
542
|
+
success, result = await adapter.get_borrowable_amount()
|
|
543
|
+
|
|
544
|
+
assert success is False
|
|
545
|
+
assert "shortfall" in result.lower()
|
|
546
|
+
|
|
547
|
+
@pytest.mark.asyncio
|
|
548
|
+
async def test_wrap_eth_simulation(self, adapter, mock_web3_service):
|
|
549
|
+
"""Test wrap ETH operation in simulation mode"""
|
|
550
|
+
mock_contract = MagicMock()
|
|
551
|
+
mock_contract.functions.deposit = MagicMock(
|
|
552
|
+
return_value=MagicMock(
|
|
553
|
+
build_transaction=AsyncMock(return_value={"data": "0x1234"})
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
mock_web3 = MagicMock()
|
|
557
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_contract)
|
|
558
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
559
|
+
|
|
560
|
+
success, result = await adapter.wrap_eth(amount=10**18)
|
|
561
|
+
|
|
562
|
+
assert success is True
|
|
563
|
+
assert "simulation" in result
|
|
564
|
+
|
|
565
|
+
def test_strategy_address_missing(self):
|
|
566
|
+
"""Test error when strategy wallet is missing"""
|
|
567
|
+
adapter = MoonwellAdapter(config={}, simulation=True)
|
|
568
|
+
|
|
569
|
+
with pytest.raises(ValueError, match="strategy_wallet"):
|
|
570
|
+
adapter._strategy_address()
|
|
571
|
+
|
|
572
|
+
def test_checksum_missing_address(self, adapter):
|
|
573
|
+
"""Test error when address is missing"""
|
|
574
|
+
with pytest.raises(ValueError, match="Missing required"):
|
|
575
|
+
adapter._checksum(None)
|
|
576
|
+
|
|
577
|
+
def test_config_override(self, mock_web3_service):
|
|
578
|
+
"""Test config can override default addresses"""
|
|
579
|
+
custom_comptroller = "0x1111111111111111111111111111111111111111"
|
|
580
|
+
custom_well = "0x2222222222222222222222222222222222222222"
|
|
581
|
+
config = {
|
|
582
|
+
"strategy_wallet": {
|
|
583
|
+
"address": "0x1234567890123456789012345678901234567890"
|
|
584
|
+
},
|
|
585
|
+
"moonwell_adapter": {
|
|
586
|
+
"comptroller": custom_comptroller,
|
|
587
|
+
"well_token": custom_well,
|
|
588
|
+
"chain_id": 1,
|
|
589
|
+
},
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
adapter = MoonwellAdapter(
|
|
593
|
+
config=config, web3_service=mock_web3_service, simulation=True
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
assert adapter.comptroller_address.lower() == custom_comptroller.lower()
|
|
597
|
+
assert adapter.well_token.lower() == custom_well.lower()
|
|
598
|
+
assert adapter.chain_id == 1
|
|
599
|
+
|
|
600
|
+
@pytest.mark.asyncio
|
|
601
|
+
async def test_max_withdrawable_mtoken_zero_balance(
|
|
602
|
+
self, adapter, mock_web3_service
|
|
603
|
+
):
|
|
604
|
+
"""Test max withdrawable when balance is zero"""
|
|
605
|
+
# Mock contracts
|
|
606
|
+
mock_mtoken = MagicMock()
|
|
607
|
+
mock_mtoken.functions.balanceOf = MagicMock(
|
|
608
|
+
return_value=MagicMock(call=AsyncMock(return_value=0))
|
|
609
|
+
)
|
|
610
|
+
mock_mtoken.functions.exchangeRateStored = MagicMock(
|
|
611
|
+
return_value=MagicMock(call=AsyncMock(return_value=MANTISSA))
|
|
612
|
+
)
|
|
613
|
+
mock_mtoken.functions.getCash = MagicMock(
|
|
614
|
+
return_value=MagicMock(call=AsyncMock(return_value=10**18))
|
|
615
|
+
)
|
|
616
|
+
mock_mtoken.functions.decimals = MagicMock(
|
|
617
|
+
return_value=MagicMock(call=AsyncMock(return_value=8))
|
|
618
|
+
)
|
|
619
|
+
mock_mtoken.functions.underlying = MagicMock(
|
|
620
|
+
return_value=MagicMock(
|
|
621
|
+
call=AsyncMock(return_value=MOONWELL_DEFAULTS["usdc"])
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
mock_web3 = MagicMock()
|
|
626
|
+
mock_web3.eth.contract = MagicMock(return_value=mock_mtoken)
|
|
627
|
+
mock_web3_service.get_web3 = MagicMock(return_value=mock_web3)
|
|
628
|
+
|
|
629
|
+
success, result = await adapter.max_withdrawable_mtoken(
|
|
630
|
+
mtoken=MOONWELL_DEFAULTS["m_usdc"]
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
assert success is True
|
|
634
|
+
assert result["cTokens_raw"] == 0
|
|
635
|
+
assert result["underlying_raw"] == 0
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
4
|
from loguru import logger
|
|
@@ -29,6 +30,8 @@ class AuthClient(WayfinderClient):
|
|
|
29
30
|
creds = self._load_config_credentials()
|
|
30
31
|
if creds.get("api_key"):
|
|
31
32
|
return True
|
|
33
|
+
if os.getenv("WAYFINDER_API_KEY"):
|
|
34
|
+
return True
|
|
32
35
|
except Exception:
|
|
33
36
|
pass
|
|
34
37
|
|
|
@@ -131,7 +131,7 @@ class WayfinderClient:
|
|
|
131
131
|
api_key = self._api_key
|
|
132
132
|
if not api_key:
|
|
133
133
|
creds = self._load_config_credentials()
|
|
134
|
-
api_key = creds.get("api_key")
|
|
134
|
+
api_key = creds.get("api_key") or os.getenv("WAYFINDER_API_KEY")
|
|
135
135
|
|
|
136
136
|
if api_key:
|
|
137
137
|
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
@@ -200,7 +200,7 @@ class WayfinderClient:
|
|
|
200
200
|
api_key = self._api_key
|
|
201
201
|
if not api_key:
|
|
202
202
|
creds = self._load_config_credentials()
|
|
203
|
-
api_key = creds.get("api_key")
|
|
203
|
+
api_key = creds.get("api_key") or os.getenv("WAYFINDER_API_KEY")
|
|
204
204
|
|
|
205
205
|
if api_key:
|
|
206
206
|
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
@@ -7,7 +7,6 @@ This package contains all constants used across the system, organized by categor
|
|
|
7
7
|
|
|
8
8
|
from .base import (
|
|
9
9
|
CHAIN_CODE_TO_ID,
|
|
10
|
-
DEFAULT_GAS_ESTIMATE_FALLBACK,
|
|
11
10
|
DEFAULT_NATIVE_GAS_UNITS,
|
|
12
11
|
DEFAULT_SLIPPAGE,
|
|
13
12
|
GAS_BUFFER_MULTIPLIER,
|
|
@@ -19,7 +18,6 @@ __all__ = [
|
|
|
19
18
|
"ZERO_ADDRESS",
|
|
20
19
|
"CHAIN_CODE_TO_ID",
|
|
21
20
|
"DEFAULT_NATIVE_GAS_UNITS",
|
|
22
|
-
"DEFAULT_GAS_ESTIMATE_FALLBACK",
|
|
23
21
|
"GAS_BUFFER_MULTIPLIER",
|
|
24
22
|
"ONE_GWEI",
|
|
25
23
|
"DEFAULT_SLIPPAGE",
|