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
@@ -52,6 +52,7 @@ class SafeTransactionExecutor:
52
52
  config = Config().core
53
53
  self.max_retries = max_retries or config.safe_tx_max_retries
54
54
  self.gas_buffer = gas_buffer or config.safe_tx_gas_buffer
55
+ self._client_cache: Dict[str, EthereumClient] = {}
55
56
 
56
57
  def execute_with_retry(
57
58
  self,
@@ -81,17 +82,27 @@ class SafeTransactionExecutor:
81
82
  try:
82
83
  # Prepare and execute attempt
83
84
  tx_hash = self._execute_attempt(
84
- safe_address, safe_tx, signer_keys, operation_name, attempt, current_gas, base_estimate
85
+ safe_address,
86
+ safe_tx,
87
+ signer_keys,
88
+ operation_name,
89
+ attempt,
90
+ current_gas,
91
+ base_estimate,
85
92
  )
86
93
 
87
94
  # Check receipt
88
95
  receipt = self.chain_interface.web3.eth.wait_for_transaction_receipt(tx_hash)
89
96
  if self._check_receipt_status(receipt):
90
97
  SAFE_TX_STATS["final_successes"] += 1
91
- logger.info(f"[{operation_name}] Success on attempt {attempt + 1}. Tx Hash: {tx_hash}")
98
+ logger.info(
99
+ f"[{operation_name}] Success on attempt {attempt + 1}. Tx Hash: {tx_hash}"
100
+ )
92
101
  return True, tx_hash, receipt
93
102
 
94
- logger.error(f"[{operation_name}] Mined but failed (status 0) on attempt {attempt + 1}.")
103
+ logger.error(
104
+ f"[{operation_name}] Mined but failed (status 0) on attempt {attempt + 1}."
105
+ )
95
106
  raise ValueError("Transaction reverted on-chain")
96
107
 
97
108
  except Exception as e:
@@ -112,7 +123,14 @@ class SafeTransactionExecutor:
112
123
  return False, str(last_error), None
113
124
 
114
125
  def _execute_attempt(
115
- self, safe_address, safe_tx, signer_keys, operation_name, attempt, current_gas, base_estimate
126
+ self,
127
+ safe_address,
128
+ safe_tx,
129
+ signer_keys,
130
+ operation_name,
131
+ attempt,
132
+ current_gas,
133
+ base_estimate,
116
134
  ) -> str:
117
135
  """Prepare client, estimate gas, simulate, and execute."""
118
136
  # 1. (Re)Create Safe client
@@ -186,7 +204,12 @@ class SafeTransactionExecutor:
186
204
  return status == 1
187
205
 
188
206
  def _handle_execution_failure(
189
- self, error: Exception, safe_address: str, safe_tx: SafeTx, attempt: int, operation_name: str
207
+ self,
208
+ error: Exception,
209
+ safe_address: str,
210
+ safe_tx: SafeTx,
211
+ attempt: int,
212
+ operation_name: str,
190
213
  ) -> Tuple[SafeTx, bool]:
191
214
  """Handle execution failure and determine next steps."""
192
215
  classification = self._classify_error(error)
@@ -220,7 +243,9 @@ class SafeTransactionExecutor:
220
243
  """Estimate gas for a Safe transaction with buffer and hard cap."""
221
244
  try:
222
245
  # Use on-chain simulation via safe-eth-py
223
- estimated = safe.estimate_tx_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation)
246
+ estimated = safe.estimate_tx_gas(
247
+ safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation
248
+ )
224
249
  with_buffer = int(estimated * self.gas_buffer)
225
250
 
226
251
  # Apply x10 hard cap if we have a base estimate
@@ -237,16 +262,17 @@ class SafeTransactionExecutor:
237
262
 
238
263
  def _recreate_safe_client(self, safe_address: str) -> Safe:
239
264
  """Recreate Safe with current (possibly rotated) RPC."""
240
- ethereum_client = EthereumClient(self.chain_interface.current_rpc)
265
+ rpc_url = self.chain_interface.current_rpc
266
+ if rpc_url not in self._client_cache:
267
+ self._client_cache[rpc_url] = EthereumClient(rpc_url)
268
+ ethereum_client = self._client_cache[rpc_url]
241
269
  return Safe(safe_address, ethereum_client)
