iwa 0.0.20__py3-none-any.whl → 0.0.23__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.
@@ -48,10 +48,17 @@ class ChainInterface:
48
48
  self._rotation_lock = threading.Lock()
49
49
  self._init_web3()
50
50
 
51
+ @property
52
+ def current_rpc(self) -> str:
53
+ """Get the current active RPC URL."""
54
+ if not self.chain.rpcs:
55
+ return ""
56
+ return self.chain.rpcs[self._current_rpc_index]
57
+
51
58
  @property
52
59
  def is_tenderly(self) -> bool:
53
60
  """Check if connected to Tenderly vNet."""
54
- rpc = self.chain.rpc or ""
61
+ rpc = self.current_rpc or ""
55
62
  return "tenderly" in rpc.lower() or "virtual" in rpc.lower()
56
63
 
57
64
  def init_block_tracking(self):
iwa/core/monitor.py CHANGED
@@ -24,7 +24,7 @@ class EventMonitor:
24
24
  self.chain_interface = ChainInterfaces().get(chain_name)
25
25
  self.web3 = self.chain_interface.web3
26
26
  self.running = False
27
- if self.chain_interface.chain.rpc:
27
+ if self.chain_interface.current_rpc:
28
28
  try:
29
29
  self.last_checked_block = self.web3.eth.block_number
30
30
  except Exception:
@@ -39,7 +39,7 @@ class EventMonitor:
39
39
  f"Starting EventMonitor for {len(self.addresses)} addresses on {self.chain_interface.chain.name}"
40
40
  )
41
41
 
42
- if not self.chain_interface.chain.rpc:
42
+ if not self.chain_interface.current_rpc:
43
43
  logger.error(
44
44
  f"Cannot start EventMonitor: No RPC URL found for chain {self.chain_interface.chain.name}"
45
45
  )
iwa/core/services/safe.py CHANGED
@@ -102,7 +102,7 @@ class SafeService:
102
102
 
103
103
  # Use ChainInterface which has proper RPC rotation and parsing
104
104
  chain_interface = ChainInterfaces().get(chain_name)
105
- return EthereumClient(chain_interface.chain.rpc)
105
+ return EthereumClient(chain_interface.current_rpc)
106
106
 
