iwa 0.0.62__py3-none-any.whl → 0.0.64__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.
@@ -1,19 +1,26 @@
1
+ """Tests for StakingContract."""
2
+
3
+ import time
4
+ from datetime import datetime, timezone
1
5
  from unittest.mock import MagicMock, mock_open, patch
2
6
 
3
- from iwa.plugins.olas.contracts.staking import StakingContract
7
+ import pytest
8
+
9
+ from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
4
10
 
5
11
 
6
- def test_staking_contract_coverage():
12
+ @pytest.fixture
13
+ def mock_staking_contract():
14
+ """Create a mocked StakingContract instance."""
7
15
  with (
8
16
  patch("iwa.core.contracts.contract.ChainInterfaces") as mock_chains,
9
17
  patch("iwa.plugins.olas.contracts.staking.ActivityCheckerContract"),
10
18
  patch("builtins.open", mock_open(read_data="[]")),
11
19
  ):
12
- # Setup mocks
13
20
  mock_interface = MagicMock()
14
21
  mock_chains.return_value.get.return_value = mock_interface
15
22
 
16
- # Mock contract calls in __init__
23
+ # Mock contract calls
17
24
  mock_interface.call_contract.side_effect = lambda method, *args: {
18
25
  "activityChecker": "0xChecker",
19
26
  "availableRewards": 100,
@@ -24,8 +31,473 @@ def test_staking_contract_coverage():
24
31
  "minStakingDeposit": 100,
25
32
  "minStakingDuration": 86400,
26
33
  "stakingToken": "0xToken",
34
+ "epochCounter": 5,
35
+ "getNextRewardCheckpointTimestamp": int(time.time()) + 3600,
36
+ "getServiceIds": [1, 2, 3],
37
+ "getStakingState": 1, # STAKED
38
+ "calculateStakingLastReward": 500,
39
+ "calculateStakingReward": 600,
40
+ "tsCheckpoint": int(time.time()) - 1000,
41
+ "mapServiceInfo": ("0xMultisig", "0xOwner", (10, 5), 1000, 750, 0),
42
+ "getServiceInfo": (
43
+ "0xMultisig",
44
+ "0xOwner",
45
+ (10, 5),
46
+ int(time.time()) - 1000,
47
+ 750,
48
+ 0,
49
+ ),
27
50
  }.get(method, 0)
28
51
 
29
- # Instantiate (This covers __init__ logic)
30
52
  contract = StakingContract(address="0x123")