242
270
 
243
271
  def _is_nonce_error(self, error: Exception) -> bool:
244
272
  """Check if error is due to Safe nonce conflict."""
245
273
  error_text = str(error).lower()
246
274
  # GS025 = Invalid nonce (NOT GS026 which is invalid signatures)
247
- return any(x in error_text for x in [
248
- "nonce", "gs025", "already executed", "duplicate"
249
- ])
275
+ return any(x in error_text for x in ["nonce", "gs025", "already executed", "duplicate"])
250
276
 
251
277
  def _is_signature_error(self, error: Exception) -> bool:
252
278
  """Check if error is due to invalid Safe signatures.
@@ -257,10 +283,17 @@ class SafeTransactionExecutor:
257
283
  GS026 = Invalid owner (signature from non-owner)
258
284
  """
259
285
  error_text = str(error).lower()
260
- return any(x in error_text for x in [
261
- "gs020", "gs021", "gs024", "gs026",
262
- "invalid signatures", "signatures data too short"
263
- ])
286
+ return any(
287
+ x in error_text
288
+ for x in [
289
+ "gs020",
290
+ "gs021",
291
+ "gs024",
292
+ "gs026",
293
+ "invalid signatures",
294
+ "signatures data too short",
295
+ ]
296
+ )
264
297
 
265
298
  def _refresh_nonce(self, safe: Safe, safe_tx: SafeTx) -> SafeTx:
266
299
  """Re-fetch nonce and rebuild transaction."""
@@ -283,7 +316,9 @@ class SafeTransactionExecutor:
283
316
  def _classify_error(self, error: Exception) -> dict:
284
317
  """Classify Safe transaction errors for retry decisions."""
285
318
  err_text = str(error).lower()
286
- is_rpc = self.chain_interface._is_rate_limit_error(error) or self.chain_interface._is_connection_error(error)
319
+ is_rpc = self.chain_interface._is_rate_limit_error(
320
+ error
321
+ ) or self.chain_interface._is_connection_error(error)
287
322
 
