iwa 0.0.58__py3-none-any.whl → 0.0.60__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.
Files changed (60) hide show
  1. iwa/core/chain/interface.py +118 -53
  2. iwa/core/chain/rate_limiter.py +35 -12
  3. iwa/core/chainlist.py +15 -10
  4. iwa/core/cli.py +3 -0
  5. iwa/core/contracts/cache.py +1 -1
  6. iwa/core/contracts/contract.py +1 -0
  7. iwa/core/contracts/decoder.py +10 -4
  8. iwa/core/http.py +31 -0
  9. iwa/core/ipfs.py +11 -19
  10. iwa/core/keys.py +10 -4
  11. iwa/core/models.py +1 -3
  12. iwa/core/pricing.py +3 -21
  13. iwa/core/rpc_monitor.py +1 -0
  14. iwa/core/services/balance.py +0 -1
  15. iwa/core/services/safe.py +8 -2
  16. iwa/core/services/safe_executor.py +52 -18
  17. iwa/core/services/transaction.py +32 -12
  18. iwa/core/services/transfer/erc20.py +0 -1
  19. iwa/core/services/transfer/native.py +1 -1
  20. iwa/core/tests/test_gnosis_fee.py +6 -2
  21. iwa/core/tests/test_ipfs.py +1 -1
  22. iwa/core/tests/test_regression_fixes.py +3 -6
  23. iwa/core/utils.py +2 -0
  24. iwa/core/wallet.py +3 -1
  25. iwa/plugins/olas/constants.py +15 -5
  26. iwa/plugins/olas/contracts/activity_checker.py +3 -3
  27. iwa/plugins/olas/contracts/staking.py +0 -1
  28. iwa/plugins/olas/events.py +15 -13
  29. iwa/plugins/olas/importer.py +26 -20
  30. iwa/plugins/olas/plugin.py +16 -14
  31. iwa/plugins/olas/service_manager/drain.py +1 -3
  32. iwa/plugins/olas/service_manager/lifecycle.py +9 -9
  33. iwa/plugins/olas/service_manager/staking.py +11 -6
  34. iwa/plugins/olas/tests/test_olas_archiving.py +25 -15
  35. iwa/plugins/olas/tests/test_olas_integration.py +49 -29
  36. iwa/plugins/olas/tests/test_service_manager.py +8 -10
  37. iwa/plugins/olas/tests/test_service_manager_errors.py +5 -4
  38. iwa/plugins/olas/tests/test_service_manager_flows.py +6 -5
  39. iwa/plugins/olas/tests/test_service_staking.py +64 -38
  40. iwa/tools/drain_accounts.py +2 -1
  41. iwa/tools/reset_env.py +2 -1
  42. iwa/tools/test_chainlist.py +5 -1
  43. iwa/tui/screens/wallets.py +1 -3
  44. iwa/web/routers/olas/services.py +10 -5
  45. {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/METADATA +1 -1
  46. {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/RECORD +60 -59
  47. tests/test_balance_service.py +0 -2
  48. tests/test_chain.py +1 -2
  49. tests/test_chain_interface.py +3 -3
  50. tests/test_rate_limiter.py +7 -5
  51. tests/test_rate_limiter_retry.py +34 -33
  52. tests/test_rpc_efficiency.py +4 -1
  53. tests/test_rpc_rate_limit.py +4 -3
  54. tests/test_rpc_rotation.py +4 -4
  55. tests/test_safe_executor.py +76 -50
  56. tests/test_safe_integration.py +11 -6
  57. {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/WHEEL +0 -0
  58. {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/entry_points.txt +0 -0
  59. {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/licenses/LICENSE +0 -0
  60. {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/top_level.txt +0 -0
@@ -146,7 +146,7 @@ class TestRateLimitRotationInterplay:
146
146
  """Test interaction between rate limiting and RPC rotation."""
147
147
 
148
148
  def test_rate_limit_triggers_rotation_first(self):
149
- """Test that rate limit error triggers RPC rotation before backoff."""
149
+ """Test that rate limit error triggers RPC rotation and global backoff."""
150
150
  from unittest.mock import MagicMock, PropertyMock
151
151
 
152
152
  from iwa.core.chain import ChainInterface, SupportedChain
@@ -171,8 +171,10 @@ class TestRateLimitRotationInterplay:
171
171
  assert result["rotated"] is True
172
172
  assert result["should_retry"] is True
173
173
  assert ci._current_rpc_index != original_index
174
- # Backoff should NOT be triggered since rotation succeeded
175
- assert ci._rate_limiter.get_status()["in_backoff"] is False
174
+ # Global backoff IS triggered to slow other threads briefly
175
+ assert ci._rate_limiter.get_status()["in_backoff"] is True
176
+ # The old RPC should be marked in per-RPC backoff
177
+ assert not ci._is_rpc_healthy(original_index)
176
178
 
177
179
  def test_rate_limit_triggers_backoff_when_no_rotation(self):
178
180
  """Test that rate limit triggers backoff when no other RPCs available."""
@@ -193,7 +195,7 @@ class TestRateLimitRotationInterplay:
193
195
  rate_limit_error = Exception("Error 429: Too Many Requests")
194
196
  result = ci._handle_rpc_error(rate_limit_error)
195
197
 
196
- # Should have triggered retry but NO backoff (skipped for single RPC)
198
+ # Should have triggered retry and global backoff
197
199
  assert result["should_retry"] is True
198
200
  assert result["rotated"] is False
199
- assert ci._rate_limiter.get_status()["in_backoff"] is False
201
+ assert ci._rate_limiter.get_status()["in_backoff"] is True
@@ -1,4 +1,3 @@
1
-
2
1
  from unittest.mock import MagicMock, PropertyMock, patch
3
2
 
4
3
  import pytest
@@ -12,7 +11,6 @@ class MockChainInterface:
12
11
 
13
12
 
14
13
  class TestRateLimitedEthRetry:
15
-
16
14
  @pytest.fixture
17
15
  def mock_deps(self):
18
16
  web3_eth = MagicMock()
@@ -21,28 +19,39 @@ class TestRateLimitedEthRetry:
21
19
  chain_interface = MockChainInterface()
22
20
  return web3_eth, rate_limiter, chain_interface
23
21
 
24
- def test_read_method_retries_on_failure(self, mock_deps):
25
- """Verify that read methods automatically retry on failure."""
22
+ def test_read_method_retries_on_transient_failure(self, mock_deps):
23
+ """Verify that read methods retry on transient (connection) errors."""
26
24
  web3_eth, rate_limiter, chain_interface = mock_deps
27
25
  eth_wrapper = RateLimitedEth(web3_eth, rate_limiter, chain_interface)
28
26
 
29
- # Mock get_balance to fail twice then succeed
27
+ # Mock get_balance to fail once with transient error then succeed
30
28
  web3_eth.get_balance.side_effect = [
31
- ValueError("RPC error 1"),
32
- ValueError("RPC error 2"),
33
- 100 # Success
29
+ ValueError("connection timeout"),
30
+ 100, # Success
34
31
  ]
35
32
 
36
- # Use patch to speed up sleep
37
33
  with patch("time.sleep") as mock_sleep:
38
34
  result = eth_wrapper.get_balance("0x123")
39
35
 
40
36
  assert result == 100
41
- assert web3_eth.get_balance.call_count == 3
42
- # Should have slept twice
43
- assert mock_sleep.call_count == 2
44
- # Verify handle_error was called
45
- assert chain_interface._handle_rpc_error.call_count == 2
37
+ # 1 initial + 1 retry = 2 calls (DEFAULT_READ_RETRIES=1)
38
+ assert web3_eth.get_balance.call_count == 2
39
+ assert mock_sleep.call_count == 1
40
+ # RateLimitedEth no longer calls _handle_rpc_error (that's for with_retry)
41
+ assert chain_interface._handle_rpc_error.call_count == 0
42
+
43
+ def test_read_method_raises_non_transient_immediately(self, mock_deps):
44
+ """Verify non-transient errors (rate limit, quota) propagate immediately."""
45
+ web3_eth, rate_limiter, chain_interface = mock_deps
46
+ eth_wrapper = RateLimitedEth(web3_eth, rate_limiter, chain_interface)
47
+
48
+ web3_eth.get_balance.side_effect = ValueError("429 Too Many Requests")
49
+
50
+ with pytest.raises(ValueError, match="429"):
51
+ eth_wrapper.get_balance("0x123")
52
+
53
+ # Only 1 attempt, no retry for non-transient errors
54
+ assert web3_eth.get_balance.call_count == 1
46
55
 
47
56
  def test_write_method_no_auto_retry(self, mock_deps):
48
57
  """Verify that write methods (send_raw_transaction) DO NOT auto-retry."""
@@ -62,23 +71,21 @@ class TestRateLimitedEthRetry:
62
71
  # Should verify it was called only once
63
72
  assert web3_eth.send_raw_transaction.call_count == 1
64
73
  # Chain interface error handler should NOT be called by the wrapper itself
65
- # (It might typically be called by the caller)
66
74
  assert chain_interface._handle_rpc_error.call_count == 0
67
75
 
68
76
  def test_retry_respects_max_attempts(self, mock_deps):
69
- """Verify that retry logic respects maximum attempts."""
77
+ """Verify that retry logic respects maximum attempts for transient errors."""
70
78
  web3_eth, rate_limiter, chain_interface = mock_deps
71
79
  eth_wrapper = RateLimitedEth(web3_eth, rate_limiter, chain_interface)
72
80
 
73
- # Override default retries for quicker test
74
- # Use object.__setattr__ because RateLimitedEth overrides __setattr__
81
+ # Override default retries
75
82
  object.__setattr__(eth_wrapper, "DEFAULT_READ_RETRIES", 2)
76
83
 
77
- # Mock always failing
78
- web3_eth.get_code.side_effect = ValueError("Persistently failing")
84
+ # Mock always failing with transient error
85
+ web3_eth.get_code.side_effect = ValueError("connection reset by peer")
79
86
 
80
87
  with patch("time.sleep"):
81
- with pytest.raises(ValueError, match="Persistently failing"):
88
+ with pytest.raises(ValueError, match="connection reset"):
82
89
  eth_wrapper.get_code("0x123")
83
90
 
84
91
  # Attempts: initial + 2 retries = 3 total calls
@@ -89,20 +96,14 @@ class TestRateLimitedEthRetry:
89
96
  web3_eth, rate_limiter, chain_interface = mock_deps
90
97
  eth_wrapper = RateLimitedEth(web3_eth, rate_limiter, chain_interface)
91
98
 
92
- # Mock property access: fail then succeed
93
- # Note: PropertyMock is needed if we were mocking a property on the CLASS,
94
- # but here we are mocking the instance attribute access which might be a method call or property.
95
- # web3.eth.block_number is a property.
96
-
97
- # We need to set side_effect on the PROPERTY of the mock
98
- type(web3_eth).block_number = PropertyMock(side_effect=[
99
- ValueError("Fail 1"),
100
- 12345
101
- ])
99
+ # block_number fails once with transient error then succeeds
100
+ type(web3_eth).block_number = PropertyMock(
101
+ side_effect=[ValueError("connection timeout"), 12345]
102
+ )
102
103
 
103
104
  with patch("time.sleep"):
104
105
  val = eth_wrapper.block_number
105
106
 
106
107
  assert val == 12345
107
- # Verify handle_error called
108
- assert chain_interface._handle_rpc_error.call_count == 1
108
+ # _handle_rpc_error is NOT called by RateLimitedEth anymore
109
+ assert chain_interface._handle_rpc_error.call_count == 0
@@ -25,6 +25,7 @@ def mock_chain_interface():
25
25
  # Yield both interface and contract mock
26
26
  yield mock_interface, mock_contract
27
27
 
28
+
28
29
  def test_staking_contract_lazy_loading(mock_chain_interface):
29
30
  """Verify StakingContract init does NOT make RPC calls."""
30
31
  mock_interface, mock_contract = mock_chain_interface
@@ -55,13 +56,14 @@ def test_staking_contract_lazy_loading(mock_chain_interface):
55
56
  # Assert still 1 call (cached)
56
57
  assert mock_contract.functions.livenessPeriod.return_value.call.call_count == 1
57
58
 
59
+
58
60
  def test_contract_cache_singleton(mock_chain_interface):
59
61
  """Verify ContractCache returns same instance and reuses property cache."""
60
62
  mock_interface, mock_contract = mock_chain_interface
61
63
  ContractCache().clear()
62
64
 
63
65
  c1 = ContractCache().get_contract(StakingContract, "0xABC", "gnosis")
64
- c2 = ContractCache().get_contract(StakingContract, "0xabc", "gnosis") # Check ignore case
66
+ c2 = ContractCache().get_contract(StakingContract, "0xabc", "gnosis") # Check ignore case
65
67
 
66
68
  assert c1 is c2
67
69
 
@@ -78,6 +80,7 @@ def test_contract_cache_singleton(mock_chain_interface):
78
80
  # Call count should NOT increase
79
81
  assert mock_contract.functions.maxNumServices.return_value.call.call_count == 1
80
82
 
83
+
81
84
  def test_epoch_aware_caching(mock_chain_interface):
82
85
  """Verify ts_checkpoint caching logic."""
83
86
  mock_interface, mock_contract = mock_chain_interface
@@ -14,8 +14,9 @@ def clean_rate_limiters():
14
14
  yield
15
15
  _rate_limiters.clear()
16
16
 
17
+
17
18
  def test_chain_interface_initializes_strict_limiter(clean_rate_limiters):
18
- """Verify ChainInterface initializes with rate=1.0 and burst=1."""
19
+ """Verify ChainInterface initializes with rate=5.0 and burst=10."""
19
20
  # Create a dummy chain
20
21
  chain = MagicMock(spec=SupportedChain)
21
22
  chain.name = "TestSlowChain"
@@ -29,5 +30,5 @@ def test_chain_interface_initializes_strict_limiter(clean_rate_limiters):
29
30
  limiter = ci._rate_limiter
30
31
 
31
32
  # Assert correct configuration
32
- assert limiter.rate == 1.0, f"Expected rate 1.0, got {limiter.rate}"
33
- assert limiter.burst == 1, f"Expected burst 1, got {limiter.burst}"
33
+ assert limiter.rate == 5.0, f"Expected rate 5.0, got {limiter.rate}"
34
+ assert limiter.burst == 10, f"Expected burst 10, got {limiter.burst}"
@@ -91,7 +91,9 @@ def test_rpc_rotation_updates_provider():
91
91
  class MockRateLimitedWeb3:
92
92
  def __init__(self, w3, rl, ci):
93
93
  self._web3 = w3
94
- self.set_backend = MagicMock(side_effect=lambda new_w3: set_backend_calls.append(new_w3))
94
+ self.set_backend = MagicMock(
95
+ side_effect=lambda new_w3: set_backend_calls.append(new_w3)
96
+ )
95
97
 
96
98
  def __getattr__(self, name):
97
99
  return getattr(self._web3, name)
@@ -230,9 +232,7 @@ def test_rotation_applies_to_subsequent_calls():
230
232
  mock_web3_class.side_effect = create_web3
231
233
  mock_web3_class.HTTPProvider = create_provider
232
234
 
233
- with patch(
234
- "iwa.core.chain.interface.RateLimitedWeb3", side_effect=lambda w3, rl, ci: w3
235
- ):
235
+ with patch("iwa.core.chain.interface.RateLimitedWeb3", side_effect=lambda w3, rl, ci: w3):
236
236
  ci = ChainInterface(chain)
237
237
 
238
238
  # First call uses RPC 1
@@ -64,10 +64,13 @@ def mock_safe():
64
64
  # Test: Basic execution success
65
65
  # =============================================================================
66
66
 
67
+
67
68
  def test_execute_success_first_try(executor, mock_chain_interface, mock_safe_tx, mock_safe):
68
69
  """Test successful execution on first attempt."""
69
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
70
- mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
70
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
71
+ mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
72
+ status=1
73
+ )
71
74
  mock_safe_tx.execute.return_value = b"tx_hash"
72
75
 
73
76
  success, tx_hash, receipt = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
@@ -81,10 +84,13 @@ def test_execute_success_first_try(executor, mock_chain_interface, mock_safe_tx,
81
84
  # Test: Tuple vs bytes return handling
82
85
  # =============================================================================
83
86
 
87
+
84
88
  def test_execute_handles_tuple_return(executor, mock_chain_interface, mock_safe_tx, mock_safe):
85
89
  """Test that executor handles safe_tx.execute() returning tuple (tx_hash, tx)."""
86
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
87
- mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
90
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
91
+ mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
92
+ status=1
93
+ )
88
94
  # Simulate tuple return: (tx_hash_bytes, tx_data)
89
95
  mock_safe_tx.execute.return_value = (b"tx_hash", {"gas": 21000})
90
96
 
@@ -96,8 +102,10 @@ def test_execute_handles_tuple_return(executor, mock_chain_interface, mock_safe_
96
102
 
97
103
  def test_execute_handles_bytes_return(executor, mock_chain_interface, mock_safe_tx, mock_safe):
98
104
  """Test that executor handles safe_tx.execute() returning raw bytes."""
99
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
100
- mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
105
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
106
+ mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
107
+ status=1
108
+ )
101
109
  mock_safe_tx.execute.return_value = b"raw_hash"
102
110
 
103
111
  success, tx_hash, receipt = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
@@ -108,8 +116,10 @@ def test_execute_handles_bytes_return(executor, mock_chain_interface, mock_safe_
108
116
 
109
117
  def test_execute_handles_string_return(executor, mock_chain_interface, mock_safe_tx, mock_safe):
110
118
  """Test that executor handles safe_tx.execute() returning hex string."""
111
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
112
- mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
119
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
120
+ mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
121
+ status=1
122
+ )
113
123
  mock_safe_tx.execute.return_value = "0xabcdef1234567890"
114
124
 
115
125
  success, tx_hash, receipt = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
@@ -122,11 +132,12 @@ def test_execute_handles_string_return(executor, mock_chain_interface, mock_safe
122
132
  # Test: Signature validation
123
133
  # =============================================================================
124
134
 
135
+
125
136
  def test_execute_fails_on_empty_signatures(executor, mock_chain_interface, mock_safe_tx, mock_safe):
126
137
  """Verify we fail immediately if no signatures exist."""
127
138
  mock_safe_tx.signatures = b"" # Empty signatures
128
139
 
129
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
140
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
130
141
  with patch("time.sleep"):
131
142
  success, error, _ = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
132
143
 
@@ -135,11 +146,13 @@ def test_execute_fails_on_empty_signatures(executor, mock_chain_interface, mock_
135
146
  assert mock_safe_tx.execute.call_count == 0 # Never tried to execute
136
147
 
137
148
 
138
- def test_execute_fails_on_truncated_signatures(executor, mock_chain_interface, mock_safe_tx, mock_safe):
149
+ def test_execute_fails_on_truncated_signatures(
150
+ executor, mock_chain_interface, mock_safe_tx, mock_safe
151
+ ):
139
152
  """Verify we detect signatures shorter than 65 bytes."""
140
153
  mock_safe_tx.signatures = b"x" * 30 # Too short (need 65)
141
154
 
142
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
155
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
143
156
  with patch("time.sleep"):
144
157
  success, error, _ = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
145
158
 
@@ -151,7 +164,7 @@ def test_execute_fails_on_none_signatures(executor, mock_chain_interface, mock_s
151
164
  """Verify we handle None signatures gracefully."""
152
165
  mock_safe_tx.signatures = None
153
166
 
154
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
167
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
155
168
  with patch("time.sleep"):
156
169
  success, error, _ = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
157
170
 
@@ -163,9 +176,10 @@ def test_execute_fails_on_none_signatures(executor, mock_chain_interface, mock_s
163
176
  # Test: Error classification (GS0xx codes)
164
177
  # =============================================================================
165
178
 
179
+
166
180
  def test_gs020_fails_fast(executor, mock_chain_interface, mock_safe_tx, mock_safe):
167
181
  """GS020 (signatures too short) should not trigger retries."""
168
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
182
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
169
183
  mock_safe_tx.call.side_effect = ValueError("execution reverted: GS020")
170
184
 
171
185
  with patch("time.sleep"):
@@ -178,7 +192,7 @@ def test_gs020_fails_fast(executor, mock_chain_interface, mock_safe_tx, mock_saf
178
192
 
179
193
  def test_gs026_fails_fast(executor, mock_chain_interface, mock_safe_tx, mock_safe):
180
194
  """GS026 (invalid owner) should not trigger retries."""
181
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
195
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
182
196
  mock_safe_tx.call.side_effect = ValueError("execution reverted: GS026")
183
197
 
184
198
  with patch("time.sleep"):
@@ -188,15 +202,18 @@ def test_gs026_fails_fast(executor, mock_chain_interface, mock_safe_tx, mock_saf
188
202
  assert "GS026" in error
189
203
 
190
204
 
191
- @pytest.mark.parametrize("error_code,is_signature_error", [
192
- ("GS020", True), # Signatures data too short
193
- ("GS021", True), # Invalid signature data pointer
194
- ("GS024", True), # Invalid contract signature
195
- ("GS026", True), # Invalid owner
196
- ("GS025", False), # Invalid nonce (not a signature error)
197
- ("GS010", False), # Not enough gas
198
- ("GS013", False), # Safe transaction failed
199
- ])
205
+ @pytest.mark.parametrize(
206
+ "error_code,is_signature_error",
207
+ [
208
+ ("GS020", True), # Signatures data too short
209
+ ("GS021", True), # Invalid signature data pointer
210
+ ("GS024", True), # Invalid contract signature
211
+ ("GS026", True), # Invalid owner
212
+ ("GS025", False), # Invalid nonce (not a signature error)
213
+ ("GS010", False), # Not enough gas
214
+ ("GS013", False), # Safe transaction failed
215
+ ],
216
+ )
200
217
  def test_error_classification(executor, error_code, is_signature_error):
201
218
  """Verify correct classification of Safe error codes."""
202
219
  error = ValueError(f"execution reverted: {error_code}")
@@ -208,18 +225,20 @@ def test_error_classification(executor, error_code, is_signature_error):
208
225
  # Test: Retry behavior
209
226
  # =============================================================================
210
227
 
228
+
211
229
  def test_retry_on_transient_error(executor, mock_chain_interface, mock_safe_tx, mock_safe):
212
230
  """Test that transient errors trigger retries without modifying tx."""
213
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
214
- mock_safe_tx.execute.side_effect = [
215
- ConnectionError("Network timeout"),
216
- b"success_hash"
217
- ]
218
- mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
231
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
232
+ mock_safe_tx.execute.side_effect = [ConnectionError("Network timeout"), b"success_hash"]
233
+ mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
234
+ status=1
235
+ )
219
236
  mock_chain_interface._is_connection_error.return_value = True
220
237
 
221
238
  with patch("time.sleep"):
222
- success, tx_hash, receipt = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
239
+ success, tx_hash, receipt = executor.execute_with_retry(
240
+ "0xSafe", mock_safe_tx, ["key1"]
241
+ )
223
242
 
224
243
  assert success is True
225
244
  assert mock_safe_tx.execute.call_count == 2
@@ -229,12 +248,11 @@ def test_retry_on_transient_error(executor, mock_chain_interface, mock_safe_tx,
229
248
 
230
249
  def test_retry_on_nonce_error(executor, mock_chain_interface, mock_safe_tx, mock_safe):
231
250
  """Test nonce refresh on GS025 error."""
232
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
233
- mock_safe_tx.execute.side_effect = [
234
- ValueError("GS025: invalid nonce"),
235
- b"success_hash"
236
- ]
237
- mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
251
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
252
+ mock_safe_tx.execute.side_effect = [ValueError("GS025: invalid nonce"), b"success_hash"]
253
+ mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
254
+ status=1
255
+ )
238
256
 
239
257
  new_tx = MagicMock(spec=SafeTx)
240
258
  new_tx.signatures = b"x" * 65
@@ -249,13 +267,12 @@ def test_retry_on_nonce_error(executor, mock_chain_interface, mock_safe_tx, mock
249
267
 
250
268
  def test_retry_on_rpc_error(executor, mock_chain_interface, mock_safe_tx, mock_safe):
251
269
  """Test RPC rotation on rate limit error."""
252
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
253
- mock_safe_tx.execute.side_effect = [
254
- ValueError("Rate limit exceeded"),
255
- b"success_hash"
256
- ]
270
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
271
+ mock_safe_tx.execute.side_effect = [ValueError("Rate limit exceeded"), b"success_hash"]
257
272
  mock_chain_interface._is_rate_limit_error.return_value = True
258
- mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
273
+ mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
274
+ status=1
275
+ )
259
276
 
260
277
  with patch("time.sleep"):
261
278
  executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
@@ -267,11 +284,13 @@ def test_retry_on_rpc_error(executor, mock_chain_interface, mock_safe_tx, mock_s
267
284
  def test_fail_after_max_retries(executor, mock_chain_interface, mock_safe_tx, mock_safe):
268
285
  """Test failure after exhausting all retries."""
269
286
  executor.max_retries = 2
270
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
287
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
271
288
  mock_safe_tx.execute.side_effect = ValueError("Persistent error")
272
289
 
273
290
  with patch("time.sleep"):
274
- success, tx_hash_or_err, receipt = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
291
+ success, tx_hash_or_err, receipt = executor.execute_with_retry(
292
+ "0xSafe", mock_safe_tx, ["key1"]
293
+ )
275
294
 
276
295
  assert success is False
277
296
  assert mock_safe_tx.execute.call_count == 3 # 1 initial + 2 retries
@@ -281,7 +300,10 @@ def test_fail_after_max_retries(executor, mock_chain_interface, mock_safe_tx, mo
281
300
  # Test: State preservation during retries
282
301
  # =============================================================================
283
302
 
284
- def test_retry_preserves_signatures_despite_clearing(executor, mock_chain_interface, mock_safe_tx, mock_safe):
303
+
304
+ def test_retry_preserves_signatures_despite_clearing(
305
+ executor, mock_chain_interface, mock_safe_tx, mock_safe
306
+ ):
285
307
  """Verify that retries don't corrupt/lose signatures even if library clears them."""
286
308
  original_signatures = mock_safe_tx.signatures
287
309
 
@@ -291,21 +313,23 @@ def test_retry_preserves_signatures_despite_clearing(executor, mock_chain_interf
291
313
  mock_safe_tx.signatures = b""
292
314
  return b"hash"
293
315
 
294
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
316
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
295
317
  # Scenario:
296
318
  # 1. Execute success (sigs cleared) but Receipt not found (triggering retry)
297
319
  # 2. Retry: Execute called again (must have restored sigs) -> Success -> Receipt found
298
320
 
299
321
  mock_chain_interface.web3.eth.wait_for_transaction_receipt.side_effect = [
300
322
  ValueError("Transaction not found"),
301
- MagicMock(status=1)
323
+ MagicMock(status=1),
302
324
  ]
303
325
 
304
326
  mock_safe_tx.execute.side_effect = execute_side_effect
305
327
  mock_chain_interface._is_connection_error.return_value = False
306
328
 
307
329
  with patch("time.sleep"):
308
- success, tx_hash, receipt = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
330
+ success, tx_hash, receipt = executor.execute_with_retry(
331
+ "0xSafe", mock_safe_tx, ["key1"]
332
+ )
309
333
 
310
334
  assert success is True
311
335
  # Signatures should be restored after the loop (or at least valid during 2nd call)
@@ -324,9 +348,11 @@ def test_retry_preserves_gas(executor, mock_chain_interface, mock_safe_tx, mock_
324
348
  """Verify that retries don't modify safe_tx_gas (which would invalidate signatures)."""
325
349
  original_gas = mock_safe_tx.safe_tx_gas
326
350
 
327
- with patch.object(executor, '_recreate_safe_client', return_value=mock_safe):
351
+ with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
328
352
  mock_safe_tx.execute.side_effect = [ConnectionError("timeout"), b"hash"]
329
- mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
353
+ mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
354
+ status=1
355
+ )
330
356
  mock_chain_interface._is_connection_error.return_value = True
331
357
 
332
358
  with patch("time.sleep"):
@@ -47,7 +47,12 @@ def real_safe_tx_mock_eth(mock_chain_interface):
47
47
  mock_contract = MagicMock()
48
48
  # Mock execTransaction function build_transaction
49
49
  mock_contract.functions.execTransaction.return_value.build_transaction.return_value = {
50
- "to": "0xSafe", "data": b"", "value": 0, "gas": 500000, "nonce": 5, "from": "0xExecutor"
50
+ "to": "0xSafe",
51
+ "data": b"",
52
+ "value": 0,
53
+ "gas": 500000,
54
+ "nonce": 5,
55
+ "from": "0xExecutor",
51
56
  }
52
57
  # Mock nonce call
53
58
  mock_contract.functions.nonce().call.return_value = 5
@@ -64,14 +69,14 @@ def real_safe_tx_mock_eth(mock_chain_interface):
64
69
  0,
65
70
  b"",
66
71
  0,
67
- 200000, # safe_tx_gas
72
+ 200000, # safe_tx_gas
68
73
  0,
69
74
  0,
70
75
  None,
71
76
  None,
72
77
  signatures=MOCK_SIGNATURE,
73
78
  safe_nonce=5,
74
- chain_id=1
79
+ chain_id=1,
75
80
  )
76
81
 
77
82
  # HACK: Force initialize properties that rely on cached_property + network
@@ -96,7 +101,7 @@ def test_integration_full_execution_flow(mock_chain_interface, real_safe_tx_mock
96
101
  # Use a dummy key (needs to be valid hex for account generation if SafeTx uses it)
97
102
  dummy_key = "0x" + "1" * 64
98
103
 
99
- with patch.object(executor, '_recreate_safe_client', return_value=MagicMock()):
104
+ with patch.object(executor, "_recreate_safe_client", return_value=MagicMock()):
100
105
  # Pre-execution check
101
106
  assert len(safe_tx.signatures) == 65
102
107
 
@@ -129,12 +134,12 @@ def test_integration_retry_preserves_signatures(mock_chain_interface, real_safe_
129
134
  # Second attempt: Success (status 1)
130
135
  mock_chain_interface.web3.eth.wait_for_transaction_receipt.side_effect = [
131
136
  ValueError("Transaction not found"),
132
- MagicMock(status=1)
137
+ MagicMock(status=1),
133
138
  ]
134
139
 
135
140
  dummy_key = "0x" + "1" * 64
136
141
 
137
- with patch.object(executor, '_recreate_safe_client', return_value=MagicMock()):
142
+ with patch.object(executor, "_recreate_safe_client", return_value=MagicMock()):
138
143
  with patch("time.sleep"): # Skip sleep
139
144
  success, tx_hash, receipt = executor.execute_with_retry("0xSafe", safe_tx, [dummy_key])
140
145
 
File without changes