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.

Files changed (54) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +1 -2
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +4 -4
  3. wayfinder_paths/adapters/brap_adapter/adapter.py +139 -23
  4. wayfinder_paths/adapters/moonwell_adapter/README.md +174 -0
  5. wayfinder_paths/adapters/moonwell_adapter/__init__.py +7 -0
  6. wayfinder_paths/adapters/moonwell_adapter/adapter.py +1226 -0
  7. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +635 -0
  8. wayfinder_paths/core/clients/AuthClient.py +3 -0
  9. wayfinder_paths/core/clients/WayfinderClient.py +2 -2
  10. wayfinder_paths/core/constants/__init__.py +0 -2
  11. wayfinder_paths/core/constants/base.py +6 -2
  12. wayfinder_paths/core/constants/moonwell_abi.py +411 -0
  13. wayfinder_paths/core/engine/StrategyJob.py +3 -0
  14. wayfinder_paths/core/services/local_evm_txn.py +182 -217
  15. wayfinder_paths/core/services/local_token_txn.py +46 -26
  16. wayfinder_paths/core/strategies/descriptors.py +1 -1
  17. wayfinder_paths/core/utils/evm_helpers.py +0 -27
  18. wayfinder_paths/run_strategy.py +34 -74
  19. wayfinder_paths/scripts/create_strategy.py +2 -27
  20. wayfinder_paths/scripts/run_strategy.py +37 -7
  21. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +1 -1
  22. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -15
  23. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -1
  24. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +108 -0
  25. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/examples.json +11 -0
  26. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2975 -0
  27. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +886 -0
  28. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -7
  29. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -1
  30. wayfinder_paths/templates/adapter/README.md +5 -21
  31. wayfinder_paths/templates/adapter/adapter.py +1 -2
  32. wayfinder_paths/templates/adapter/test_adapter.py +1 -1
  33. wayfinder_paths/templates/strategy/README.md +4 -21
  34. wayfinder_paths/tests/test_smoke_manifest.py +17 -2
  35. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/METADATA +60 -187
  36. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/RECORD +38 -45
  37. wayfinder_paths/CONFIG_GUIDE.md +0 -390
  38. wayfinder_paths/adapters/balance_adapter/manifest.yaml +0 -8
  39. wayfinder_paths/adapters/brap_adapter/manifest.yaml +0 -11
  40. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +0 -10
  41. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +0 -8
  42. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +0 -11
  43. wayfinder_paths/adapters/pool_adapter/manifest.yaml +0 -10
  44. wayfinder_paths/adapters/token_adapter/manifest.yaml +0 -6
  45. wayfinder_paths/config.example.json +0 -22
  46. wayfinder_paths/core/engine/manifest.py +0 -97
  47. wayfinder_paths/scripts/validate_manifests.py +0 -213
  48. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +0 -23
  49. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +0 -7
  50. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +0 -17
  51. wayfinder_paths/templates/adapter/manifest.yaml +0 -6
  52. wayfinder_paths/templates/strategy/manifest.yaml +0 -8
  53. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/LICENSE +0 -0
  54. {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",