288
323
  return {
289
324
  "is_gas_error": any(x in err_text for x in ["gas", "out of gas", "intrinsic"]),
@@ -296,6 +331,7 @@ class SafeTransactionExecutor:
296
331
  def _decode_revert_reason(self, error: Exception) -> Optional[str]:
297
332
  """Attempt to decode the revert reason."""
298
333
  import re
334
+
299
335
  error_text = str(error)
300
336
  hex_match = re.search(r"0x[0-9a-fA-F]{8,}", error_text)
301
337
  if hex_match:
@@ -311,6 +347,4 @@ class SafeTransactionExecutor:
311
347
 
312
348
  def _log_retry(self, attempt: int, error: Exception, strategy: str):
313
349
  """Log a retry attempt."""
314
- logger.warning(
315
- f"Safe TX attempt {attempt} failed, strategy: {strategy}. Error: {error}"
316
- )
350
+ logger.warning(f"Safe TX attempt {attempt} failed, strategy: {strategy}. Error: {error}")
@@ -45,19 +45,29 @@ class TransferLogger:
45
45
  if tx_hash:
46
46
  try:
47
47
  tx = self.chain_interface.web3.eth.get_transaction(tx_hash)
48
- native_value = getattr(tx, "value", 0) or tx.get("value", 0) if isinstance(tx, dict) else getattr(tx, "value", 0)
48
+ native_value = (
49
+ getattr(tx, "value", 0) or tx.get("value", 0)
50
+ if isinstance(tx, dict)
51
+ else getattr(tx, "value", 0)
52
+ )
49
53
  if native_value and int(native_value) > 0:
50
- from_addr = getattr(tx, "from", "") if hasattr(tx, "from") else tx.get("from", "")
54
+ from_addr = (
55
+ getattr(tx, "from", "") if hasattr(tx, "from") else tx.get("from", "")
56
+ )
51
57
  # Handle AttributeDict's special 'from' attribute
52
58
  if not from_addr and hasattr(tx, "__getitem__"):
53
59
  from_addr = tx["from"]
54
- to_addr = getattr(tx, "to", "") or (tx.get("to", "") if isinstance(tx, dict) else "")
60
+ to_addr = getattr(tx, "to", "") or (
61
+ tx.get("to", "") if isinstance(tx, dict) else ""
62
+ )
55
63
  self._log_native_transfer(from_addr, to_addr, native_value)
56
64
  except Exception as e:
57
65
  logger.debug(f"Could not get tx for native transfer logging: {e}")
58
66
 
59
67
  # Log ERC20 transfers from event logs
60
- logs = receipt.get("logs", []) if isinstance(receipt, dict) else getattr(receipt, "logs", [])
68
+ logs = (
69
+ receipt.get("logs", []) if isinstance(receipt, dict) else getattr(receipt, "logs", [])
70
+ )
61
71
 
62
72
  for log in logs:
63
73
  self._process_log(log)
@@ -156,7 +166,9 @@ class TransferLogger:
156
166
  else:
157
167
  # Likely an NFT (ERC721) - the amount is the token ID
158
168
  if amount_wei > 0:
159
- logger.info(f"[NFT TRANSFER] Token #{amount_wei} {token_label}: {from_label} → {to_label}")
169
+ logger.info(
170
+ f"[NFT TRANSFER] Token #{amount_wei} {token_label}: {from_label} → {to_label}"
171
+ )
160
172
  else:
161
173
  logger.debug(f"[NFT TRANSFER] {token_label}: {from_label} → {to_label}")
162
174
 
@@ -265,9 +277,7 @@ class TransactionService:
265
277
  """Inner operation wrapped by with_retry."""
266
278
  try:
267
279
  signed_txn = self.key_storage.sign_transaction(tx, signer_address_or_tag)
268
- txn_hash = chain_interface.web3.eth.send_raw_transaction(
269
- signed_txn.raw_transaction
270
- )
280
+ txn_hash = chain_interface.web3.eth.send_raw_transaction(signed_txn.raw_transaction)
271
281
  receipt = chain_interface.web3.eth.wait_for_transaction_receipt(txn_hash)
272
282
 
273
283
  status = getattr(receipt, "status", None)
@@ -412,10 +422,12 @@ class TransactionService:
412
422
  signer_account: StoredSafeAccount,
413
423
  chain_interface,
414
424
  chain_name: str,
415
- tags: List[str] = None
425
+ tags: List[str] = None,
416
426
  ) -> Tuple[bool, Dict]:
417
427
  """Execute transaction via SafeService."""
418
- logger.info(f"Routing transaction via Safe {self._resolve_label(signer_account.address, chain_name)}...")
428
+ logger.info(
429
+ f"Routing transaction via Safe {self._resolve_label(signer_account.address, chain_name)}..."
430
+ )
419
431
 
420
432
  try:
421
433
  # Extract basic params
@@ -431,7 +443,7 @@ class TransactionService:
431
443
  to=to_addr,
432
444
  value=value,
433
445
  chain_name=chain_name,
434
- data=data
446
+ data=data,
435
447
  )
436
448
 
437
449
  # Receipt is already waited for inside execute_safe_transaction/executor
@@ -445,7 +457,13 @@ class TransactionService:
445
457
  if receipt and status == 1:
446
458
  logger.info(f"Safe transaction executed successfully. Tx Hash: {tx_hash}")
447
459
  self._log_successful_transaction(
448
- receipt, tx, signer_account, chain_name, bytes.fromhex(tx_hash.replace("0x", "")), tags, chain_interface
460
+ receipt,
461
+ tx,
462
+ signer_account,
463
+ chain_name,
464
+ bytes.fromhex(tx_hash.replace("0x", "")),
465
+ tags,
466
+ chain_interface,
449
467
  )
450
468
  return True, receipt
451
469
  else:
@@ -460,11 +478,13 @@ class TransactionService:
460
478
  # Extract hex data from common error patterns
461
479
  # Pattern 1: ('execution reverted', '0x...')
462
480
  import re
481
+
463
482
  hex_match = re.search(r"0x[0-9a-fA-F]{8,}", error_text)
464
483
 
465
484
  if hex_match:
466
485
  try:
