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.
@@ -0,0 +1,253 @@
1
+ """Tests for contract instance caching."""
2
+
3
+ import time
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def reset_singleton():
11
+ """Reset the ContractCache singleton before each test."""
12
+ from iwa.core.contracts.cache import ContractCache
13
+
14
+ ContractCache._instance = None
15
+ yield
16
+ ContractCache._instance = None
17
+
18
+
19
+ class TestContractCache:
20
+ """Test ContractCache singleton and caching behavior."""
21
+
22
+ def test_singleton_pattern(self):
23
+ """Test that ContractCache is a singleton."""
24
+ from iwa.core.contracts.cache import ContractCache
25
+
26
+ c1 = ContractCache()
27
+ c2 = ContractCache()
28
+ assert c1 is c2
29
+
30
+ def test_get_contract_creates_new_instance(self):
31
+ """Test get_contract creates new instance when not cached."""
32
+ from iwa.core.contracts.cache import ContractCache
33
+
34
+ cache = ContractCache()
35
+ mock_cls = MagicMock(__name__="MockContract")
36
+ mock_instance = MagicMock()
37
+ mock_cls.return_value = mock_instance
38
+
39
+ result = cache.get_contract(
40
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
41
+ )
42
+
43
+ mock_cls.assert_called_once_with(
44
+ "0x1234567890123456789012345678901234567890", chain_name="gnosis"
45
+ )
46
+ assert result is mock_instance
47
+
48
+ def test_get_contract_returns_cached_instance(self):
49
+ """Test get_contract returns cached instance on second call."""
50
+ from iwa.core.contracts.cache import ContractCache
51
+
52
+ cache = ContractCache()
53
+ mock_cls = MagicMock(__name__="MockContract")
54
+ mock_instance = MagicMock()
55
+ mock_cls.return_value = mock_instance
56
+
57
+ result1 = cache.get_contract(
58
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
59
+ )
60
+ result2 = cache.get_contract(
61
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
62
+ )
63
+
64
+ # Should only create once
65
+ mock_cls.assert_called_once()
66
+ assert result1 is result2
67
+
68
+ def test_get_contract_raises_on_empty_address(self):
69
+ """Test get_contract raises ValueError for empty address."""
70
+ from iwa.core.contracts.cache import ContractCache
71
+
72
+ cache = ContractCache()
73
+ mock_cls = MagicMock(__name__="MockContract")
74
+
75
+ with pytest.raises(ValueError, match="Address is required"):
76
+ cache.get_contract(mock_cls, "", "gnosis")
77
+
78
+ def test_get_contract_respects_ttl_expiry(self):
79
+ """Test get_contract recreates instance after TTL expires."""
80
+ from iwa.core.contracts.cache import ContractCache
81
+
82
+ cache = ContractCache()
83
+ mock_cls = MagicMock(__name__="MockContract")
84
+ mock_cls.return_value = MagicMock()
85
+
86
+ # First call
87
+ cache.get_contract(
88
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis", ttl=0
89
+ )
90
+
91
+ # Wait for expiry (TTL=0 means immediate expiry)
92
+ time.sleep(0.01)
93
+
94
+ # Second call should create new instance
95
+ cache.get_contract(
96
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis", ttl=0
97
+ )
98
+
99
+ assert mock_cls.call_count == 2
100
+
101
+ def test_get_if_cached_returns_cached_instance(self):
102
+ """Test get_if_cached returns cached instance."""
103
+ from iwa.core.contracts.cache import ContractCache
104
+
105
+ cache = ContractCache()
106
+ mock_cls = MagicMock(__name__="MockContract")
107
+ mock_instance = MagicMock()
108
+ mock_cls.return_value = mock_instance
109
+
110
+ # First populate cache
111
+ cache.get_contract(
112
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
113
+ )
114
+
115
+ # get_if_cached should return it
116
+ result = cache.get_if_cached(
117
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
118
+ )
119
+
120
+ assert result is mock_instance
121
+
122
+ def test_get_if_cached_returns_none_when_not_cached(self):
123
+ """Test get_if_cached returns None when not cached."""
124
+ from iwa.core.contracts.cache import ContractCache
125
+
126
+ cache = ContractCache()
127
+ mock_cls = MagicMock(__name__="MockContract")
128
+
129
+ result = cache.get_if_cached(
130
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
131
+ )
132
+
133
+ assert result is None
134
+
135
+ def test_get_if_cached_returns_none_for_empty_address(self):
136
+ """Test get_if_cached returns None for empty address."""
137
+ from iwa.core.contracts.cache import ContractCache
138
+
139
+ cache = ContractCache()
140
+ mock_cls = MagicMock(__name__="MockContract")
141
+
142
+ result = cache.get_if_cached(mock_cls, "", "gnosis")
143
+
144
+ assert result is None
145
+
146
+ def test_get_if_cached_returns_none_after_expiry(self):
147
+ """Test get_if_cached returns None after TTL expires."""
148
+ from iwa.core.contracts.cache import ContractCache
149
+
150
+ cache = ContractCache()
151
+ cache.ttl = 0 # Immediate expiry
152
+ mock_cls = MagicMock(__name__="MockContract")
153
+ mock_cls.return_value = MagicMock()
154
+
155
+ # Populate cache
156
+ cache.get_contract(
157
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
158
+ )
159
+
160
+ time.sleep(0.01)
161
+
162
+ # get_if_cached should return None due to expiry
163
+ result = cache.get_if_cached(
164
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
165
+ )
166
+
167
+ assert result is None
168
+
169
+ def test_clear_removes_all_entries(self):
170
+ """Test clear removes all cached contracts."""
171
+ from iwa.core.contracts.cache import ContractCache
172
+
173
+ cache = ContractCache()
174
+ mock_cls = MagicMock(__name__="MockContract")
175
+ mock_cls.return_value = MagicMock()
176
+
177
+ # Populate cache
178
+ cache.get_contract(
179
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
180
+ )
181
+
182
+ cache.clear()
183
+
184
+ # get_if_cached should return None
185
+ result = cache.get_if_cached(
186
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
187
+ )
188
+ assert result is None
189
+
190
+ def test_invalidate_removes_specific_entry(self):
191
+ """Test invalidate removes specific cached contract."""
192
+ from iwa.core.contracts.cache import ContractCache
193
+
194
+ cache = ContractCache()
195
+ mock_cls1 = MagicMock(__name__="Contract1")
196
+ mock_cls2 = MagicMock(__name__="Contract2")
197
+ mock_cls1.return_value = MagicMock()
198
+ mock_cls2.return_value = MagicMock()
199
+
200
+ # Populate cache with two contracts
201
+ cache.get_contract(
202
+ mock_cls1, "0x1234567890123456789012345678901234567890", "gnosis"
203
+ )
204
+ cache.get_contract(
205
+ mock_cls2, "0xABCDEF1234567890123456789012345678901234", "gnosis"
206
+ )
207
+
208
+ # Invalidate only the first one
209
+ cache.invalidate(
210
+ mock_cls1, "0x1234567890123456789012345678901234567890", "gnosis"
211
+ )
212
+
213
+ # First should be gone
214
+ result1 = cache.get_if_cached(
215
+ mock_cls1, "0x1234567890123456789012345678901234567890", "gnosis"
216
+ )
217
+ assert result1 is None
218
+
219
+ # Second should still exist
220
+ result2 = cache.get_if_cached(
221
+ mock_cls2, "0xABCDEF1234567890123456789012345678901234", "gnosis"
222
+ )
223
+ assert result2 is not None
224
+
225
+ def test_invalidate_nonexistent_does_nothing(self):
226
+ """Test invalidate does nothing for non-existent entry."""
227
+ from iwa.core.contracts.cache import ContractCache
228
+
229
+ cache = ContractCache()
230
+ mock_cls = MagicMock(__name__="Contract")
231
+
232
+ # Should not raise
233
+ cache.invalidate(
234
+ mock_cls, "0x1234567890123456789012345678901234567890", "gnosis"
235
+ )
236
+
237
+ def test_env_ttl_configuration(self):
238
+ """Test TTL is configurable via environment variable."""
239
+ from iwa.core.contracts.cache import ContractCache
240
+
241
+ with patch.dict("os.environ", {"IWA_CONTRACT_CACHE_TTL": "7200"}):
242
+ ContractCache._instance = None
243
+ cache = ContractCache()
244
+ assert cache.ttl == 7200
245
+
246
+ def test_invalid_env_ttl_uses_default(self):
247
+ """Test invalid TTL env var uses default value."""
248
+ from iwa.core.contracts.cache import ContractCache
249
+
250
+ with patch.dict("os.environ", {"IWA_CONTRACT_CACHE_TTL": "invalid"}):
251
+ ContractCache._instance = None
252
+ cache = ContractCache()
253
+ assert cache.ttl == 3600
@@ -46,7 +46,7 @@ def test_claim_rewards_claim_tx_fails(mock_drain_manager):
46
46
  with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
