iwa 0.0.58__py3-none-any.whl → 0.0.59__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 (58) hide show
  1. iwa/core/chain/interface.py +32 -21
  2. iwa/core/chain/rate_limiter.py +0 -6
  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.59.dist-info}/METADATA +1 -1
  46. {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/RECORD +58 -57
  47. tests/test_balance_service.py +0 -2
  48. tests/test_chain.py +1 -2
  49. tests/test_rate_limiter_retry.py +2 -7
  50. tests/test_rpc_efficiency.py +4 -1
  51. tests/test_rpc_rate_limit.py +1 -0
  52. tests/test_rpc_rotation.py +4 -4
  53. tests/test_safe_executor.py +76 -50
  54. tests/test_safe_integration.py +11 -6
  55. {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/WHEEL +0 -0
  56. {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/entry_points.txt +0 -0
  57. {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/licenses/LICENSE +0 -0
  58. {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/top_level.txt +0 -0
@@ -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