467
486
  from iwa.core.contracts.decoder import ErrorDecoder
487
+
468
488
  data = hex_match.group(0)
469
489
  decoded = ErrorDecoder().decode(data)
470
490
  if decoded:
@@ -14,7 +14,6 @@ if TYPE_CHECKING:
14
14
  from iwa.core.services.transfer import TransferService
15
15
 
16
16
 
17
-
18
17
  class ERC20TransferMixin:
19
18
  """Mixin for ERC20 token transfers and approvals."""
20
19
 
@@ -91,7 +91,7 @@ class NativeTransferMixin:
91
91
  "from": from_account.address,
92
92
  "to": to_address,
93
93
  "value": amount_wei,
94
- }
94
+ },
95
95
  )
96
96
 
97
97
  # Use unified TransactionService
@@ -41,7 +41,9 @@ class TestGnosisFeeFix(unittest.TestCase):
41
41
 
42
42
  # CRITICAL ASSERTION: maxPriorityFeePerGas must be >= 1
43
43
  # If the fix works, it should be 1. If it fails (old behavior), it would be 0.
44
- self.assertEqual(params["maxPriorityFeePerGas"], 1, "Priority fee should be forced to 1 wei")
44
+ self.assertEqual(
45
+ params["maxPriorityFeePerGas"], 1, "Priority fee should be forced to 1 wei"
46
+ )
45
47
 
46
48
  # Verify max fee calculation: (base * 1.5) + priority
47
49
  expected_max_fee = int(5000 * 1.5) + 1
@@ -84,4 +86,6 @@ class TestGnosisFeeFix(unittest.TestCase):
84
86
 
85
87
  params = eth_interface.calculate_transaction_params(mock_func, {"from": "0x123"})
86
88
 
87
- self.assertEqual(params["maxPriorityFeePerGas"], 1, "Generic fallback should apply to all chains")
89
+ self.assertEqual(
90
+ params["maxPriorityFeePerGas"], 1, "Generic fallback should apply to all chains"
91
+ )
@@ -61,7 +61,7 @@ def test_push_to_ipfs_sync_uses_session(mock_config, mock_cid_decode):
61
61
 
62
62
  # Verify second call reuses session
63
63
  push_to_ipfs_sync(b"test data 2")
64
- mock_session_cls.assert_called_once() # Should still be 1 call
64
+ mock_session_cls.assert_called_once() # Should still be 1 call
65
65
  assert mock_session.post.call_count == 2
66
66
 
67
67
 
@@ -34,18 +34,14 @@ class TestRegressionFixes(unittest.TestCase):
34
34
  # This is what we are testing: get_suggested_fees() provides the safety net
35
35
  mock_chain_interface.get_suggested_fees.return_value = {
36
36
  "maxFeePerGas": 1500,
37
- "maxPriorityFeePerGas": 10
37
+ "maxPriorityFeePerGas": 10,
38
38
  }
39
39
 
40
40
  with patch("iwa.core.services.transaction.ChainInterfaces") as mock_interfaces:
41
41
  mock_interfaces.return_value.get.return_value = mock_chain_interface
42
42
 
43
43
  # 3. Prepare transaction WITHOUT fees
44
- tx = {
45
- "to": "0x09312C66A14a024B4e903D986Ca7E2C0dDD06227",
46
- "value": 1000,
47
- "gas": 21000
48
- }
44
+ tx = {"to": "0x09312C66A14a024B4e903D986Ca7E2C0dDD06227", "value": 1000, "gas": 21000}
49
45
 
50
46
  # 4. Run internal preparation
51
47
  service._prepare_transaction(tx, "signer", mock_chain_interface)
@@ -96,5 +92,6 @@ class TestRegressionFixes(unittest.TestCase):
96
92
  self.assertIsInstance(accounts[key]["address"], str)
97
93
  self.assertEqual(accounts[key]["address"].lower(), addr_str.lower())
98
94
 
95
+
99
96
  if __name__ == "__main__":
100
97
  unittest.main()
iwa/core/utils.py CHANGED
@@ -85,9 +85,11 @@ def configure_logger():
85
85
  configure_logger.configured = True
86
86
  return logger
87
87
 