47
47
  mock_staking = mock_staking_cls.return_value
48
48
  mock_staking.get_staking_state.return_value = StakingState.STAKED
49
- mock_staking.get_accrued_rewards.return_value = 1000000000000000000
49
+ mock_staking.calculate_staking_reward.return_value = 1000000000000000000
50
50
  mock_staking.prepare_claim_tx.return_value = None # Failed to prepare
51
51
 
52
52
  success, amount = mock_drain_manager.claim_rewards()
@@ -63,7 +63,7 @@ def test_claim_rewards_send_fails(mock_drain_manager):
63
63
  with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
64
64
  mock_staking = mock_staking_cls.return_value
65
65
  mock_staking.get_staking_state.return_value = StakingState.STAKED
66
- mock_staking.get_accrued_rewards.return_value = 1000000000000000000
66
+ mock_staking.calculate_staking_reward.return_value = 1000000000000000000
67
67
  mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
68
68
  mock_drain_manager.wallet.sign_and_send_transaction.return_value = (False, None)
69
69
 
@@ -81,7 +81,7 @@ def test_claim_rewards_success_no_event(mock_drain_manager):
81
81
  with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
82
82
  mock_staking = mock_staking_cls.return_value
83
83
  mock_staking.get_staking_state.return_value = StakingState.STAKED