31
- assert contract.address == "0x123"
53
+ contract._interface = mock_interface
54
+ yield contract
55
+
56
+
57
+ class TestStakingContractInit:
58
+ """Test StakingContract initialization."""
59
+
60
+ def test_basic_init(self):
61
+ with (
62
+ patch("iwa.core.contracts.contract.ChainInterfaces") as mock_chains,
63
+ patch("iwa.plugins.olas.contracts.staking.ActivityCheckerContract"),
64
+ patch("builtins.open", mock_open(read_data="[]")),
65
+ ):
66
+ mock_interface = MagicMock()
67
+ mock_chains.return_value.get.return_value = mock_interface
68
+ mock_interface.call_contract.return_value = 0
69
+
70
+ contract = StakingContract(address="0x123")
71
+ assert contract.address == "0x123"
72
+ assert contract.chain_name == "gnosis"
73
+
74
+
75
+ class TestStakingState:
76
+ """Test StakingState enum."""
77
+
78
+ def test_not_staked(self):
79
+ assert StakingState.NOT_STAKED.value == 0
80
+
81
+ def test_staked(self):
82
+ assert StakingState.STAKED.value == 1
83
+
84
+ def test_evicted(self):
85
+ assert StakingState.EVICTED.value == 2
86
+
87
+
88
+ class TestGetRequirements:
89
+ """Test get_requirements method."""
90
+
91
+ def test_returns_dict_with_required_fields(self, mock_staking_contract):
92
+ # Use cache to set property values
93
+ mock_staking_contract._contract_params_cache = {
94
+ "stakingToken": "0xOLAS",
95
+ "minStakingDeposit": 50,
96
+ }
97
+
98
+ result = mock_staking_contract.get_requirements()
99
+
100
+ assert "staking_token" in result
101
+ assert "min_staking_deposit" in result
102
+ assert "required_agent_bond" in result
103
+ assert result["staking_token"] == "0xOLAS"
104
+ assert result["min_staking_deposit"] == 50
105
+ assert result["required_agent_bond"] == 50
106
+
107
+
108
+ class TestCalculationMethods:
109
+ """Test reward calculation methods."""
110
+
111
+ def test_calculate_accrued_staking_reward(self, mock_staking_contract):
112
+ mock_staking_contract.call = MagicMock(return_value=500)
113
+ result = mock_staking_contract.calculate_accrued_staking_reward(1)
114
+ assert result == 500
115
+ mock_staking_contract.call.assert_called_with("calculateStakingLastReward", 1)
116
+
117
+ def test_calculate_staking_reward(self, mock_staking_contract):
118
+ mock_staking_contract.call = MagicMock(return_value=600)
119
+ result = mock_staking_contract.calculate_staking_reward(1)
120
+ assert result == 600
121
+ mock_staking_contract.call.assert_called_with("calculateStakingReward", 1)
122
+
123
+ def test_get_epoch_counter(self, mock_staking_contract):
124
+ mock_staking_contract.call = MagicMock(return_value=5)
125
+ result = mock_staking_contract.get_epoch_counter()
126
+ assert result == 5
127
+ mock_staking_contract.call.assert_called_with("epochCounter")
128
+
129
+ def test_get_service_ids(self, mock_staking_contract):
130
+ mock_staking_contract.call = MagicMock(return_value=[1, 2, 3])
131
+ result = mock_staking_contract.get_service_ids()
132
+ assert result == [1, 2, 3]
133
+
134
+
135
+ class TestGetNextEpochStart:
136
+ """Test get_next_epoch_start method."""
137
+
138
+ def test_returns_datetime(self, mock_staking_contract):
139
+ future_ts = int(time.time()) + 3600
140
+ mock_staking_contract.call = MagicMock(return_value=future_ts)
141
+ result = mock_staking_contract.get_next_epoch_start()
142
+ assert isinstance(result, datetime)
143
+ assert result.tzinfo == timezone.utc
144
+
145
+
146
+ class TestGetStakingState:
147
+ """Test get_staking_state method."""
148
+
149
+ def test_returns_staked(self, mock_staking_contract):
150
+ mock_staking_contract.call = MagicMock(return_value=1)
151
+ result = mock_staking_contract.get_staking_state(1)
152
+ assert result == StakingState.STAKED
153
+
154
+ def test_returns_not_staked(self, mock_staking_contract):
155
+ mock_staking_contract.call = MagicMock(return_value=0)
156
+ result = mock_staking_contract.get_staking_state(1)
157
+ assert result == StakingState.NOT_STAKED
158
+
159
+ def test_returns_evicted(self, mock_staking_contract):
160
+ mock_staking_contract.call = MagicMock(return_value=2)
161
+ result = mock_staking_contract.get_staking_state(1)
162
+ assert result == StakingState.EVICTED
163
+
164
+
165
+ class TestTsCheckpoint:
166
+ """Test ts_checkpoint caching."""
167
+
168
+ def test_caches_value(self, mock_staking_contract):
169
+ ts = int(time.time()) - 100
170
+ mock_staking_contract.call = MagicMock(return_value=ts)
171
+ mock_staking_contract._contract_params_cache = {"livenessPeriod": 3600}
172
+
173
+ # First call fetches
174
+ result1 = mock_staking_contract.ts_checkpoint()
175
+ assert result1 == ts
176
+ assert mock_staking_contract.call.call_count == 1
177
+
178
+ # Second call uses cache (within liveness_period)
179
+ result2 = mock_staking_contract.ts_checkpoint()
180
+ assert result2 == ts
181
+ # Should still be 1 call (cached)
182
+ assert mock_staking_contract.call.call_count == 1
183
+
184
+ def test_clear_epoch_cache(self, mock_staking_contract):
185
+ mock_staking_contract._contract_params_cache = {
186
+ "ts_checkpoint": 1000,
187
+ "ts_checkpoint_last_checked": 900,
188
+ "other_key": "value",
189
+ }
190
+
191
+ mock_staking_contract.clear_epoch_cache()
192
+
193
+ assert "ts_checkpoint" not in mock_staking_contract._contract_params_cache
194
+ assert "ts_checkpoint_last_checked" not in mock_staking_contract._contract_params_cache
195
+ assert "other_key" in mock_staking_contract._contract_params_cache
196
+
197
+
198
+ class TestGetRequiredRequests:
199
+ """Test get_required_requests method."""
200
+
201
+ def test_with_liveness_period(self, mock_staking_contract):
202
+ mock_staking_contract._contract_params_cache = {"livenessPeriod": 86400}
203
+ mock_activity_checker = MagicMock()
204
+ mock_activity_checker.liveness_ratio = 1e18 # 1 request per second
205
+ mock_staking_contract._activity_checker = mock_activity_checker
206
+
207
+ result = mock_staking_contract.get_required_requests(use_liveness_period=True)
208
+
209
+ # Should be ceiling of (86400 * 1e18 / 1e18) + 1 = 86401
210
+ assert result == 86401
211
+
212
+
213
+ class TestProperties:
214
+ """Test cached properties."""
215
+
216
+ def test_available_rewards(self, mock_staking_contract):
217
+ mock_staking_contract.call = MagicMock(return_value=1000)
218
+ mock_staking_contract._contract_params_cache = {}
219
+
220
+ result = mock_staking_contract.available_rewards
221
+ assert result == 1000
222
+ mock_staking_contract.call.assert_called_with("availableRewards")
223
+
224
+ def test_balance(self, mock_staking_contract):
225
+ mock_staking_contract.call = MagicMock(return_value=5000)
226
+ mock_staking_contract._contract_params_cache = {}
227
+
228
+ result = mock_staking_contract.balance
229
+ assert result == 5000
230
+
231
+ def test_liveness_period(self, mock_staking_contract):
232
+ mock_staking_contract.call = MagicMock(return_value=86400)
233
+ mock_staking_contract._contract_params_cache = {}
234
+
235
+ result = mock_staking_contract.liveness_period
236
+ assert result == 86400
237
+
238
+ def test_rewards_per_second(self, mock_staking_contract):
239
+ mock_staking_contract.call = MagicMock(return_value=100)
240
+ mock_staking_contract._contract_params_cache = {}
241
+
242
+ result = mock_staking_contract.rewards_per_second
243
+ assert result == 100
244
+
245
+ def test_max_num_services(self, mock_staking_contract):
246
+ mock_staking_contract.call = MagicMock(return_value=50)
247
+ mock_staking_contract._contract_params_cache = {}
248
+
249
+ result = mock_staking_contract.max_num_services
250
+ assert result == 50
251
+
252
+ def test_min_staking_deposit(self, mock_staking_contract):
253
+ mock_staking_contract.call = MagicMock(return_value=100)
254
+ mock_staking_contract._contract_params_cache = {}
255
+
256
+ result = mock_staking_contract.min_staking_deposit
257
+ assert result == 100
258
+
259
+ def test_min_staking_duration_hours(self, mock_staking_contract):
260
+ mock_staking_contract._contract_params_cache = {"minStakingDuration": 7200}
261
+ result = mock_staking_contract.min_staking_duration_hours
262
+ assert result == 2.0 # 7200 / 3600
263
+
264
+ def test_staking_token_address(self, mock_staking_contract):
265
+ mock_staking_contract.call = MagicMock(return_value="0xOLAS")
266
+ mock_staking_contract._contract_params_cache = {}
267
+
268
+ result = mock_staking_contract.staking_token_address
269
+ assert result == "0xOLAS"
270
+
271
+ def test_min_staking_duration(self, mock_staking_contract):
272
+ mock_staking_contract.call = MagicMock(return_value=86400)
273
+ mock_staking_contract._contract_params_cache = {}
274
+
275
+ result = mock_staking_contract.min_staking_duration
276
+ assert result == 86400
277
+
278
+
279
+ class TestActivityChecker:
280
+ """Test activity checker related methods."""
281
+
282
+ def test_activity_checker_address_value(self, mock_staking_contract):
283
+ mock_staking_contract.call = MagicMock(return_value="0xChecker")
284
+ mock_staking_contract._activity_checker_address = None
285
+
286
+ result = mock_staking_contract.activity_checker_address_value
287
+ assert result == "0xChecker"
288
+
289
+ def test_activity_checker_address_backwards_compat(self, mock_staking_contract):
290
+ mock_staking_contract._activity_checker_address = "0xChecker"
291
+ result = mock_staking_contract.activity_checker_address
292
+ assert result == "0xChecker"
293
+
294
+ def test_activity_checker_lazy_load(self, mock_staking_contract):
295
+ mock_staking_contract._activity_checker = None
296
+ mock_staking_contract._activity_checker_address = "0xChecker"
297
+
298
+ with patch(
299
+ "iwa.plugins.olas.contracts.staking.ActivityCheckerContract"
300
+ ) as mock_ac_cls:
301
+ mock_ac = MagicMock()
302
+ mock_ac_cls.return_value = mock_ac
303
+
304
+ result = mock_staking_contract.activity_checker
305
+ assert result == mock_ac
306
+ mock_ac_cls.assert_called_with("0xChecker", chain_name="gnosis")
307
+
308
+
309
+ class TestIsLivenessRatioPassed:
310
+ """Test is_liveness_ratio_passed method."""
311
+
312
+ def test_returns_false_for_zero_time_diff(self, mock_staking_contract):
313
+ current_nonces = (10, 5)
314
+ last_nonces = (8, 3)
315
+ # ts_start in future = negative time diff
316
+ ts_start = int(time.time()) + 100
317
+
318
+ result = mock_staking_contract.is_liveness_ratio_passed(
319
+ current_nonces, last_nonces, ts_start
320
+ )
321
+ assert result is False
322
+
323
+ def test_calls_activity_checker(self, mock_staking_contract):
324
+ mock_ac = MagicMock()
325
+ mock_ac.is_ratio_pass.return_value = True
326
+ mock_staking_contract._activity_checker = mock_ac
327
+
328
+ current_nonces = (10, 5)
329
+ last_nonces = (8, 3)
330
+ ts_start = int(time.time()) - 1000
331
+
332
+ result = mock_staking_contract.is_liveness_ratio_passed(
333
+ current_nonces, last_nonces, ts_start
334
+ )
335
+
336
+ assert result is True
337
+ mock_ac.is_ratio_pass.assert_called_once()
338
+
339
+
340
+ class TestPrepareTxMethods:
341
+ """Test transaction preparation methods."""
342
+
343
+ def test_prepare_stake_tx(self, mock_staking_contract):
344
+ mock_staking_contract.prepare_transaction = MagicMock(return_value={"to": "0x"})
345
+
346
+ result = mock_staking_contract.prepare_stake_tx("0xOwner", 1)
347
+
348
+ assert result == {"to": "0x"}
349
+ mock_staking_contract.prepare_transaction.assert_called_with(
350
+ method_name="stake",
351
+ method_kwargs={"serviceId": 1},
352
+ tx_params={"from": "0xOwner"},
353
+ )
354
+
355
+ def test_prepare_unstake_tx(self, mock_staking_contract):
356
+ mock_staking_contract.prepare_transaction = MagicMock(return_value={"to": "0x"})
357
+
358
+ result = mock_staking_contract.prepare_unstake_tx("0xOwner", 1)
359
+
360
+ assert result == {"to": "0x"}
361
+ mock_staking_contract.prepare_transaction.assert_called_with(
362
+ method_name="unstake",
363
+ method_kwargs={"serviceId": 1},
364
+ tx_params={"from": "0xOwner"},
365
+ )
366
+
367
+ def test_prepare_claim_tx(self, mock_staking_contract):
368
+ mock_staking_contract.prepare_transaction = MagicMock(return_value={"to": "0x"})
369
+
370
+ result = mock_staking_contract.prepare_claim_tx("0xOwner", 1)
371
+
372
+ assert result == {"to": "0x"}
373
+ mock_staking_contract.prepare_transaction.assert_called_with(
374
+ method_name="claim",
375
+ method_kwargs={"serviceId": 1},
376
+ tx_params={"from": "0xOwner"},
377
+ )
378
+
379
+ def test_prepare_checkpoint_tx(self, mock_staking_contract):
380
+ mock_staking_contract.prepare_transaction = MagicMock(return_value={"to": "0x"})
381
+
382
+ result = mock_staking_contract.prepare_checkpoint_tx("0xCaller")
383
+
384
+ assert result == {"to": "0x"}
385
+ mock_staking_contract.prepare_transaction.assert_called_with(
386
+ method_name="checkpoint",
387
+ method_kwargs={},
388
+ tx_params={"from": "0xCaller"},
389
+ )
390
+
391
+
392
+ class TestGetAccruedRewards:
393
+ """Test get_accrued_rewards method."""
394
+
395
+ def test_extracts_reward_from_service_info(self, mock_staking_contract):
396
+ # mapServiceInfo returns (multisig, owner, nonces, tsStart, reward, inactivity)
397
+ mock_staking_contract.call = MagicMock(
398
+ return_value=("0xMultisig", "0xOwner", (10, 5), 1000, 750, 0)
399
+ )
400
+
401
+ result = mock_staking_contract.get_accrued_rewards(1)
402
+
403
+ assert result == 750
404
+
405
+ def test_returns_zero_for_short_response(self, mock_staking_contract):
406
+ mock_staking_contract.call = MagicMock(return_value=(1, 2, 3))
407
+
408
+ result = mock_staking_contract.get_accrued_rewards(1)
409
+
410
+ assert result == 0
411
+
412
+
413
+ class TestIsCheckpointNeeded:
414
+ """Test is_checkpoint_needed method."""
415
+
416
+ def test_returns_false_before_epoch_end(self, mock_staking_contract):
417
+ future_time = datetime.now(timezone.utc).replace(microsecond=0)
418
+ future_time = future_time.replace(hour=future_time.hour + 1)
419
+
420
+ with patch.object(mock_staking_contract, "get_next_epoch_start", return_value=future_time):
421
+ result = mock_staking_contract.is_checkpoint_needed()
422
+ assert result is False
423
+
424
+ def test_returns_false_within_grace_period(self, mock_staking_contract):
425
+ # Epoch ended 5 minutes ago (300 seconds), grace period is 600 seconds
426
+ past_time = datetime.now(timezone.utc).replace(microsecond=0)
427
+ from datetime import timedelta
428
+ past_time = past_time - timedelta(seconds=300)
429
+
430
+ with patch.object(mock_staking_contract, "get_next_epoch_start", return_value=past_time):
431
+ result = mock_staking_contract.is_checkpoint_needed(grace_period_seconds=600)
432
+ assert result is False
433
+
434
+ def test_returns_true_after_grace_period(self, mock_staking_contract):
435
+ # Epoch ended 20 minutes ago (1200 seconds)
436
+ from datetime import timedelta
437
+ past_time = datetime.now(timezone.utc).replace(microsecond=0) - timedelta(seconds=1200)
438
+
439
+ with patch.object(mock_staking_contract, "get_next_epoch_start", return_value=past_time):
440
+ result = mock_staking_contract.is_checkpoint_needed(grace_period_seconds=600)
441
+ assert result is True
442
+
443
+
444
+ class TestGetServiceInfo:
445
+ """Test get_service_info method."""
446
+
447
+ def test_parses_service_info(self, mock_staking_contract):
448
+ ts_now = int(time.time())
449
+ ts_start = ts_now - 1000
450
+ ts_checkpoint_val = ts_now - 500
451
+
452
+ mock_staking_contract.call = MagicMock(
453
+ return_value=("0xMultisig", "0xOwner", (10, 5), ts_start, 750, 0)
454
+ )
455
+
456
+ mock_activity_checker = MagicMock()
457
+ mock_activity_checker.get_multisig_nonces.return_value = (15, 8)
458
+ mock_activity_checker.liveness_ratio = 1e15 # Low ratio for easy testing
459
+ mock_activity_checker.is_ratio_pass.return_value = True
460
+ mock_staking_contract._activity_checker = mock_activity_checker
461
+
462
+ with patch.object(mock_staking_contract, "ts_checkpoint", return_value=ts_checkpoint_val):
463
+ with patch.object(mock_staking_contract, "get_required_requests", return_value=5):
464
+ with patch.object(
465
+ mock_staking_contract,
466
+ "get_next_epoch_start",
467
+ return_value=datetime.fromtimestamp(ts_now + 3600, tz=timezone.utc),
468
+ ):
469
+ result = mock_staking_contract.get_service_info(1)
470
+
471
+ assert result["multisig_address"] == "0xMultisig"
472
+ assert result["owner_address"] == "0xOwner"
473
+ assert result["current_safe_nonce"] == 15
474
+ assert result["current_mech_requests"] == 8
475
+ assert result["last_checkpoint_safe_nonce"] == 10
476
+ assert result["last_checkpoint_mech_requests"] == 5
477
+ assert result["mech_requests_this_epoch"] == 3 # 8 - 5
478
+ assert result["accrued_reward_wei"] == 750
479
+
480
+ def test_handles_nested_tuple_response(self, mock_staking_contract):
481
+ """Test handling of nested tuple response from web3."""
482
+ ts_now = int(time.time())
483
+ ts_start = ts_now - 1000
484
+
485
+ # Response wrapped in extra tuple (as sometimes returned by web3)
486
+ nested_response = [("0xMultisig", "0xOwner", (10, 5), ts_start, 750, 0)]
487
+ mock_staking_contract.call = MagicMock(return_value=nested_response)
488
+
489
+ mock_activity_checker = MagicMock()
490
+ mock_activity_checker.get_multisig_nonces.return_value = (15, 8)
491
+ mock_activity_checker.is_ratio_pass.return_value = True
492
+ mock_staking_contract._activity_checker = mock_activity_checker
493
+
494
+ with patch.object(mock_staking_contract, "ts_checkpoint", return_value=ts_now - 500):
495
+ with patch.object(mock_staking_contract, "get_required_requests", return_value=5):
496
+ with patch.object(
497
+ mock_staking_contract,
498
+ "get_next_epoch_start",
499
+ return_value=datetime.fromtimestamp(ts_now + 3600, tz=timezone.utc),
500
+ ):
501
+ result = mock_staking_contract.get_service_info(1)
502
+
503
+ assert result["multisig_address"] == "0xMultisig"
File without changes