88
+
88
89
  def get_version(package_name: str) -> str:
89
90
  """Get package version."""
90
91
  from importlib.metadata import PackageNotFoundError, version
92
+
91
93
  try:
92
94
  return version(package_name)
93
95
  except PackageNotFoundError:
iwa/core/wallet.py CHANGED
@@ -37,7 +37,9 @@ class Wallet:
37
37
  self.balance_service = BalanceService(self.key_storage, self.account_service)
38
38
  self.safe_service = SafeService(self.key_storage, self.account_service)
39
39
  # self.transaction_manager = TransactionManager(self.key_storage, self.account_service)
40
- self.transaction_service = TransactionService(self.key_storage, self.account_service, self.safe_service)
40
+ self.transaction_service = TransactionService(
41
+ self.key_storage, self.account_service, self.safe_service
42
+ )
41
43
 
42
44
  self.transfer_service = TransferService(
43
45
  self.key_storage,
@@ -92,8 +92,12 @@ OLAS_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
92
92
  OLAS_TRADER_STAKING_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
93
93
  "gnosis": {
94
94
  # === LEGACY (no marketplace) ===
95
- "Hobbyist 1 Legacy (100 OLAS)": EthereumAddress("0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C"),
96
- "Hobbyist 2 Legacy (500 OLAS)": EthereumAddress("0x238EB6993b90A978ec6AAD7530D6429c949C08DA"),
95
+ "Hobbyist 1 Legacy (100 OLAS)": EthereumAddress(
96
+ "0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C"
97
+ ),
98
+ "Hobbyist 2 Legacy (500 OLAS)": EthereumAddress(
99
+ "0x238EB6993b90A978ec6AAD7530D6429c949C08DA"
100
+ ),
97
101
  "Expert Legacy (1k OLAS)": EthereumAddress("0x5344B7DD311e5d3DdDd46A4f71481Bd7b05AAA3e"),
98
102
  "Expert 2 Legacy (1k OLAS)": EthereumAddress("0xb964e44c126410df341ae04B13aB10A985fE3513"),
99
103
  "Expert 3 Legacy (2k OLAS)": EthereumAddress("0x80faD33Cadb5F53f9D29F02Db97D682E8B101618"),
@@ -103,9 +107,15 @@ OLAS_TRADER_STAKING_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
103
107
  "Expert 7 Legacy (10k OLAS)": EthereumAddress("0xD7A3C8b975f71030135f1a66E9e23164d54fF455"),
104
108
  "Expert 8 Legacy (2k OLAS)": EthereumAddress("0x356C108D49C5eebd21c84c04E9162de41933030c"),
105
109
  "Expert 9 Legacy (10k OLAS)": EthereumAddress("0x17dBAe44BC5618Cc254055B386A29576b4F87015"),
106
- "Expert 10 Legacy (10k OLAS)": EthereumAddress("0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f"),
107
- "Expert 11 Legacy (10k OLAS)": EthereumAddress("0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7"),
108
- "Expert 12 Legacy (10k OLAS)": EthereumAddress("0xF4a75F476801B3fBB2e7093aCDcc3576593Cc1fc"),
110
+ "Expert 10 Legacy (10k OLAS)": EthereumAddress(
111
+ "0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f"
112
+ ),
113
+ "Expert 11 Legacy (10k OLAS)": EthereumAddress(
114
+ "0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7"
115
+ ),
116
+ "Expert 12 Legacy (10k OLAS)": EthereumAddress(
117
+ "0xF4a75F476801B3fBB2e7093aCDcc3576593Cc1fc"
118
+ ),
109
119
  # === MM v1 (old marketplace 0x4554fE75...) ===
110
120
  "Expert 15 MM v1 (10k OLAS)": EthereumAddress("0x88eB38FF79fBa8C19943C0e5Acfa67D5876AdCC1"),
111
121
  "Expert 16 MM v1 (10k OLAS)": EthereumAddress("0x6c65430515c70a3f5E62107CC301685B7D46f991"),
@@ -48,7 +48,6 @@ class ActivityCheckerContract(ContractInstance):
48
48
  self._agent_mech: Optional[EthereumAddress] = None
49
49
  self._liveness_ratio: Optional[int] = None
50
50
 
51
-
52
51
  def get_multisig_nonces(self, multisig: EthereumAddress) -> Tuple[int, int]:
53
52
  """Get the nonces for a multisig address.
54
53
 
@@ -64,7 +63,6 @@ class ActivityCheckerContract(ContractInstance):
64
63
  nonces = self.contract.functions.getMultisigNonces(multisig).call()
65
64
  return (nonces[0], nonces[1])
66
65
 
67
-
68
66
  @property
69
67
  def mech_marketplace(self) -> Optional[EthereumAddress]:
70
68
  """Get the mech marketplace address."""
@@ -83,7 +81,9 @@ class ActivityCheckerContract(ContractInstance):
83
81
  try:
84
82
  agent_mech_function = getattr(self.contract.functions, "agentMech", None)
85
83
  self._agent_mech = (
86
- agent_mech_function().call() if agent_mech_function else DEFAULT_MECH_CONTRACT_ADDRESS
84
+ agent_mech_function().call()
85
+ if agent_mech_function
86
+ else DEFAULT_MECH_CONTRACT_ADDRESS
87
87
  )
88
88
  except Exception:
89
89
  self._agent_mech = DEFAULT_MECH_CONTRACT_ADDRESS
@@ -82,7 +82,6 @@ class StakingContract(ContractInstance):
82
82
  self._activity_checker: Optional[ActivityCheckerContract] = None
83
83
  self._activity_checker_address: Optional[EthereumAddress] = None
84
84
 
85
-
86
85
  def get_requirements(self) -> Dict[str, Union[str, int]]:
87
86
  """Get the contract requirements for token and deposits.
88
87
 
@@ -1,6 +1,5 @@
1
1
  """Event-based cache invalidation for Olas contracts."""
2
2
 
3
-
4
3
  from loguru import logger
5
4
 
6
5
  from iwa.core.contracts.cache import ContractCache
@@ -75,7 +74,7 @@ class OlasEventInvalidator:
75
74
  except Exception as e:
76
75
  logger.error(f"Error in OlasEventInvalidator: {e}")
77
76
 
78
- time.sleep(10) # check every 10 seconds
77
+ time.sleep(10) # check every 10 seconds
79
78
 
80
79
  def _check_events(self, from_block: int, to_block: int):
81
80
  """Check for relevant events in the block range."""
@@ -101,14 +100,19 @@ class OlasEventInvalidator:
101
100
  StakingContract, self.staking_addresses[0], self.chain_name
102
101
  )
103
102
 
104
-
105
- logs = self.web3.eth.get_logs({
106
- "fromBlock": from_block,
107
- "toBlock": to_block,
108
- "address": self.staking_addresses,
109
- "topics": [self.web3.keccak(text="Checkpoint(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)").hex()]
110
- # Note: signature might vary, safer to use the event object if ABI allows
111
- })
103
+ logs = self.web3.eth.get_logs(
104
+ {
105
+ "fromBlock": from_block,
106
+ "toBlock": to_block,
107
+ "address": self.staking_addresses,
108
+ "topics": [
109
+ self.web3.keccak(
110
+ text="Checkpoint(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)"
111
+ ).hex()
112
+ ],
113
+ # Note: signature might vary, safer to use the event object if ABI allows
114
+ }
115
+ )
112
116
 
113
117
  # If we used the contract event object to filter, it handles the topic generation:
114
118
  # logs = checkpoint_event_abi.get_logs(fromBlock=from_block, toBlock=to_block)
@@ -130,9 +134,7 @@ class OlasEventInvalidator:
130
134
  # self.contract_cache.invalidate(StakingContract, addr, self.chain_name)
131
135
 
132
136
  # Option B: Get instance and clear specific cache (safe public access)
133
- instance = self.contract_cache.get_if_cached(
134
- StakingContract, addr, self.chain_name
135
- )
137
+ instance = self.contract_cache.get_if_cached(StakingContract, addr, self.chain_name)
136
138
  if instance:
137
139
  instance.clear_epoch_cache()
138
140
  logger.debug(f"Cleared epoch cache for {addr}")
@@ -181,9 +181,7 @@ class OlasServiceImporter:
181
181
  if service.service_id:
182
182
  key = f"{service.chain_name}:{service.service_id}"
183
183
  if key in seen_keys:
184
- logger.debug(
185
- f"Skipping duplicate service {key} from {service.source_folder}"
186
- )
184
+ logger.debug(f"Skipping duplicate service {key} from {service.source_folder}")
187
185
  duplicates += 1
188
186
  continue
189
187
  seen_keys.add(key)
@@ -482,9 +480,7 @@ class OlasServiceImporter:
482
480
  staking_program_id, chain_name
483
481
  )