107
107
  def _deploy_safe_contract(
108
108
  self,
@@ -254,7 +254,7 @@ class SafeService:
254
254
 
255
255
  # Use ChainInterface which has proper RPC rotation and parsing
256
256
  chain_interface = ChainInterfaces().get(chain)
257
- ethereum_client = EthereumClient(chain_interface.chain.rpc)
257
+ ethereum_client = EthereumClient(chain_interface.current_rpc)
258
258
 
259
259
  code = ethereum_client.w3.eth.get_code(account.address)
260
260
 
@@ -30,7 +30,7 @@ class SafeMultisig:
30
30
  from iwa.core.chain import ChainInterfaces
31
31
 
32
32
  chain_interface = ChainInterfaces().get(chain_name.lower())
33
- ethereum_client = EthereumClient(chain_interface.chain.rpc)
33
+ ethereum_client = EthereumClient(chain_interface.current_rpc)
34
34
  self.multisig = Safe(safe_account.address, ethereum_client)
35
35
  self.ethereum_client = ethereum_client
36
36
 
@@ -42,7 +42,7 @@ def test_init(safe_account, mock_settings, mock_safe_eth):
42
42
  """Test initialization."""
43
43
  with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
44
44
  mock_ci = mock_ci_cls.return_value
45
- mock_ci.get.return_value.chain.rpc = "http://rpc"
45
+ mock_ci.get.return_value.current_rpc = "http://rpc"
46
46
  ms = SafeMultisig(safe_account, "gnosis")
47
47
  assert ms.multisig is not None
48
48
  mock_safe_eth[0].assert_called_with("http://rpc") # EthereumClient init
@@ -17,6 +17,28 @@ from loguru import logger
17
17
  from iwa.core.keys import EncryptedAccount, KeyStorage
18
18
  from iwa.core.models import Config, StoredSafeAccount
19
19
 
20
+ # Known mappings from olas-operate-middleware staking programs
21
+ # See: https://github.com/valory-xyz/olas-operate-middleware/blob/main/operate/ledger/profiles.py
22
+ STAKING_PROGRAM_MAP = {
23
+ # Pearl staking programs (gnosis) - operate format
24
+ "pearl_alpha": "0x5344B7DD311e5d3DdDd46A4f71481Bd7b05AAA3e", # Expert Legacy
25
+ "pearl_beta": "0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C", # Hobbyist 1 Legacy
26
+ "pearl_beta_2": "0xE56dF1E563De1B10715cB313D514af350D207212", # Expert 5 Legacy
27
+ "pearl_beta_3": "0xD7A3C8b975f71030135f1a66E9e23164d54fF455", # Expert 7 Legacy
28
+ "pearl_beta_4": "0x17dBAe44BC5618Cc254055B386A29576b4F87015", # Expert 9 Legacy
29
+ "pearl_beta_5": "0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f", # Expert 10 Legacy
30
+ "pearl_beta_mm_v2_1": "0x75eeca6207be98cac3fde8a20ecd7b01e50b3472", # Expert 3 MM v2
31
+ "pearl_beta_mm_v2_2": "0x9c7f6103e3a72e4d1805b9c683ea5b370ec1a99f", # Expert 4 MM v2
32
+ "pearl_beta_mm_v2_3": "0xcdC603e0Ee55Aae92519f9770f214b2Be4967f7d", # Expert 5 MM v2
33
+ # Quickstart staking programs (gnosis) - quickstart format
34
+ "quickstart_beta_expert_4": "0xaD9d891134443B443D7F30013c7e14Fe27F2E029", # Expert 4 Legacy
35
+ "quickstart_beta_expert_7": "0xD7A3C8b975f71030135f1a66E9e23164d54fF455", # Expert 7 Legacy
36
+ "quickstart_beta_expert_9": "0x17dBAe44BC5618Cc254055B386A29576b4F87015", # Expert 9 Legacy
37
+ "quickstart_beta_expert_11": "0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7", # Expert 11 Legacy
38
+ "quickstart_beta_expert_16_mech_marketplace": "0x6c65430515c70a3f5E62107CC301685B7D46f991", # Expert 16 MM v1
39
+ "quickstart_beta_expert_18_mech_marketplace": "0x041e679d04Fc0D4f75Eb937Dea729Df09a58e454", # Expert 18 MM v1
40
+ }
41
+
20
42
 
21
43
  @dataclass
22
44
  class DiscoveredKey:
@@ -26,8 +48,10 @@ class DiscoveredKey:
26
48
  private_key: Optional[str] = None # Plaintext hex (None if still encrypted)
27
49
  encrypted_keystore: Optional[dict] = None # Web3 v3 keystore format
28
50
  source_file: Path = field(default_factory=Path)
29
- role: str = "unknown" # "agent", "operator", "owner"
51
+ role: str = "unknown" # "agent", "owner"
30
52
  is_encrypted: bool = False
53
+ signature_verified: bool = False
54
+ signature_failed: bool = False
31
55
 
32
56
  @property
33
57
  def is_decrypted(self) -> bool:
@@ -55,6 +79,9 @@ class DiscoveredService:
55
79
  source_folder: Path = field(default_factory=Path)
56
80
  format: str = "unknown" # "trader_runner" or "operate"
57
81
  service_name: Optional[str] = None
82
+ # New fields for full service import
83
+ staking_contract_address: Optional[str] = None
84
+ service_owner_address: Optional[str] = None
58
85
 
59
86
  @property
60
87
  def agent_key(self) -> Optional[DiscoveredKey]:
@@ -66,9 +93,14 @@ class DiscoveredService:
66
93
 
67
94
  @property
68
95
  def operator_key(self) -> Optional[DiscoveredKey]:
69
- """Get the operator key if present."""
96
+ """Get the operator (owner) key. Alias for compatibility."""
97
+ return self.owner_key
98
+
99
+ @property
100
+ def owner_key(self) -> Optional[DiscoveredKey]:
101
+ """Get the owner key if present (matches 'owner' or 'operator' roles)."""
70
102
  for key in self.keys:
71
- if key.role in ("operator", "owner"):
103
+ if key.role in ["owner", "operator"]:
72
104
  return key
73
105
  return None
74
106
 
@@ -89,15 +121,17 @@ class ImportResult:
89
121
  class OlasServiceImporter:
90
122
  """Discover and import Olas services from external directories."""
91
123
 
92
- def __init__(self, key_storage: Optional[KeyStorage] = None):
124
+ def __init__(self, key_storage: Optional[KeyStorage] = None, password: Optional[str] = None):
93
125
  """Initialize the importer.
94
126
 
95
127
  Args:
96
128
  key_storage: KeyStorage instance. If None, will create one.
129
+ password: Optional password to decrypt discovered keystores.
97
130
 
98
131
  """
99
132
  self.key_storage = key_storage or KeyStorage()
100
133
  self.config = Config()
134
+ self.password = password
101
135
 
102
136
  def scan_directory(self, path: Path) -> List[DiscoveredService]:
103
137
  """Recursively scan a directory for Olas services.
@@ -106,7 +140,7 @@ class OlasServiceImporter:
106
140
  path: Directory to scan.
107
141
 
108
142
  Returns:
109
- List of discovered services.
143
+ List of discovered services (deduplicated by chain:service_id).
110
144
 
111
145
  """
112
146
  path = Path(path)
@@ -129,8 +163,51 @@ class OlasServiceImporter:
129
163
  services = self._parse_operate_format(operate)
130
164
  discovered.extend(services)
131
165
 
132
- logger.info(f"Discovered {len(discovered)} Olas service(s)")
133
- return discovered
166
+ return self._deduplicate_services(discovered)
167
+
168
+ def _deduplicate_services(self, services: List[DiscoveredService]) -> List[DiscoveredService]:
169
+ """Deduplicate discovered services by chain:service_id."""
170
+ seen_keys: set = set()
171
+ unique_services = []
172
+ duplicates = 0
173
+ for service in services:
174
+ if service.service_id:
175
+ key = f"{service.chain_name}:{service.service_id}"
176
+ if key in seen_keys:
177
+ logger.debug(
178
+ f"Skipping duplicate service {key} from {service.source_folder}"
179
+ )
180
+ duplicates += 1
181
+ continue
182
+ seen_keys.add(key)
183
+ unique_services.append(service)
184
+
185
+ if duplicates:
186
+ logger.info(f"Skipped {duplicates} duplicate service(s)")
187
+ logger.info(f"Discovered {len(unique_services)} unique Olas service(s)")
188
+ return unique_services
189
+
190
+ def _find_trader_name(self, folder: Path) -> str:
191
+ """Find the trader name by traversing up the directory tree.
192
+
193
+ Handles quickstart format where the .operate folder is nested inside
194
+ a quickstart folder, e.g.: trader_altair/quickstart/.operate/
195
+
196
+ Returns the first folder name starting with 'trader_' or the
197
+ immediate folder name if none found.
198
+ """
199
+ current = folder
200
+ fallback = folder.name
201
+
202
+ # Traverse up looking for trader_* folder
203
+ for _ in range(5): # Max 5 levels up
204
+ if current.name.startswith("trader_"):
205
+ return current.name
206
+ current = current.parent
207
+ if current == current.parent: # Reached root
208
+ break
209
+
210
+ return fallback
134
211
 
135
212
  def _parse_trader_runner_format(self, folder: Path) -> Optional[DiscoveredService]:
136
213
  """Parse a .trader_runner folder.
@@ -153,6 +230,9 @@ class OlasServiceImporter:
153
230
  service.safe_address = self._extract_safe_address(folder)
154
231
  service.keys = self._extract_trader_keys(folder)
155
232
 
233
+ # Extract staking program from .env
234
+ self._extract_staking_from_env(service, folder)
235
+
156
236
  if not service.keys and not service.service_id:
157
237
  logger.debug(f"No valid data found in {folder}")
158
238
  return None
@@ -187,12 +267,13 @@ class OlasServiceImporter:
187
267
  if key:
188
268
  keys.append(key)
189
269
 
190
- # Parse operator_pkey.txt
270
+ # Parse operator_pkey.txt (contains owner key)
191
271
  operator_file = folder / "operator_pkey.txt"
192
272
  if operator_file.exists():
193
- key = self._parse_keystore_file(operator_file, role="operator")
273
+ key = self._parse_keystore_file(operator_file, role="owner")
194
274
  if key:
195
275
  keys.append(key)
276
+ self._verify_key_signature(key)
196
277
 
197
278
  # Also check keys.json (array of keystores)
198
279
  keys_file = folder / "keys.json"
@@ -205,6 +286,31 @@ class OlasServiceImporter:
205
286
  keys.append(key)
206
287
  return keys
207
288
 
289
+ def _extract_staking_from_env(self, service: DiscoveredService, folder: Path) -> None:
290
+ """Extract STAKING_PROGRAM from .env file in trader_runner folder."""
291
+ # Check parent folder for .env (usually alongside .trader_runner)
292
+ env_file = folder.parent / ".env"
293
+ if not env_file.exists():
294
+ # Also check inside the folder itself
295
+ env_file = folder / ".env"
296
+ if not env_file.exists():
297
+ return
298
+
299
+ try:
300
+ content = env_file.read_text()
301
+ for line in content.splitlines():
302
+ line = line.strip()
303
+ if line.startswith("STAKING_PROGRAM="):
304
+ program_id = line.split("=", 1)[1].strip().strip('"').strip("'")
305
+ if program_id:
306
+ service.staking_contract_address = self._resolve_staking_contract(
307
+ program_id, service.chain_name
308
+ )
309
+ logger.debug(f"Found STAKING_PROGRAM={program_id} in {env_file}")
310
+ break
311
+ except IOError as e:
312
+ logger.warning(f"Failed to read {env_file}: {e}")
313
+
208
314
  def _parse_operate_format(self, folder: Path) -> List[DiscoveredService]:
209
315
  """Parse a .operate folder.
210
316
 
@@ -288,12 +394,15 @@ class OlasServiceImporter:
288
394
 
289
395
  # Use the folder name containing .operate (e.g., "trader_xi")
290
396
  operate_folder = config_file.parent.parent.parent # services/<uuid> -> .operate
291
- parent_folder = operate_folder.parent # .operate -> trader_xi
397
+ parent_folder = operate_folder.parent # .operate -> trader_xi or quickstart
398
+
399
+ # Handle quickstart format: traverse up to find trader_* folder
400
+ service_name = self._find_trader_name(parent_folder)
292
401
 
293
402
  service = DiscoveredService(
294
403
  source_folder=config_file.parent,
295
404
  format="operate",
296
- service_name=parent_folder.name,
405
+ service_name=service_name,
297
406
  )
298
407
 
299
408
  # 1. Extract keys from config
@@ -311,6 +420,9 @@ class OlasServiceImporter:
311
420
  external_keys = self._extract_external_keys_folder(operate_folder)
312
421
  self._merge_unique_keys(service, external_keys)
313
422
 
423
+ # 5. Extract owner address from wallets folder
424
+ self._extract_owner_address(service, operate_folder)
425
+
314
426
  return service
315
427
 
316
428
  def _extract_keys_from_operate_config(
@@ -325,19 +437,19 @@ class OlasServiceImporter:
325
437
  # Remove 0x prefix if present
326
438
  if private_key.startswith("0x"):
327
439
  private_key = private_key[2:]
328
- keys.append(
329
- DiscoveredKey(
330
- address=key_data["address"],
331
- private_key=private_key,
332
- role="agent",
333
- source_file=config_file,
334
- is_encrypted=False,
335
- )
440
+ key = DiscoveredKey(
441
+ address=key_data["address"],
442
+ private_key=private_key,
443
+ role="agent",
444
+ source_file=config_file,
445
+ is_encrypted=False,
336
446
  )
447
+ self._verify_key_signature(key)
448
+ keys.append(key)
337
449
  return keys
338
450
 
339
451
  def _enrich_service_with_chain_info(self, service: DiscoveredService, data: dict) -> None:
340
- """Extract service ID and Safe address from chain configs."""
452
+ """Extract service ID, Safe address, and staking contract from chain configs."""
341
453
  chain_configs = data.get("chain_configs", {})
342
454
  for chain_name, chain_config in chain_configs.items():
343
455
  chain_data = chain_config.get("chain_data", {})
@@ -350,6 +462,25 @@ class OlasServiceImporter:
350
462
  if "multisig" in chain_data:
351
463
  service.safe_address = chain_data["multisig"]
352
464
 
465
+ # Extract staking contract from user_params
466
+ user_params = chain_data.get("user_params", {})
467
+ staking_program_id = user_params.get("staking_program_id")
468
+ if staking_program_id:
469
+ service.staking_contract_address = self._resolve_staking_contract(
470
+ staking_program_id, chain_name
471
+ )
472
+
473
+ def _resolve_staking_contract(
474
+ self, staking_program_id: str, chain_name: str
475
+ ) -> Optional[str]:
476
+ """Resolve a staking program ID to a contract address."""
477
+ address = STAKING_PROGRAM_MAP.get(staking_program_id)
478
+ if address:
479
+ logger.debug(f"Resolved staking program '{staking_program_id}' -> {address}")
480
+ else:
481
+ logger.warning(f"Unknown staking program ID: {staking_program_id}")
482
+ return address
483
+
353
484
  def _extract_parent_wallet_keys(self, operate_folder: Path) -> List[DiscoveredKey]:
354
485
  """Extract owner keys from parent wallets folder."""
355
486
  keys = []
@@ -357,7 +488,12 @@ class OlasServiceImporter:
357
488
  if wallets_folder.exists():
358
489
  eth_txt = wallets_folder / "ethereum.txt"
359
490
  if eth_txt.exists():
491
+ # Try plaintext first
360
492
  key = self._parse_plaintext_key_file(eth_txt, role="owner")
493
+ if not key:
494
+ # Fallback to keystore
495
+ key = self._parse_keystore_file(eth_txt, role="owner")
496
+
361
497
  if key:
362
498
  keys.append(key)
363
499
  return keys
@@ -374,6 +510,22 @@ class OlasServiceImporter:
374
510
  keys.append(key)
375
511
  return keys
376
512
 
513
+ def _extract_owner_address(self, service: DiscoveredService, operate_folder: Path) -> None:
514
+ """Extract owner address from wallets/ethereum.json."""
515
+ wallets_folder = operate_folder / "wallets"
516
+ if not wallets_folder.exists():
517
+ return
518
+
519
+ eth_json = wallets_folder / "ethereum.json"
520
+ if eth_json.exists():
521
+ try:
522
+ data = json.loads(eth_json.read_text())
523
+ if "address" in data:
524
+ service.service_owner_address = data["address"]
525
+ logger.debug(f"Extracted owner address: {service.service_owner_address}")
526
+ except (json.JSONDecodeError, IOError) as e:
527
+ logger.warning(f"Failed to parse {eth_json}: {e}")
528
+
377
529
  def _merge_unique_keys(self, service: DiscoveredService, new_keys: List[DiscoveredKey]):
378
530
  """Merge new keys into service avoiding duplicates by address."""
379
531
  existing_addrs = {k.address.lower() for k in service.keys}
@@ -398,13 +550,21 @@ class OlasServiceImporter:
398
550
  if not address.startswith("0x"):
399
551
  address = "0x" + address
400
552
 
401
- return DiscoveredKey(
553
+ key = DiscoveredKey(
402
554
  address=address,
403
555
  encrypted_keystore=keystore,
404
556
  role=role,
405
557
  source_file=file_path,
406
558
  is_encrypted=True,
407
559
  )
560
+
561
+ # Attempt decryption if password provided
562
+ if self.password:
563
+ self._attempt_decryption(key)
564
+ if key.private_key:
565
+ self._verify_key_signature(key)
566
+
567
+ return key
408
568
  except (json.JSONDecodeError, IOError) as e:
409
569
  logger.warning(f"Failed to parse keystore {file_path}: {e}")
410
570
  return None
@@ -422,15 +582,19 @@ class OlasServiceImporter:
422
582
  address = keystore.get("address", "")
423
583
  if not address.startswith("0x"):
424
584
  address = "0x" + address
425
- keys.append(
426
- DiscoveredKey(
427
- address=address,
428
- encrypted_keystore=keystore,
429
- role="agent",
430
- source_file=file_path,
431
- is_encrypted=True,
432
- )
585
+ key = DiscoveredKey(
586
+ address=address,
587
+ encrypted_keystore=keystore,
588
+ role="agent",
589
+ source_file=file_path,
590
+ is_encrypted=True,
433
591
  )
592
+ # Attempt decryption if password provided
593
+ if self.password:
594
+ self._attempt_decryption(key)
595
+ if key.private_key:
596
+ self._verify_key_signature(key)
597
+ keys.append(key)
434
598
  return keys
435
599
  except (json.JSONDecodeError, IOError):
436
600
  return []
@@ -446,13 +610,15 @@ class OlasServiceImporter:
446
610
  try:
447
611
  data = json.loads(content)
448
612
  if isinstance(data, dict) and "private_key" in data and "address" in data:
449
- return DiscoveredKey(
613
+ key = DiscoveredKey(
450
614
  address=data["address"],
451
615
  private_key=data["private_key"],
452
616
  role=role,
453
617
  source_file=file_path,
454
618
  is_encrypted=False,
455
619
  )
620
+ self._verify_key_signature(key)
621
+ return key
456
622
  except json.JSONDecodeError:
457
623
  pass
458
624
 
@@ -460,13 +626,15 @@ class OlasServiceImporter:
460
626
  if len(content) == 64 or (len(content) == 66 and content.startswith("0x")):
461
627
  private_key = content[2:] if content.startswith("0x") else content
462
628
  account = Account.from_key(bytes.fromhex(private_key))
463
- return DiscoveredKey(
629
+ key = DiscoveredKey(
464
630
  address=account.address,
465
631
  private_key=private_key,
466
632
  role=role,
467
633
  source_file=file_path,
468
634
  is_encrypted=False,
469
635
  )
636
+ self._verify_key_signature(key)
637
+ return key
470
638
 
471
639
  return None
472
640
  except Exception as e:
@@ -698,6 +866,7 @@ class OlasServiceImporter:
698
866
  def _import_service_config(self, service: DiscoveredService) -> Tuple[bool, str]:
699
867
  """Import service config to OlasConfig."""
700
868
  try:
869
+ from iwa.plugins.olas.constants import OLAS_TOKEN_ADDRESS_GNOSIS
701
870
  from iwa.plugins.olas.models import OlasConfig, Service
702
871
 
703
872
  # Get or create OlasConfig
@@ -711,13 +880,16 @@ class OlasServiceImporter:
711
880
  if key in olas_config.services:
712
881
  return False, "duplicate"
713
882
 
714
- # Create service model
883
+ # Create service model with all fields
715
884
  olas_service = Service(
716
885
  service_name=service.service_name or f"service_{service.service_id}",
717
886
  chain_name=service.chain_name,
718
887
  service_id=service.service_id,
719
- agent_ids=[], # Would need on-chain query
888
+ agent_ids=[25], # Trader agents always use agent ID 25
720
889
  multisig_address=service.safe_address,
890
+ service_owner_address=service.service_owner_address,
891
+ staking_contract_address=service.staking_contract_address,
892
+ token_address=str(OLAS_TOKEN_ADDRESS_GNOSIS),
721
893
  )
722
894
 
723
895
  # Set agent address if we have one
@@ -734,3 +906,59 @@ class OlasServiceImporter:
734
906
  return False, "Olas plugin not available"
735
907
  except Exception as e:
736
908
  return False, str(e)
909
+
910
+ def _attempt_decryption(self, key: DiscoveredKey) -> None:
911
+ """Attempt to decrypt an encrypted keystore using the provided password."""
912
+ if not self.password or not key.encrypted_keystore:
913
+ return
914
+
915
+ try:
916
+ logger.debug(f"Attempting decryption for {key.address}")
917
+
918
+ # Use Account.decrypt to handle standard web3 keystores
919
+ private_key_bytes = Account.decrypt(key.encrypted_keystore, self.password)
920
+ key.private_key = private_key_bytes.hex()
921
+ key.is_encrypted = False
922
+ # If we successfully decrypted, it's no longer "encrypted" for verification purposes
923
+ logger.debug(f"Successfully decrypted key for {key.address}")
924
+ except ValueError as e:
925
+ # Password incorrect
926
+ logger.warning(f"Decryption failed (ValueError) for {key.address}: {e}")
927
+ pass
928
+ except Exception as e:
929
+ logger.warning(f"Error decrypting key {key.address}: {type(e).__name__} - {e}")
930
+
931
+ def _verify_key_signature(self, key: DiscoveredKey) -> None:
932
+ """Verify that the plaintext private key can sign a message and recover the address."""
933
+ if not key.private_key or not key.address:
934
+ return
935
+
936
+ try:
937
+ from eth_account.messages import encode_defunct
938
+
939
+ message = "Hello, world!"
940
+ encoded_message = encode_defunct(text=message)
941
+ signed_message = Account.sign_message(encoded_message, private_key=key.private_key)
942
+ recovered_address = Account.recover_message(
943
+ encoded_message, signature=signed_message.signature
944
+ )
945
+
946
+ # Normalize address to lowercase with 0x prefix
947
+ key_addr = key.address.lower()
948
+ if not key_addr.startswith("0x"):
949
+ key_addr = "0x" + key_addr
950
+ recovered_addr = recovered_address.lower()
951
+
952
+ if recovered_addr == key_addr:
953
+ key.signature_verified = True
954
+ logger.debug(f"Signature verified for key {key.address}")
955
+ else:
956
+ key.signature_failed = True
957
+ logger.warning(
958
+ f"Signature verification FAILED for key {key.address}. "
959
+ f"Recovered: {recovered_address}"
960
+ )
961
+ except Exception as e:
962
+ key.signature_failed = True
963
+ logger.warning(f"Error verifying signature for key {key.address}: {e}")
964
+
@@ -1,10 +1,11 @@
1
1
  """Olas plugin."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Dict, Optional, Type
4
+ from typing import Dict, List, Optional, Tuple, Type
5
5
 
6
6
  import typer
7
7
  from pydantic import BaseModel
8
+ from rich.console import Console
8
9
 
9
10
  from iwa.core.plugins import Plugin
10
11
  from iwa.core.wallet import Wallet
@@ -73,12 +74,12 @@ class OlasPlugin(Plugin):
73
74
 
74
75
  try:
75
76
  chain_interface = ChainInterfaces().get(chain_name)
76
- if not chain_interface.chain.rpcs:
77
+ if not chain_interface.current_rpc:
77
78
  return None, None
78
79
  except ValueError:
79
80
  return None, None # Chain not supported/configured
80
81
 
81
- ethereum_client = EthereumClient(chain_interface.chain.rpc)
82
+ ethereum_client = EthereumClient(chain_interface.current_rpc)
82
83
  safe = Safe(safe_address, ethereum_client)
83
84
  owners = safe.retrieve_owners()
84
85
  return owners, True
@@ -86,7 +87,18 @@ class OlasPlugin(Plugin):
86
87
  # Query failed - Safe likely doesn't exist
87
88
  return [], False
88
89
 
89
- def _display_service_table(self, console, service, index: int) -> None:
90
+ def _resolve_staking_name(self, address: str, chain_name: str) -> str | None:
91
+ """Resolve staking contract address to human-readable name."""
92
+ from iwa.plugins.olas.constants import OLAS_TRADER_STAKING_CONTRACTS
93
+
94
+ chain_contracts = OLAS_TRADER_STAKING_CONTRACTS.get(chain_name, {})
95
+ addr_lower = address.lower()
96
+ for name, contract_addr in chain_contracts.items():
97
+ if str(contract_addr).lower() == addr_lower:
98
+ return name
99
+ return None
100
+
101
+ def _display_service_table(self, console: Console, service, index: int) -> None:
90
102
  """Display a single discovered service as a Rich table."""
91
103
  from rich.table import Table
92
104
 
@@ -98,46 +110,102 @@ class OlasPlugin(Plugin):
98
110
 
99
111
  table.add_row("Format", service.format)
100
112
  table.add_row("Source", str(service.source_folder))
101
- table.add_row("Service ID", str(service.service_id) if service.service_id else "N/A")
113
+ table.add_row(
114
+ "Service ID",
115
+ str(service.service_id) if service.service_id else "[red]Not detected[/red]",
116
+ )
102
117
  table.add_row("Chain", service.chain_name)
103
118
 
104
119
  # Verify Safe and display
120
+ on_chain_signers, safe_exists = self._add_safe_info(table, service)
121
+
122
+ # Display staking contract info
123
+ self._add_staking_info(table, service)
124
+
125
+ # Display owner
126
+ self._add_owner_info(table, service)
127
+
128
+ # Display agent key
129
+ self._add_agent_info(table, service, on_chain_signers, safe_exists)
130
+
131
+ console.print(table)
132
+ console.print()
133
+
134
+ def _add_safe_info(self, table, service) -> Tuple[Optional[List[str]], Optional[bool]]:
135
+ """Add Safe information to the display table."""
105
136
  on_chain_signers, safe_exists = None, None
106
137
  if service.safe_address:
107
138
  on_chain_signers, safe_exists = self._get_safe_signers(
108
139
  service.safe_address, service.chain_name
109
140
  )
110
- if safe_exists is None:
111
- table.add_row("Safe", service.safe_address)
112
- elif safe_exists:
113
- table.add_row("Safe", f"{service.safe_address} [green]✓[/green]")
114
- else:
115
- table.add_row(
116
- "Safe",
117
- f"[bold red]⚠ {service.safe_address} - DOES NOT EXIST ON-CHAIN![/bold red]",
141
+ safe_text = service.safe_address
142
+ if safe_exists:
143
+ safe_text += " [green]✓[/green]"
144
+ elif safe_exists is False:
145
+ safe_text = (
146
+ f"[bold red]⚠ {service.safe_address} - DOES NOT EXIST ON-CHAIN![/bold red]"
118
147
  )
148
+ table.add_row("Multisig", safe_text)
119
149
  else:
120
- table.add_row("Safe", "N/A")
121
-
122
- # Display keys with signer verification
123
- for key in service.keys:
124
- status = "🔒 encrypted" if key.is_encrypted else "🔓 plaintext"
125
- key_info = f"{key.address} {status}"
126
-
127
- if key.role == "agent" and service.safe_address:
128
- if not safe_exists:
129
- key_info = f"[bold red]⚠ {key.address} - NOT A SIGNER OF THE SAFE![/bold red]"
150
+ table.add_row("Multisig", "[red]Not detected[/red]")
151
+ return on_chain_signers, safe_exists
152
+
153
+ def _add_staking_info(self, table, service) -> None:
154
+ """Add staking information to the display table."""
155
+ if service.staking_contract_address:
156
+ staking_name = self._resolve_staking_name(
157
+ service.staking_contract_address, service.chain_name
158
+ )
159
+ val = staking_name if staking_name else "[red]Unknown[/red]"
160
+ table.add_row("Staking", val)
161
+ table.add_row("Staking Addr", service.staking_contract_address)
162
+ else:
163
+ table.add_row("Staking", "[red]Not detected[/red]")
164
+ table.add_row("Staking Addr", "[red]Not detected[/red]")
165
+
166
+ def _add_owner_info(self, table, service) -> None:
167
+ """Add owner information to the display table."""
168
+ owner_key = next((k for k in service.keys if k.role == "owner"), None)
169
+ owner_addr = service.service_owner_address
170
+ if not owner_addr and owner_key:
171
+ owner_addr = owner_key.address
172
+
173
+ if owner_addr:
174
+ val = owner_addr
175
+ if owner_key:
176
+ if owner_key.signature_verified:
177
+ val = f"[green]{owner_addr}[/green]"
178
+ elif not owner_key.is_encrypted:
179
+ val = f"[red]{owner_addr}[/red]"
180
+ status = "🔒 encrypted" if owner_key.is_encrypted else "🔓 plaintext"
181
+ table.add_row("Owner", f"{val} {status}")
182
+ else:
183
+ table.add_row("Owner", val)
184
+ else:
185
+ table.add_row("Owner", "[red]Not detected[/red]")
186
+
187
+ def _add_agent_info(self, table, service, on_chain_signers, safe_exists) -> None:
188
+ """Add agent information to the display table."""
189
+ agent_key = next((k for k in service.keys if k.role == "agent"), None)
190
+ if agent_key:
191
+ status = "🔒 encrypted" if agent_key.is_encrypted else "🔓 plaintext"
192
+ addr_val = agent_key.address
193
+ if agent_key.signature_verified:
194
+ addr_val = f"[green]{agent_key.address}[/green]"
195
+ elif not agent_key.is_encrypted:
196
+ addr_val = f"[red]{agent_key.address}[/red]"
197
+
198
+ key_info = f"{addr_val} {status}"
199
+ if service.safe_address:
200
+ if safe_exists is False:
201
+ key_info = f"[bold red]⚠ {agent_key.address} - NOT A SIGNER![/bold red]"
130
202
  elif on_chain_signers is not None:
131
- is_signer = key.address.lower() in [s.lower() for s in on_chain_signers]
203
+ is_signer = agent_key.address.lower() in [s.lower() for s in on_chain_signers]
132
204
  if not is_signer:
133
- key_info = (
134
- f"[bold red]⚠ {key.address} - NOT A SIGNER OF THE SAFE![/bold red]"
135
- )
136
-
137
- table.add_row(f"Key ({key.role})", key_info)
138
-
139
- console.print(table)
140
- console.print()
205
+ key_info = f"[bold red]⚠ {agent_key.address} - NOT A SIGNER![/bold red]"
206
+ table.add_row("Agent", key_info)
207
+ else:
208
+ table.add_row("Agent", "[red]Not detected[/red]")
141
209
 
142
210
  def _import_and_print_results(self, console, importer, discovered, password) -> tuple:
143
211
  """Import all discovered services and print results."""
@@ -194,15 +262,18 @@ class OlasPlugin(Plugin):
194
262
  ),
195
263
  ):
196
264
  """Import Olas services and keys from external directories."""
197
- from rich.console import Console
198
-
199
265
  from iwa.plugins.olas.importer import OlasServiceImporter
200
266
 
201
267
  console = Console()
202
268
 
203
269
  # Scan directory
204
270
  console.print(f"\n[bold]Scanning[/bold] {path}...")
205
- importer = OlasServiceImporter()
271
+
272
+ # Ask for password in dry-run to allow signature verification of encrypted keys
273
+ if dry_run and not password:
274
+ password = self._prompt_dry_run_password()
275
+
276
+ importer = OlasServiceImporter(password=password)
206
277
  discovered = importer.scan_directory(Path(path))
207
278
 
208
279
  if not discovered:
@@ -219,11 +290,9 @@ class OlasPlugin(Plugin):
219
290
  raise typer.Exit(code=0)
220
291
 
221
292
  # Confirm import
222
- if not yes:
223
- confirm = typer.confirm("Import these services?")
224
- if not confirm:
225
- console.print("[yellow]Aborted.[/yellow]")
226
- raise typer.Exit(code=0)
293
+ if not yes and not typer.confirm("Import these services?"):
294
+ console.print("[yellow]Aborted.[/yellow]")
295
+ raise typer.Exit(code=0)
227
296
 
228
297
  # Check if we need a password for encrypted keys
229
298
  needs_password = any(key.is_encrypted for service in discovered for key in service.keys)
@@ -234,11 +303,28 @@ class OlasPlugin(Plugin):
234
303
  password = typer.prompt("Password", hide_input=True)
235
304
 
236
305
  # Import services
237
- total_keys, total_safes, total_services, all_skipped, all_errors = (
238
- self._import_and_print_results(console, importer, discovered, password)
306
+ results = self._import_and_print_results(console, importer, discovered, password)
307
+ self._print_import_summary(console, *results)
308
+
309
+ def _prompt_dry_run_password(self) -> Optional[str]:
310
+ """Prompt for password during dry-run."""
311
+ pwd = typer.prompt(
312
+ "Enter wallet password to verify encrypted keys (optional, press Enter to skip)",
313
+ hide_input=True,
314
+ default="",
239
315
  )
316
+ return pwd if pwd else None
240
317
 
241
- # Summary
318
+ def _print_import_summary(
319
+ self,
320
+ console: Console,
321
+ total_keys: int,
322
+ total_safes: int,
323
+ total_services: int,
324
+ all_skipped: List[str],
325
+ all_errors: List[str],
326
+ ) -> None:
327
+ """Print import summary."""
242
328
  console.print("\n[bold]Summary:[/bold]")
243
329
  console.print(f" Keys imported: {total_keys}")
244
330
  console.print(f" Safes imported: {total_safes}")
@@ -161,7 +161,7 @@ def test_parse_trader_runner_keys(importer, tmp_path):
161
161
  assert service.safe_address == "0xSafeAddress"
162
162
  assert len(service.keys) == 2
163
163
  assert any(k.role == "agent" for k in service.keys)
164
- assert any(k.role == "operator" for k in service.keys)
164
+ assert any(k.role == "owner" for k in service.keys)
165
165
 
166
166
 
167
167
  def test_parse_trader_runner_invalid_id(importer, tmp_path):
@@ -68,7 +68,7 @@ def test_import_services_cli_scan_only(plugin, runner):
68
68
  ]
69
69
 
70
70
  # Test dry-run
71
- result = runner.invoke(app, ["/tmp/test", "--dry-run"])
71
+ result = runner.invoke(app, ["/tmp/test", "--dry-run"], input="\n")
72
72
  assert result.exit_code == 0
73
73
  assert "Found 1 service(s)" in result.output
74
74
  assert "Dry run mode" in result.output
@@ -105,7 +105,7 @@ def test_get_safe_signers_edge_cases(plugin):
105
105
  # 1. No RPC configured
106
106
  with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
107
107
  mock_ci = mock_ci_cls.return_value
108
- mock_ci.get.return_value.chain.rpcs = []
108
+ mock_ci.get.return_value.current_rpc = ""
109
109
  signers, exists = plugin._get_safe_signers("0x1", "gnosis")
110
110
  assert signers is None
111
111
  assert exists is None
@@ -113,8 +113,7 @@ def test_get_safe_signers_edge_cases(plugin):
113
113
  # 2. Safe doesn't exist (raises exception)
114
114
  with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
115
115
  mock_ci = mock_ci_cls.return_value
116
- mock_ci.get.return_value.chain.rpcs = ["http://rpc"]
117
- mock_ci.get.return_value.chain.rpc = "http://rpc"
116
+ mock_ci.get.return_value.current_rpc = "http://rpc"
118
117
  with patch("safe_eth.eth.EthereumClient"), patch("safe_eth.safe.Safe") as mock_safe_cls:
119
118
  mock_safe = mock_safe_cls.return_value
120
119
  mock_safe.retrieve_owners.side_effect = Exception("Generic error")
@@ -126,8 +125,7 @@ def test_get_safe_signers_edge_cases(plugin):
126
125
  # 3. Success path
127
126
  with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
128
127
  mock_ci = mock_ci_cls.return_value
129
- mock_ci.get.return_value.chain.rpcs = ["http://rpc"]
130
- mock_ci.get.return_value.chain.rpc = "http://rpc"
128
+ mock_ci.get.return_value.current_rpc = "http://rpc"
131
129
  with patch("safe_eth.eth.EthereumClient"), patch("safe_eth.safe.Safe") as mock_safe_cls:
132
130
  mock_safe = mock_safe_cls.return_value
133
131
  mock_safe.retrieve_owners.return_value = ["0xAgent"]
@@ -187,15 +185,15 @@ def test_import_services_cli_complex_display(plugin, runner):
187
185
  # Mock Safe exists with Agent as signer
188
186
  mock_get_signers.return_value = (["0xAgent"], True)
189
187
 
190
- result = runner.invoke(app, ["/tmp/test", "--dry-run"])
188
+ result = runner.invoke(app, ["/tmp/test", "--dry-run"], input="\n")
191
189
  assert "0xSafe" in result.output
192
190
  assert "✓" in result.output
193
191
  assert "0xAgent 🔓 plaintext" in result.output # Not a warning
194
192
 
195
193
  # 2. Service where agent is NOT a signer
196
194
  mock_get_signers.return_value = (["0xOther"], True)
197
- result = runner.invoke(app, ["/tmp/test", "--dry-run"])
198
- assert "NOT A SIGNER OF THE SAFE" in result.output
195
+ result = runner.invoke(app, ["/tmp/test", "--dry-run"], input="\n")
196
+ assert "NOT A SIGNER!" in result.output
199
197
 
200
198
 
201
199
  def test_import_services_cli_password_prompt(plugin, runner):
iwa/tui/rpc.py CHANGED
@@ -38,7 +38,7 @@ class RPCView(Static):
38
38
  results.append((chain_name, "N/A", "Not Configured", "-"))
39
39
  continue
40
40
 
41
- rpc_url = interface.chain.rpc
41
+ rpc_url = interface.current_rpc
42
42
  if not rpc_url:
43
43
  results.append((chain_name, "None", "Missing URL", "-"))
44
44
  continue
@@ -381,7 +381,7 @@ class WalletsScreen(VerticalScroll):
381
381
  self.stop_monitor()
382
382
  addresses = [acc.address for acc in self.wallet.key_storage.accounts.values()]
383
383
  for chain_name, interface in ChainInterfaces().items():
384
- if interface.chain.rpc:
384
+ if interface.current_rpc:
385
385
  monitor = EventMonitor(addresses, self.monitor_callback, chain_name)
386
386
 
387
387
  # Worker wrapper
@@ -497,7 +497,7 @@ class WalletsScreen(VerticalScroll):
497
497
  """Handle blockchain selection changes."""
498
498
  if event.value and event.value != self.active_chain:
499
499
  interface = ChainInterfaces().get(event.value)
500
- if not interface or not interface.chain.rpc:
500
+ if not interface or not interface.current_rpc:
501
501
  self.notify(f"No RPC for {event.value}", severity="warning")
502
502
  event.control.value = self.active_chain
503
503
  return
iwa/tui/tests/test_rpc.py CHANGED
@@ -71,7 +71,7 @@ def test_check_rpcs_success(rpc_view, mock_chain_interfaces):
71
71
  """Test check_rpcs with successful connections."""
72
72
  # Setup mock chain interfaces
73
73
  mock_gnosis = MagicMock()
74
- mock_gnosis.chain.rpc = "http://gnosis"
74
+ mock_gnosis.current_rpc = "http://gnosis"
75
75
  mock_gnosis.web3.is_connected.return_value = True
76
76
 
77
77
  mock_chain_interfaces.get.side_effect = lambda name: mock_gnosis if name == "gnosis" else None
@@ -99,7 +99,7 @@ def test_check_rpcs_success(rpc_view, mock_chain_interfaces):
99
99
  def test_check_rpcs_error(rpc_view, mock_chain_interfaces):
100
100
  """Test check_rpcs with connection error."""
101
101
  mock_eth = MagicMock()
102
- mock_eth.chain.rpc = "http://eth"
102
+ mock_eth.current_rpc = "http://eth"
103
103
  mock_eth.web3.is_connected.side_effect = Exception("Connection fail")
104
104
 
105
105
  mock_chain_interfaces.get.side_effect = lambda name: mock_eth if name == "ethereum" else None
iwa/tui/widgets/base.py CHANGED
@@ -39,7 +39,7 @@ class ChainSelector(Horizontal):
39
39
 
40
40
  for name in chain_names:
41
41
  interface = ChainInterfaces().get(name)
42
- if interface.chain.rpc:
42
+ if interface.current_rpc:
43
43
  label = name.title()
44
44
  chain_options.append((label, name))
45
45
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iwa
3
- Version: 0.0.20
3
+ Version: 0.0.23
4
4
  Summary: A secure, modular, and plugin-based framework for crypto agents and ops
5
5
  Requires-Python: <4.0,>=3.12
6
6
  Description-Content-Type: text/markdown
@@ -9,7 +9,7 @@ iwa/core/ipfs.py,sha256=aHjq_pflgwDVHl8g5EMQv0q2RAmMs-a0pOTVsj_L5xE,4980
9
9
  iwa/core/keys.py,sha256=ckacVZxm_02V9hlmHIxz-CkxjXdGHqvGGAXfO6EeHCw,22365
10
10
  iwa/core/mnemonic.py,sha256=LiG1VmpydQoHQ0pHUJ1OIlrWJry47VSMnOqPM_Yk-O8,12930
11
11
  iwa/core/models.py,sha256=kBQ0cBe6uFmL2QfW7mjKiMFeZxhT-FRN-RyK3Ko0vE8,12849
12
- iwa/core/monitor.py,sha256=OmhKVMkfhvtxig3wDUL6iGwBIClTx0YUqMncCao4SqI,7953
12
+ iwa/core/monitor.py,sha256=6hQHAdJIsyoOwnZ9KdYDk_k0mclgr94iFk8V6BtatFQ,7957
13
13
  iwa/core/plugins.py,sha256=FLvOG4S397fKi0aTH1fWBEtexn4yvGv_QzGWqFrhSKE,1102
14
14
  iwa/core/pricing.py,sha256=uENpqVMmuogZHctsLuEsU7WJ1cLSNAI-rZTtbpTDjeQ,4048
15
15
  iwa/core/rpc_monitor.py,sha256=-NHR1Mn2IJKJ9x975NGfsze_shI12yL0OyTPtmjUMKg,1661
@@ -22,7 +22,7 @@ iwa/core/utils.py,sha256=shJuANkXSWVO3NF49syPA9hCG7H5AzaMJOG8V4fo6IM,4279
22
22
  iwa/core/wallet.py,sha256=sNFK-_0y-EgeLpNHt9o5tCqTM0oVqJra-eAWjR7AgyU,13038
23
23
  iwa/core/chain/__init__.py,sha256=XJMmn0ed-_aVkY2iEMKpuTxPgIKBd41dexSVmEZTa-o,1604
24
24
  iwa/core/chain/errors.py,sha256=9SEbhxZ-qASPkzt-DoI51qq0GRJVqRgqgL720gO7a64,1275
25
- iwa/core/chain/interface.py,sha256=uUIUo9Q6Sr4h9AjqFe6WqQzdbMs9VkJcUj0mgd6lfx4,18602
25
+ iwa/core/chain/interface.py,sha256=04eGlhonHAvxFnqLoHRWUaQBzys6jW6BppUuNNjlnSk,18809
26
26
  iwa/core/chain/manager.py,sha256=cFEzh6pK5OyVhjhpeMAqhc9RnRDQR1DjIGiGKp-FXBI,1159
27
27
  iwa/core/chain/models.py,sha256=0OgBo08FZEQisOdd00YUMXSAV7BC0CcWpqJ2y-gs0cI,4863
28
28
  iwa/core/chain/rate_limiter.py,sha256=gU7TmWdH9D_wbXKT1X7mIgoIUCWVuebgvRhxiyLGAmI,6613
@@ -38,7 +38,7 @@ iwa/core/services/__init__.py,sha256=ab5pYzmu3LrZLTO5N-plx6Rp4R0hBEnbbzsgz84zWGM
38
38
  iwa/core/services/account.py,sha256=01MoEvl6FJlMnMB4fGwsPtnGa4kgA-d5hJeKu_ACg7Y,1982
39
39
  iwa/core/services/balance.py,sha256=mPE12CuOFfCaJXaQXWOcQM1O03ZF3ghpy_-oOjNk_GE,4104
40
40
  iwa/core/services/plugin.py,sha256=GNNlbtELyHl7MNVChrypF76GYphxXduxDog4kx1MLi8,3277
41
- iwa/core/services/safe.py,sha256=ytNJMndXrzTMHwhDZKYLIh4Q0UTWDBgQgTpof-UqIkA,14827
41
+ iwa/core/services/safe.py,sha256=ZmgVwbQhYlH5r3qhlY5uP8nCPtkkvV3sNnYG7_UCWUQ,14831
42
42
  iwa/core/services/transaction.py,sha256=DiEVwE1L_UpCyC5UmknaRwRYRxsDlAkwMQRN64NiwIQ,15162
43
43
  iwa/core/services/transfer/__init__.py,sha256=ZJfshFxJRsp8rkOqfVvd1cqEzIJ9tqBJh8pc0l90GLk,5576
44
44
  iwa/core/services/transfer/base.py,sha256=sohz-Ss2i-pGYGl4x9bD93cnYKcSvsXaXyvyRawvgQs,9043
@@ -51,20 +51,20 @@ iwa/plugins/__init__.py,sha256=zy-DjOZn8GSgIETN2X_GAb9O6yk71t6ZRzeUgoZ52KA,23
51
51
  iwa/plugins/gnosis/__init__.py,sha256=dpx0mE84eV-g5iZaH5nKivZJnoKWyRFX5rhdjowBwuU,114
52
52
  iwa/plugins/gnosis/cow_utils.py,sha256=iSvbfgTr2bCqRsUznKCWqmoTnyuX-WZX4oh0E-l3XBU,2263
53
53
  iwa/plugins/gnosis/plugin.py,sha256=AgkgOGYfnrcjWrPUiAvySMj6ITnss0SFXiEi6Z6fnMs,1885
54
- iwa/plugins/gnosis/safe.py,sha256=aDcBz50Bliur90sVqxgjGoQJyCqsfZlm0PDsmWIDBak,5517
54
+ iwa/plugins/gnosis/safe.py,sha256=ye5GQhzKALPNiyJhr7lyrhDgdrDyIj_h3TN2QWI4Xds,5519
55
55
  iwa/plugins/gnosis/cow/__init__.py,sha256=lZN5QpIYWL67rE8r7z7zS9dlr8OqFrYeD9T4-RwUghU,224
56
56
  iwa/plugins/gnosis/cow/quotes.py,sha256=u2xFKgL7QTKqCkSPMv1RHaXvZ6WzID4haaZDMVS42Bs,5177
57
57
  iwa/plugins/gnosis/cow/swap.py,sha256=XZdvJbTbh54hxer7cKkum7lNQ-03gddMK95K3MenaFE,15209
58
58
  iwa/plugins/gnosis/cow/types.py,sha256=-9VRiFhAkmN1iIJ95Pg7zLFSeXtkkW00sl13usxi3o8,470
59
59
  iwa/plugins/gnosis/tests/test_cow.py,sha256=iVy5ockMIcPZWsX4WGXU91DhBsYEZ5NOxtFzAQ2sK3o,8440
60
- iwa/plugins/gnosis/tests/test_safe.py,sha256=pw1zrYvAiVtmPIU5k7BtOQpDNAQTSTrLIaeljCjSahc,3216
60
+ iwa/plugins/gnosis/tests/test_safe.py,sha256=hQHVHBWQhGnuvzvx4U9fOWEwASJWwql42q6cfRcuAls,3218
61
61
  iwa/plugins/olas/__init__.py,sha256=_NhBczzM61fhGYwGhnWfEeL8Jywyy_730GASe2BxzeQ,106
62
62
  iwa/plugins/olas/constants.py,sha256=iTFoO2QW3KbhL5k5sKsJxxyDytl9wVIb_9hAih55KrE,7728
63
63
  iwa/plugins/olas/events.py,sha256=SWD3wYdQ-l6dLUJSkfh_WsLmedH4Vsw_EvYXg7QC3yc,5970
64
- iwa/plugins/olas/importer.py,sha256=f8KlZ9dGcNbpg8uoTYbO9sDvbluZoslhpWFLqPjPnLA,26717
64
+ iwa/plugins/olas/importer.py,sha256=gf6BQKcaaPiFnjiBW3tv4ZVp2HoHw4xY1NS6r4Ip-8o,37447
65
65
  iwa/plugins/olas/mech_reference.py,sha256=CaSCpQnQL4F7wOG6Ox6Zdoy-uNEQ78YBwVLILQZKL8Q,5782
66
66
  iwa/plugins/olas/models.py,sha256=xC5hYakX53pBT6zZteM9cyiC7t6XRLLpobjQmDYueOo,3520
67
- iwa/plugins/olas/plugin.py,sha256=S_vnvZ02VdVWD7N5kp7u5JIRQ2JLtfwGDZ7OHkAN0M8,9390
67
+ iwa/plugins/olas/plugin.py,sha256=sv1Hx4-wHZOwpdjhrpvxrFaA44wiGkcCXjbI_7NLH2Y,13127
68
68
  iwa/plugins/olas/contracts/activity_checker.py,sha256=WXxuzbpXGVqIfEiMPiiqN3Z_UxIY-Lvx0raa1ErBfPA,5323
69
69
  iwa/plugins/olas/contracts/base.py,sha256=y73aQbDq6l4zUpz_eQAg4MsLkTAEqjjupXlcvxjfgCI,240
70
70
  iwa/plugins/olas/contracts/mech.py,sha256=dXYtyORc-oiu9ga5PtTquOFkoakb6BLGKvlUsteygIg,2767
@@ -92,7 +92,7 @@ iwa/plugins/olas/service_manager/mech.py,sha256=NVzVbEmyOe3wK92VEzCCOSuy3HDkEP1M
92
92
  iwa/plugins/olas/service_manager/staking.py,sha256=7REp_HziKtqF9uSvbcq01C9XiaxgVT3gCimuLAAdNnM,28219
93
93
  iwa/plugins/olas/tests/conftest.py,sha256=4vM7EI00SrTGyeP0hNzsGSQHEj2-iznVgzlNh2_OGfo,739
94
94
  iwa/plugins/olas/tests/test_importer.py,sha256=i9LKov7kNRECB3hmRnhKBwcfx3uxtjWe4BB77bOOpeo,4282
95
- iwa/plugins/olas/tests/test_importer_error_handling.py,sha256=X37TrvJ6-3-NuJ2megm0Cnx3KJdA1wke563pRf_EPJk,12084
95
+ iwa/plugins/olas/tests/test_importer_error_handling.py,sha256=O5yd7w_eURtkJb8_IwAGkz8fyHLTzYfI5c2JxWl3oOo,12081
96
96
  iwa/plugins/olas/tests/test_mech_contracts.py,sha256=wvxuigPafF-ySIHVBdWVei3AO418iPh7cSVdAlUGm_s,3566
97
97
  iwa/plugins/olas/tests/test_olas_contracts.py,sha256=B8X-5l1KfYMoZOiM94_rcNzbILLl78rqt_jhyxzAOqE,10835
98
98
  iwa/plugins/olas/tests/test_olas_integration.py,sha256=vjL8-RNdxXu6RFR5F1Bn7xqnxnUVWTzl2--Pp7-0r5A,22973
@@ -101,7 +101,7 @@ iwa/plugins/olas/tests/test_olas_view.py,sha256=kh3crsriyoRiZC6l8vzGllocvQnYmqzi
101
101
  iwa/plugins/olas/tests/test_olas_view_actions.py,sha256=jAxr9bjFNAaxGf1btIrxdMaHgJ0PWX9aDwVU-oPGMpk,5109
102
102
  iwa/plugins/olas/tests/test_olas_view_modals.py,sha256=8j0PNFjKqFC5V1kBdVFWNLMvqGt49H6fLSYGxn02c8o,5562
103
103
  iwa/plugins/olas/tests/test_plugin.py,sha256=RVgU-Cq6t_3mOh90xFAGwlJOV7ZIgp0VNaK5ZAxisAQ,2565
104
- iwa/plugins/olas/tests/test_plugin_full.py,sha256=xevLYmdU4zd5FyEEEn9gvzGimPztmt6vymybeZHXnq8,8507
104
+ iwa/plugins/olas/tests/test_plugin_full.py,sha256=GBZ-TL5t8NXy6HVQozCI1hMEn2EY4lXjGFC89QpoPxQ,8415
105
105
  iwa/plugins/olas/tests/test_service_lifecycle.py,sha256=sOCtpz8T9s55AZe9AoqP1h3XrXw5NDSjDqwLgYThvU4,5559
106
106
  iwa/plugins/olas/tests/test_service_manager.py,sha256=rS2m0A26apc-o4HsfP5oXmVcmZSR5e874bjhQKZRaSg,40650
107
107
  iwa/plugins/olas/tests/test_service_manager_errors.py,sha256=udlAsQj_t1F5TwVQuWhroF6jDJ4RmGEXaxPh87tMsuA,8538
@@ -125,18 +125,18 @@ iwa/tools/test_chainlist.py,sha256=9J06sTsKgnEcN7WSn-YgJkCHhfbGDdVS-KNMDBhYllA,1
125
125
  iwa/tools/wallet_check.py,sha256=IQLgb8oCt4oG6FMEAqzUxM57DLv_UE24dFUSVxtBo_Y,4774
126
126
  iwa/tui/__init__.py,sha256=XYIZNQNy-fZC1NHHM0sd9qUO0vE1slml-cm0CpQ4NLY,27
127
127
  iwa/tui/app.py,sha256=XDQ4nAPGBwhrEmdL_e3V8oYSOho8pY7jsd3C_wk92UU,4163
128
- iwa/tui/rpc.py,sha256=4q2zcBu7XXDU9c66UYtfbXiNdQyS_uLa9xOs3UmPOO0,2131
128
+ iwa/tui/rpc.py,sha256=iEp7aQ2MZxeXWqvxYud_5Y5oX2NoweMd1DQQlGYBGv8,2133
129
129
  iwa/tui/workers.py,sha256=lvzbIS375_H1rj7-9d-w0PKnkDJ4lW_13aWzZRaX9fY,1192
130
130
  iwa/tui/modals/__init__.py,sha256=OyrjWjaPqQAllZcUJ-Ac_e1PtTouJy8m1eGo132p-EA,130
131
131
  iwa/tui/modals/base.py,sha256=q9dEV6We_SPxbMRh711amFDwAOBywD00Qg0jcqvh5LE,12060
132
132
  iwa/tui/screens/__init__.py,sha256=j0brLsuVd9M8hM5LHH05E7manY3ZVj24yf7nFyGryp4,31
133
- iwa/tui/screens/wallets.py,sha256=F_1feKuml3rUUHSfdT2XmgBIViGHNrdQeUcgpFUb0Ys,30821
133
+ iwa/tui/screens/wallets.py,sha256=U6IUbV_7ByAyUi3aBVdFr3A1QlGzNORRE4uOHBQXQB0,30825
134
134
  iwa/tui/tests/test_app.py,sha256=F0tJthsyWzwNbHcGtiyDQtKDPn3m9N1qt2vMGiXrQTQ,3868
135
- iwa/tui/tests/test_rpc.py,sha256=chH9b6GunfymaYdqz3GVQ_LQui4zihyBkrCG6xOUY1Q,4326
135
+ iwa/tui/tests/test_rpc.py,sha256=4m2HC-R5R9kO5pluo2G_CrTBQv63YYrdZNufTjtnGUk,4330
136
136
  iwa/tui/tests/test_wallets_refactor.py,sha256=71G3HLbhTtgDy3ffVbYv0MFYRgdYd-NWGBdvdzW4M9c,998
137
137
  iwa/tui/tests/test_widgets.py,sha256=C9UgIGeWRaQ459JygFEQx-7hOi9mWrSUDDIMZH1ge50,3994
138
138
  iwa/tui/widgets/__init__.py,sha256=UzD6nJbwv9hOtkWl9I7faXm1a-rcu4xFRxrf4KBwwY4,161
139
- iwa/tui/widgets/base.py,sha256=G844GU61qSS6AgY5NxmOVHTghbgHlyTfo8hhE2VUrqQ,3379
139
+ iwa/tui/widgets/base.py,sha256=Z8FigMhsfD76PkFVERqMaotd-xwXfuFZm_8TmCMOsl4,3381
140
140
  iwa/web/dependencies.py,sha256=0_dAJlRh6gKrUDRPKUe92eshFsg572yx_H0lQgSqGDA,2103
141
141
  iwa/web/models.py,sha256=MSD9WPy_Nz_amWgoo2KSDTn4ZLv_AV0o0amuNtSf-68,3035
142
142
  iwa/web/server.py,sha256=4ZLVFEKoGs_NoCcXMeyYzDNdxUXazjwHQaX7CR1pwHE,5239
@@ -157,7 +157,7 @@ iwa/web/tests/test_web_endpoints.py,sha256=C264MH-CTyDW4GLUrTXBgLJKUk4-89pFAScBd
157
157
  iwa/web/tests/test_web_olas.py,sha256=0CVSsrncOeJ3x0ECV7mVLQV_CXZRrOqGiVjgLIi6hZ8,16308
158
158
  iwa/web/tests/test_web_swap.py,sha256=7A4gBJFL01kIXPtW1E1J17SCsVc_0DmUn-R8kKrnnVA,2974
159
159
  iwa/web/tests/test_web_swap_coverage.py,sha256=zGNrzlhZ_vWDCvWmLcoUwFgqxnrp_ACbo49AtWBS_Kw,5584
160
- iwa-0.0.20.dist-info/licenses/LICENSE,sha256=eIubm_IlBHPYRQlLNZKbBNKhJUUP3JH0A2miZUhAVfI,1078
160
+ iwa-0.0.23.dist-info/licenses/LICENSE,sha256=eIubm_IlBHPYRQlLNZKbBNKhJUUP3JH0A2miZUhAVfI,1078
161
161
  tests/legacy_cow.py,sha256=oOkZvIxL70ReEoD9oHQbOD5GpjIr6AGNHcOCgfPlerU,8389
162
162
  tests/legacy_safe.py,sha256=AssM2g13E74dNGODu_H0Q0y412lgqsrYnEzI97nm_Ts,2972
163
163
  tests/legacy_transaction_retry_logic.py,sha256=D9RqZ7DBu61Xr2djBAodU2p9UE939LL-DnQXswX5iQk,1497
@@ -182,7 +182,7 @@ tests/test_migration.py,sha256=fYoxzI3KqGh0cPV0bFcbvGrAnKcNlvnwjggG_uD0QGo,1789
182
182
  tests/test_mnemonic.py,sha256=BFtXMMg17uHWh_H-ZwAOn0qzgbUCqL8BRLkgRjzfzxo,7379
183
183
  tests/test_modals.py,sha256=R_lXa7wnnGewAP5jJvVZDyQyY1FbE98IeO2B7y3x86c,2945
184
184
  tests/test_models.py,sha256=1bEfPiDVgEdtwFEzwecSPAHjCF8kjOPSMeQExJ7eCJ4,7107
185
- tests/test_monitor.py,sha256=P_hF61VMlCX2rh9yu_a6aKhlgXOAcCMGOZRntjcqrd0,7255
185
+ tests/test_monitor.py,sha256=dRVS6EkTwfvGEOg7t0dVhs6M3oEZExBH7iBZe6hmk4M,7261
186
186
  tests/test_multisend.py,sha256=IvXpwnC5xSDRCyCDGcMdO3L-eQegvdjAzHZB0FoVFUI,2685
187
187
  tests/test_plugin_service.py,sha256=ZEe37kV_sv4Eb04032O1hZIoo9yf5gJo83ks7Grzrng,3767
188
188
  tests/test_pricing.py,sha256=ptu_2Csc6d64bIzMMw3TheJge2Kfn05Gs-twz_KmBzg,5276
@@ -210,8 +210,8 @@ tests/test_utils.py,sha256=vkP49rYNI8BRzLpWR3WnKdDr8upeZjZcs7Rx0pjbQMo,1292
210
210
  tests/test_workers.py,sha256=MInwdkFY5LdmFB3o1odIaSD7AQZb3263hNafO1De5PE,2793
211
211
  tools/create_and_stake_service.py,sha256=1xwy_bJQI1j9yIQ968Oc9Db_F6mk1659LuuZntTASDE,3742
212
212
  tools/verify_drain.py,sha256=PkMjblyOOAuQge88FwfEzRtCYeEtJxXhPBmtQYCoQ-8,6743
213
- iwa-0.0.20.dist-info/METADATA,sha256=_ERQ1f_nDtVFZClmaQ4vW6o6Yso2JjqumBbS56JJCCs,7295
214
- iwa-0.0.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
215
- iwa-0.0.20.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
216
- iwa-0.0.20.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
217
- iwa-0.0.20.dist-info/RECORD,,
213
+ iwa-0.0.23.dist-info/METADATA,sha256=M0c-f0k7QPegtSsPkXm8gP8Ed12IS_8zEJZMNGMK3Ug,7295
214
+ iwa-0.0.23.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
215
+ iwa-0.0.23.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
216
+ iwa-0.0.23.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
217
+ iwa-0.0.23.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
tests/test_monitor.py CHANGED
@@ -12,7 +12,7 @@ def mock_chain_interfaces():
12
12
  instance = mock.return_value
13
13
  gnosis_interface = MagicMock()
14
14
  gnosis_interface.chain.name = "Gnosis"
15
- gnosis_interface.chain.rpc = "https://rpc"
15
+ gnosis_interface.current_rpc = "https://rpc"
16
16
  gnosis_interface.web3 = MagicMock()
17
17
  instance.get.return_value = gnosis_interface
18
18
  yield instance
@@ -46,7 +46,7 @@ def test_monitor_init_rpc_fail(mock_chain_interfaces, mock_callback):
46
46
 
47
47
  def test_monitor_init_no_rpc(mock_chain_interfaces, mock_callback):
48
48
  chain_interface = mock_chain_interfaces.get.return_value
49
- chain_interface.chain.rpc = ""
49
+ chain_interface.current_rpc = ""
50
50
 
51
51
  monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
52
52
  assert monitor.last_checked_block == 0
@@ -54,7 +54,7 @@ def test_monitor_init_no_rpc(mock_chain_interfaces, mock_callback):
54
54
 
55
55
  def test_start_no_rpc(mock_chain_interfaces, mock_callback):
56
56
  chain_interface = mock_chain_interfaces.get.return_value
57
- chain_interface.chain.rpc = ""
57
+ chain_interface.current_rpc = ""
58
58
 
59
59
  monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
60
60