84
- mock_staking.get_accrued_rewards.return_value = 1000000000000000000
84
+ mock_staking.calculate_staking_reward.return_value = 1000000000000000000
85
85
  mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
86
86
  mock_staking.extract_events.return_value = [] # No RewardClaimed event
87
87
  mock_drain_manager.wallet.sign_and_send_transaction.return_value = (
@@ -94,6 +94,33 @@ def test_claim_rewards_success_no_event(mock_drain_manager):
94
94
  assert amount == 1000000000000000000
95
95
 
96
96
 
97
+ def test_claim_rewards_fallback_to_accrued(mock_drain_manager):
98
+ """Test claim_rewards falls back to get_accrued_rewards when calculate_staking_reward fails."""
99
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
100
+ mock_drain_manager.service.service_id = 1
101
+ mock_drain_manager.wallet.master_account.address = "0xMaster"
102
+
103
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
104
+ mock_staking = mock_staking_cls.return_value
105
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
106
+ # calculate_staking_reward fails, should fallback to get_accrued_rewards
107
+ mock_staking.calculate_staking_reward.side_effect = Exception("RPC error")
108
+ mock_staking.get_accrued_rewards.return_value = 2000000000000000000 # 2 OLAS
109
+ mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
110
+ mock_staking.extract_events.return_value = []
111
+ mock_drain_manager.wallet.sign_and_send_transaction.return_value = (
112
+ True,
113
+ {"transactionHash": "0xHash"},
114
+ )
115
+
116
+ success, amount = mock_drain_manager.claim_rewards()
117
+ assert success
118
+ assert amount == 2000000000000000000
119
+ # Verify both methods were called
120
+ mock_staking.calculate_staking_reward.assert_called_once()
121
+ mock_staking.get_accrued_rewards.assert_called_once()
122
+
123
+
97
124
  def test_withdraw_rewards_fallback_to_master(mock_drain_manager):
98
125
  """Test withdraw_rewards falls back to master account when not configured."""
99
126
  mock_drain_manager.service.multisig_address = "0x1111111111111111111111111111111111111111"
@@ -178,3 +205,238 @@ def test_drain_owner_skipped_when_equals_target(mock_drain_manager):
178
205
  # Should skip and return None without calling drain
179
206
  assert result is None
180
207
  mock_drain_manager.wallet.drain.assert_not_called()
208
+
209
+
210
+ def test_claim_rewards_staking_contract_load_fails(mock_drain_manager):
211
+ """Test claim_rewards when StakingContract fails to load."""
212
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
213
+ mock_drain_manager.service.service_id = 1
214
+
215
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
216
+ mock_staking_cls.side_effect = Exception("Failed to load contract")
217
+
218
+ success, amount = mock_drain_manager.claim_rewards()
219
+ assert not success
220
+ assert amount == 0
221
+
222
+
223
+ def test_claim_rewards_not_staked_state(mock_drain_manager):
224
+ """Test claim_rewards when service staking state is not STAKED."""
225
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
226
+ mock_drain_manager.service.service_id = 1
227
+
228
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
229
+ mock_staking = mock_staking_cls.return_value
230
+ mock_staking.get_staking_state.return_value = StakingState.NOT_STAKED
231
+
232
+ success, amount = mock_drain_manager.claim_rewards()
233
+ assert not success
234
+ assert amount == 0
235
+
236
+
237
+ def test_claim_rewards_zero_rewards(mock_drain_manager):
238
+ """Test claim_rewards when claimable rewards is zero."""
239
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
240
+ mock_drain_manager.service.service_id = 1
241
+
242
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
243
+ mock_staking = mock_staking_cls.return_value
244
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
245
+ mock_staking.calculate_staking_reward.return_value = 0
246
+
247
+ success, amount = mock_drain_manager.claim_rewards()
248
+ assert not success
249
+ assert amount == 0
250
+
251
+
252
+ def test_claim_rewards_with_event_amount(mock_drain_manager):
253
+ """Test claim_rewards extracts amount from RewardClaimed event."""
254
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
255
+ mock_drain_manager.service.service_id = 1
256
+ mock_drain_manager.wallet.master_account.address = "0xMaster"
257
+
258
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
259
+ mock_staking = mock_staking_cls.return_value
260
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
261
+ mock_staking.calculate_staking_reward.return_value = 1000000000000000000
262
+ mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
263
+ # RewardClaimed event with 'amount' field
264
+ mock_staking.extract_events.return_value = [
265
+ {"name": "RewardClaimed", "args": {"amount": 1500000000000000000}}
266
+ ]
267
+ mock_drain_manager.wallet.sign_and_send_transaction.return_value = (
268
+ True,
269
+ {"transactionHash": "0xHash"},
270
+ )
271
+
272
+ success, amount = mock_drain_manager.claim_rewards()
273
+ assert success
274
+ # Should use amount from event, not the estimate
275
+ assert amount == 1500000000000000000
276
+
277
+
278
+ def test_claim_rewards_with_event_reward_field(mock_drain_manager):
279
+ """Test claim_rewards extracts from 'reward' field when 'amount' missing."""
280
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
281
+ mock_drain_manager.service.service_id = 1
282
+ mock_drain_manager.wallet.master_account.address = "0xMaster"
283
+
284
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
285
+ mock_staking = mock_staking_cls.return_value
286
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
287
+ mock_staking.calculate_staking_reward.return_value = 1000000000000000000
288
+ mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
289
+ # RewardClaimed event with 'reward' field (no 'amount')
290
+ mock_staking.extract_events.return_value = [
291
+ {"name": "RewardClaimed", "args": {"reward": 2000000000000000000}}
292
+ ]
293
+ mock_drain_manager.wallet.sign_and_send_transaction.return_value = (
294
+ True,
295
+ {"transactionHash": "0xHash"},
296
+ )
297
+
298
+ success, amount = mock_drain_manager.claim_rewards()
299
+ assert success
300
+ assert amount == 2000000000000000000
301
+
302
+
303
+ def test_withdraw_rewards_no_service(mock_drain_manager):
304
+ """Test withdraw_rewards with no active service."""
305
+ mock_drain_manager.service = None
306
+ success, amount = mock_drain_manager.withdraw_rewards()
307
+ assert not success
308
+ assert amount == 0
309
+
310
+
311
+ def test_withdraw_rewards_no_multisig(mock_drain_manager):
312
+ """Test withdraw_rewards when service has no multisig."""
313
+ mock_drain_manager.service.multisig_address = None
314
+ success, amount = mock_drain_manager.withdraw_rewards()
315
+ assert not success
316
+ assert amount == 0
317
+
318
+
319
+ def test_withdraw_rewards_success(mock_drain_manager):
320
+ """Test withdraw_rewards succeeds with balance."""
321
+ mock_drain_manager.service.multisig_address = "0xSafe"
322
+ mock_drain_manager.olas_config.withdrawal_address = "0xWithdraw"
323
+ mock_drain_manager.wallet.account_service.get_tag_by_address.return_value = None
324
+ mock_drain_manager.wallet.send.return_value = "0xTxHash"
325
+
326
+ with patch("iwa.plugins.olas.service_manager.drain.ERC20Contract") as mock_erc20_cls:
327
+ mock_erc20 = mock_erc20_cls.return_value
328
+ mock_erc20.balance_of_wei.return_value = 5000000000000000000 # 5 OLAS
329
+
330
+ success, amount = mock_drain_manager.withdraw_rewards()
331
+ assert success
332
+ assert amount == 5.0 # 5 OLAS
333
+
334
+
335
+ def test_withdraw_rewards_transfer_fails(mock_drain_manager):
336
+ """Test withdraw_rewards when transfer fails."""
337
+ mock_drain_manager.service.multisig_address = "0xSafe"
338
+ mock_drain_manager.olas_config.withdrawal_address = "0xWithdraw"
339
+ mock_drain_manager.wallet.account_service.get_tag_by_address.return_value = None
340
+ mock_drain_manager.wallet.send.return_value = None # Transfer failed
341
+
342
+ with patch("iwa.plugins.olas.service_manager.drain.ERC20Contract") as mock_erc20_cls:
343
+ mock_erc20 = mock_erc20_cls.return_value
344
+ mock_erc20.balance_of_wei.return_value = 5000000000000000000
345
+
346
+ success, amount = mock_drain_manager.withdraw_rewards()
347
+ assert not success
348
+ assert amount == 0
349
+
350
+
351
+ def test_drain_service_full_flow(mock_drain_manager):
352
+ """Test drain_service drains all accounts."""
353
+ mock_drain_manager.service.key = "test_service"
354
+ mock_drain_manager.service.staking_contract_address = None # No staking
355
+ mock_drain_manager.service.multisig_address = "0xSafe"
356
+ mock_drain_manager.service.agent_address = "0xAgent"
357
+ mock_drain_manager.service.service_owner_address = "0xOwner"
358
+ mock_drain_manager.wallet.master_account.address = "0xMaster"
359
+
360
+ # Mock drain returns
361
+ mock_drain_manager.wallet.drain.return_value = (True, {"transactionHash": b"0x123"})
362
+
363
+ result = mock_drain_manager.drain_service(target_address="0xTarget")
364
+
365
+ assert "safe" in result or "agent" in result or "owner" in result
366
+
367
+
368
+ def test_drain_safe_account_with_retry(mock_drain_manager):
369
+ """Test _drain_safe_account retries when rewards were claimed."""
370
+ mock_drain_manager.service.multisig_address = "0xSafe"
371
+ # First call returns None (balance not updated yet), second returns result
372
+ mock_drain_manager.wallet.drain.side_effect = [
373
+ None,
374
+ (True, {"transactionHash": b"0xhash"}),
375
+ ]
376
+
377
+ with patch("time.sleep"): # Don't actually sleep
378
+ mock_drain_manager._drain_safe_account(
379
+ "0xTarget", "gnosis", claimed_rewards=1000
380
+ )
381
+
382
+ # Should have retried
383
+ assert mock_drain_manager.wallet.drain.call_count >= 1
384
+
385
+
386
+ def test_drain_agent_account_success(mock_drain_manager):
387
+ """Test _drain_agent_account success."""
388
+ mock_drain_manager.service.agent_address = "0xAgent"
389
+ mock_drain_manager.wallet.drain.return_value = (True, {"transactionHash": b"0xhash"})
390
+
391
+ result = mock_drain_manager._drain_agent_account("0xTarget", "gnosis")
392
+
393
+ assert result is not None
394
+
395
+
396
+ def test_drain_owner_account_success(mock_drain_manager):
397
+ """Test _drain_owner_account success."""
398
+ mock_drain_manager.service.service_owner_address = "0xOwner"
399
+ mock_drain_manager.wallet.drain.return_value = (True, {"transactionHash": b"0xhash"})
400
+
401
+ result = mock_drain_manager._drain_owner_account("0xTarget", "gnosis")
402
+
403
+ assert result is not None
404
+
405
+
406
+ def test_normalize_drain_result_passthrough(mock_drain_manager):
407
+ """Test _normalize_drain_result passes through non-tuple results."""
408
+ result = mock_drain_manager._normalize_drain_result({"some": "dict"})
409
+ assert result == {"some": "dict"}
410
+
411
+
412
+ def test_normalize_drain_result_string_hash(mock_drain_manager):
413
+ """Test _normalize_drain_result handles string transaction hash."""
414
+ result = mock_drain_manager._normalize_drain_result(
415
+ (True, {"transactionHash": "0xABCDEF"})
416
+ )
417
+ assert result == "0xABCDEF"
418
+
419
+
420
+ def test_claim_rewards_if_needed_success(mock_drain_manager):
421
+ """Test _claim_rewards_if_needed returns claimed amount on success."""
422
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
423
+ mock_drain_manager.claim_rewards = MagicMock(return_value=(True, 1000000000000000000))
424
+
425
+ result = mock_drain_manager._claim_rewards_if_needed(claim_rewards=True)
426
+ assert result == 1000000000000000000
427
+
428
+
429
+ def test_claim_rewards_if_needed_disabled(mock_drain_manager):
430
+ """Test _claim_rewards_if_needed returns 0 when disabled."""
431
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
432
+
433
+ result = mock_drain_manager._claim_rewards_if_needed(claim_rewards=False)
434
+ assert result == 0
435
+
436
+
437
+ def test_claim_rewards_if_needed_no_staking(mock_drain_manager):
438
+ """Test _claim_rewards_if_needed returns 0 when not staked."""
439
+ mock_drain_manager.service.staking_contract_address = None
440
+
441
+ result = mock_drain_manager._claim_rewards_if_needed(claim_rewards=True)
442
+ assert result == 0