484
482
 
485
- def _resolve_staking_contract(
486
- self, staking_program_id: str, chain_name: str
487
- ) -> Optional[str]:
483
+ def _resolve_staking_contract(self, staking_program_id: str, chain_name: str) -> Optional[str]:
488
484
  """Resolve a staking program ID to a contract address."""
489
485
  address = STAKING_PROGRAM_MAP.get(staking_program_id)
490
486
  if address:
@@ -540,8 +536,10 @@ class OlasServiceImporter:
540
536
 
541
537
  # Check for "safes" entry which indicates the owner is a Safe
542
538
  # Structure: "safes": { "gnosis": "0x..." }
543
- if "safes" in data and FLAGS_OWNER_SAFE in data["safes"]: # Need to detect chain dynamically or iterate
544
- pass
539
+ if (
540
+ "safes" in data and FLAGS_OWNER_SAFE in data["safes"]
541
+ ): # Need to detect chain dynamically or iterate
542
+ pass
545
543
 
546
544
  # Logic update:
547
545
  # 1. Capture EOA address always (it's the signer)
@@ -557,9 +555,13 @@ class OlasServiceImporter:
557
555
  if safe_owner_address:
558
556
  # CASE: Owner is Safe
559
557
  service.service_owner_multisig_address = safe_owner_address
