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.
- iwa/plugins/olas/models.py +5 -22
- iwa/plugins/olas/service_manager/drain.py +34 -12
- iwa/plugins/olas/service_manager/lifecycle.py +3 -3
- iwa/plugins/olas/tests/test_olas_models.py +5 -5
- iwa/plugins/olas/tests/test_olas_view_actions.py +1 -1
- iwa/plugins/olas/tests/test_service_manager_rewards.py +6 -1
- iwa/plugins/olas/tests/test_service_staking.py +1 -0
- iwa/web/tests/test_web_olas.py +1 -1
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/METADATA +1 -1
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/RECORD +18 -17
- tests/test_chainlist_enrichment.py +233 -0
- tests/test_contract_cache.py +253 -0
- tests/test_drain_coverage.py +265 -3
- tests/test_staking_simple.py +478 -6
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/WHEEL +0 -0
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/top_level.txt +0 -0
tests/test_staking_simple.py
CHANGED
|
@@ -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
|
-
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
|
|
4
10
|
|
|
5
11
|
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|