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
iwa/core/pricing.py CHANGED
@@ -4,9 +4,9 @@ import time
4
4
  from datetime import datetime, timedelta
5
5
  from typing import Dict, Optional
6
6
 
7
- import requests
8
7
  from loguru import logger
9
8
 
9
+ from iwa.core.http import create_retry_session
10
10
  from iwa.core.secrets import secrets
11
11
 
12
12
  # Global cache shared across all PriceService instances
@@ -28,28 +28,12 @@ class PriceService:
28
28
  if self.secrets.coingecko_api_key
29
29
  else None
30
30
  )
31
- self.session = requests.Session()
32
- # Configure retry strategy
33
- from requests.adapters import HTTPAdapter
34
- from urllib3.util.retry import Retry
35
-
36
- retry_strategy = Retry(
37
- total=3,
38
- backoff_factor=1,
39
- status_forcelist=[429, 500, 502, 503, 504],
40
- )
41
- adapter = HTTPAdapter(max_retries=retry_strategy)
42
- self.session.mount("https://", adapter)
43
- self.session.mount("http://", adapter)
31
+ self.session = create_retry_session()
44
32
 
45
33
  def close(self):
46
34
  """Close the session."""
47
35
  self.session.close()
48
36
 
49
- def __del__(self):
50
- """Cleanup on deletion."""
51
- self.close()
52
-
53
37
  def get_token_price(self, token_id: str, vs_currency: str = "eur") -> Optional[float]:
54
38
  """Get token price in specified currency.
55
39
 
@@ -115,9 +99,7 @@ class PriceService:
115
99
  return float(data[token_id][vs_currency])
116
100
 
117
101
  # If we got response but price not found, it's likely a wrong ID
118
- logger.debug(
119
- f"Price for {token_id} in {vs_currency} not found in response: {data}"
120
- )
102
+ logger.debug(f"Price for {token_id} in {vs_currency} not found in response: {data}")
121
103
  return None
122
104
 
123
105
  except Exception as e:
iwa/core/rpc_monitor.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """RPC Monitor for tracking API usage."""
2
+
2
3
  import threading
3
4
  from collections import defaultdict
4
5
  from typing import Dict
@@ -88,4 +88,3 @@ class BalanceService:
88
88
 
89
89
  contract = ERC20Contract(chain_name=chain_name, address=token_address)
90
90
  return contract.balance_of_wei(account.address)
91
-
iwa/core/services/safe.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Safe service module."""
2
2
 
3
- from typing import TYPE_CHECKING, List, Optional, Tuple
3
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
4
4
 
5
5
  from loguru import logger
6
6
  from safe_eth.eth import EthereumClient
@@ -35,6 +35,7 @@ class SafeService:
35
35
  """Initialize SafeService."""
36
36
  self.key_storage = key_storage
37
37
  self.account_service = account_service
38
+ self._client_cache: Dict[str, EthereumClient] = {}
38
39
 
39
40
  def create_safe(
40
41
  self,
@@ -102,7 +103,12 @@ class SafeService:
102
103
 
103
104
  # Use ChainInterface which has proper RPC rotation and parsing
104
105
  chain_interface = ChainInterfaces().get(chain_name)
105
- return EthereumClient(chain_interface.current_rpc)
106
+ rpc_url = chain_interface.current_rpc
107
+
108
+ if rpc_url not in self._client_cache:
109
+ self._client_cache[rpc_url] = EthereumClient(rpc_url)
110
+
111
+ return self._client_cache[rpc_url]
106
112
 
107
113
  def _deploy_safe_contract(
108
114
  self,
@@ -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}")