560
- service.service_owner_eoa_address = eoa_address # The EOA is the signer/controller
558
+ service.service_owner_eoa_address = (
559
+ eoa_address # The EOA is the signer/controller
560
+ )
561
561
 
562
- logger.debug(f"Extracted Safe owner address: {safe_owner_address} (Signer: {eoa_address})")
562
+ logger.debug(
563
+ f"Extracted Safe owner address: {safe_owner_address} (Signer: {eoa_address})"
564
+ )
563
565
  elif eoa_address:
564
566
  # CASE: Owner is EOA
565
567
  service.service_owner_eoa_address = eoa_address
@@ -767,8 +769,8 @@ class OlasServiceImporter:
767
769
  safe_result = self._import_safe(
768
770
  address=service.safe_address,
769
771
  signers=self._get_agent_signers(service),
770
- tag_suffix="multisig", # e.g. trader_zeta_safe
771
- service_name=service.service_name
772
+ tag_suffix="multisig", # e.g. trader_zeta_safe
773
+ service_name=service.service_name,
772
774
  )
773
775
  if safe_result[0]:
774
776
  result.imported_safes.append(service.safe_address)
@@ -778,19 +780,22 @@ class OlasServiceImporter:
778
780
  result.errors.append(f"Safe {service.safe_address}: {safe_result[1]}")
779
781
 
780
782
  # 2. Import Owner Safe (if it exists and is different)
781
- if service.service_owner_multisig_address and service.service_owner_multisig_address != service.safe_address:
782
- # Signer for Owner Safe is the EOA owner key
783
+ if (
784
+ service.service_owner_multisig_address
785
+ and service.service_owner_multisig_address != service.safe_address
786
+ ):
787
+ # Signer for Owner Safe is the EOA owner key
783
788
  owner_signers = self._get_owner_signers(service)
784
789
 
785
790
  safe_result = self._import_safe(
786
791
  address=service.service_owner_multisig_address,
787
792
  signers=owner_signers,
788
- tag_suffix="owner_multisig", # e.g. trader_zeta_owner_safe
789
- service_name=service.service_name
793
+ tag_suffix="owner_multisig", # e.g. trader_zeta_owner_safe
794
+ service_name=service.service_name,
790
795
  )
791
796
  if safe_result[0]:
792
- result.imported_safes.append(service.service_owner_multisig_address)
793
- logger.info(f"Imported Owner Safe {service.service_owner_multisig_address}")
797
+ result.imported_safes.append(service.service_owner_multisig_address)
798
+ logger.info(f"Imported Owner Safe {service.service_owner_multisig_address}")
794
799
 
795
800
  def _get_agent_signers(self, service: DiscoveredService) -> List[str]:
796
801
  """Get list of signers for the agent safe."""
@@ -926,7 +931,7 @@ class OlasServiceImporter:
926
931
  address: str,
927
932
  signers: List[str] = None,
928
933
  tag_suffix: str = "multisig",
929
- service_name: Optional[str] = None
934
+ service_name: Optional[str] = None,
930
935
  ) -> Tuple[bool, str]:
931
936
  """Import a generic Safe."""
932
937
  if not address:
@@ -1062,4 +1067,5 @@ class OlasServiceImporter:
1062
1067
  key.signature_failed = True
1063
1068
  logger.warning(f"Error verifying signature for key {key.address}: {e}")
1064
1069
 
1065
- FLAGS_OWNER_SAFE="deprecated"
1070
+
1071
+ FLAGS_OWNER_SAFE = "deprecated"
@@ -151,7 +151,9 @@ class OlasPlugin(Plugin):
151
151
 
152
152
  is_signer = key_addr in [s.lower() for s in on_chain_signers]
153
153
  if not is_signer:
154
- safe_text += f"\n[bold red]⚠ Agent {agent_key.address} - NOT A SIGNER![/bold red]"
154
+ safe_text += (
155
+ f"\n[bold red]⚠ Agent {agent_key.address} - NOT A SIGNER![/bold red]"
156
+ )
155
157
  else:
156
158
  safe_text += f" (Signer: {agent_key.address[:6]}...)"
157
159
 
@@ -182,21 +184,21 @@ class OlasPlugin(Plugin):
182
184
  # 1. Display Signer/EOA Owner
183
185
  owner_key = next((k for k in service.keys if k.role == "owner"), None)
184
186
  if owner_key:
185
- val = owner_key.address
186
- if not val.startswith("0x"):
187
- val = "0x" + val
188
-
189
- if owner_key.signature_verified:
190
- val = f"[green]{val}[/green]"
191
- elif not owner_key.is_encrypted:
192
- val = f"[red]{val}[/red]"
193
- status = "🔒 encrypted" if owner_key.is_encrypted else "🔓 plaintext"
194
- table.add_row("Owner (EOA)", f"{val} {status}")
187
+ val = owner_key.address
188
+ if not val.startswith("0x"):
189
+ val = "0x" + val
190
+
191
+ if owner_key.signature_verified:
192
+ val = f"[green]{val}[/green]"
193
+ elif not owner_key.is_encrypted:
194
+ val = f"[red]{val}[/red]"
195
+ status = "🔒 encrypted" if owner_key.is_encrypted else "🔓 plaintext"
196
+ table.add_row("Owner (EOA)", f"{val} {status}")
195
197
  elif service.service_owner_eoa_address:
196
- # Fallback if we have an address but no key object
197
- table.add_row("Owner (EOA)", service.service_owner_eoa_address)
198
+ # Fallback if we have an address but no key object
199
+ table.add_row("Owner (EOA)", service.service_owner_eoa_address)
198
200
  else:
199
- table.add_row("Owner (EOA)", "[yellow]N/A[/yellow]")
201
+ table.add_row("Owner (EOA)", "[yellow]N/A[/yellow]")
200
202
 
201
203
  # 2. Display Safe Owner
202
204
  if service.service_owner_multisig_address:
@@ -135,9 +135,7 @@ class DrainManagerMixin:
135
135
  withdrawal_tag = self.wallet.get_tag_by_address(withdrawal_address) or withdrawal_address
136
136
  multisig_tag = self.wallet.get_tag_by_address(multisig_address) or multisig_address
137
137
 
138
- logger.info(
139
- f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}"
140
- )
138
+ logger.info(f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}")
141
139
 
142
140
  # Transfer from Safe to withdrawal address
143
141
  tx_hash = self.wallet.send(