iwa 0.0.21__py3-none-any.whl → 0.0.24__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.
- iwa/plugins/olas/importer.py +261 -37
- iwa/plugins/olas/plugin.py +127 -41
- iwa/plugins/olas/tests/test_importer_error_handling.py +1 -1
- iwa/plugins/olas/tests/test_plugin_full.py +4 -4
- {iwa-0.0.21.dist-info → iwa-0.0.24.dist-info}/METADATA +1 -1
- {iwa-0.0.21.dist-info → iwa-0.0.24.dist-info}/RECORD +10 -10
- {iwa-0.0.21.dist-info → iwa-0.0.24.dist-info}/WHEEL +1 -1
- {iwa-0.0.21.dist-info → iwa-0.0.24.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.21.dist-info → iwa-0.0.24.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.21.dist-info → iwa-0.0.24.dist-info}/top_level.txt +0 -0
iwa/plugins/olas/importer.py
CHANGED
|
@@ -7,6 +7,7 @@ Supports two formats:
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
+
import re
|
|
10
11
|
from dataclasses import dataclass, field
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import List, Optional, Tuple
|
|
@@ -17,6 +18,28 @@ from loguru import logger
|
|
|
17
18
|
from iwa.core.keys import EncryptedAccount, KeyStorage
|
|
18
19
|
from iwa.core.models import Config, StoredSafeAccount
|
|
19
20
|
|
|
21
|
+
# Known mappings from olas-operate-middleware staking programs
|
|
22
|
+
# See: https://github.com/valory-xyz/olas-operate-middleware/blob/main/operate/ledger/profiles.py
|
|
23
|
+
STAKING_PROGRAM_MAP = {
|
|
24
|
+
# Pearl staking programs (gnosis) - operate format
|
|
25
|
+
"pearl_alpha": "0x5344B7DD311e5d3DdDd46A4f71481Bd7b05AAA3e", # Expert Legacy
|
|
26
|
+
"pearl_beta": "0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C", # Hobbyist 1 Legacy
|
|
27
|
+
"pearl_beta_2": "0xE56dF1E563De1B10715cB313D514af350D207212", # Expert 5 Legacy
|
|
28
|
+
"pearl_beta_3": "0xD7A3C8b975f71030135f1a66E9e23164d54fF455", # Expert 7 Legacy
|
|
29
|
+
"pearl_beta_4": "0x17dBAe44BC5618Cc254055B386A29576b4F87015", # Expert 9 Legacy
|
|
30
|
+
"pearl_beta_5": "0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f", # Expert 10 Legacy
|
|
31
|
+
"pearl_beta_mm_v2_1": "0x75eeca6207be98cac3fde8a20ecd7b01e50b3472", # Expert 3 MM v2
|
|
32
|
+
"pearl_beta_mm_v2_2": "0x9c7f6103e3a72e4d1805b9c683ea5b370ec1a99f", # Expert 4 MM v2
|
|
33
|
+
"pearl_beta_mm_v2_3": "0xcdC603e0Ee55Aae92519f9770f214b2Be4967f7d", # Expert 5 MM v2
|
|
34
|
+
# Quickstart staking programs (gnosis) - quickstart format
|
|
35
|
+
"quickstart_beta_expert_4": "0xaD9d891134443B443D7F30013c7e14Fe27F2E029", # Expert 4 Legacy
|
|
36
|
+
"quickstart_beta_expert_7": "0xD7A3C8b975f71030135f1a66E9e23164d54fF455", # Expert 7 Legacy
|
|
37
|
+
"quickstart_beta_expert_9": "0x17dBAe44BC5618Cc254055B386A29576b4F87015", # Expert 9 Legacy
|
|
38
|
+
"quickstart_beta_expert_11": "0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7", # Expert 11 Legacy
|
|
39
|
+
"quickstart_beta_expert_16_mech_marketplace": "0x6c65430515c70a3f5E62107CC301685B7D46f991", # Expert 16 MM v1
|
|
40
|
+
"quickstart_beta_expert_18_mech_marketplace": "0x041e679d04Fc0D4f75Eb937Dea729Df09a58e454", # Expert 18 MM v1
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
|
|
21
44
|
@dataclass
|
|
22
45
|
class DiscoveredKey:
|
|
@@ -26,8 +49,10 @@ class DiscoveredKey:
|
|
|
26
49
|
private_key: Optional[str] = None # Plaintext hex (None if still encrypted)
|
|
27
50
|
encrypted_keystore: Optional[dict] = None # Web3 v3 keystore format
|
|
28
51
|
source_file: Path = field(default_factory=Path)
|
|
29
|
-
role: str = "unknown" # "agent", "
|
|
52
|
+
role: str = "unknown" # "agent", "owner"
|
|
30
53
|
is_encrypted: bool = False
|
|
54
|
+
signature_verified: bool = False
|
|
55
|
+
signature_failed: bool = False
|
|
31
56
|
|
|
32
57
|
@property
|
|
33
58
|
def is_decrypted(self) -> bool:
|
|
@@ -55,6 +80,9 @@ class DiscoveredService:
|
|
|
55
80
|
source_folder: Path = field(default_factory=Path)
|
|
56
81
|
format: str = "unknown" # "trader_runner" or "operate"
|
|
57
82
|
service_name: Optional[str] = None
|
|
83
|
+
# New fields for full service import
|
|
84
|
+
staking_contract_address: Optional[str] = None
|
|
85
|
+
service_owner_address: Optional[str] = None
|
|
58
86
|
|
|
59
87
|
@property
|
|
60
88
|
def agent_key(self) -> Optional[DiscoveredKey]:
|
|
@@ -66,9 +94,14 @@ class DiscoveredService:
|
|
|
66
94
|
|
|
67
95
|
@property
|
|
68
96
|
def operator_key(self) -> Optional[DiscoveredKey]:
|
|
69
|
-
"""Get the operator key
|
|
97
|
+
"""Get the operator (owner) key. Alias for compatibility."""
|
|
98
|
+
return self.owner_key
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def owner_key(self) -> Optional[DiscoveredKey]:
|
|
102
|
+
"""Get the owner key if present (matches 'owner' or 'operator' roles)."""
|
|
70
103
|
for key in self.keys:
|
|
71
|
-
if key.role in
|
|
104
|
+
if key.role in ["owner", "operator"]:
|
|
72
105
|
return key
|
|
73
106
|
return None
|
|
74
107
|
|
|
@@ -89,15 +122,17 @@ class ImportResult:
|
|
|
89
122
|
class OlasServiceImporter:
|
|
90
123
|
"""Discover and import Olas services from external directories."""
|
|
91
124
|
|
|
92
|
-
def __init__(self, key_storage: Optional[KeyStorage] = None):
|
|
125
|
+
def __init__(self, key_storage: Optional[KeyStorage] = None, password: Optional[str] = None):
|
|
93
126
|
"""Initialize the importer.
|
|
94
127
|
|
|
95
128
|
Args:
|
|
96
129
|
key_storage: KeyStorage instance. If None, will create one.
|
|
130
|
+
password: Optional password to decrypt discovered keystores.
|
|
97
131
|
|
|
98
132
|
"""
|
|
99
133
|
self.key_storage = key_storage or KeyStorage()
|
|
100
134
|
self.config = Config()
|
|
135
|
+
self.password = password
|
|
101
136
|
|
|
102
137
|
def scan_directory(self, path: Path) -> List[DiscoveredService]:
|
|
103
138
|
"""Recursively scan a directory for Olas services.
|
|
@@ -106,7 +141,7 @@ class OlasServiceImporter:
|
|
|
106
141
|
path: Directory to scan.
|
|
107
142
|
|
|
108
143
|
Returns:
|
|
109
|
-
List of discovered services.
|
|
144
|
+
List of discovered services (deduplicated by chain:service_id).
|
|
110
145
|
|
|
111
146
|
"""
|
|
112
147
|
path = Path(path)
|
|
@@ -129,8 +164,51 @@ class OlasServiceImporter:
|
|
|
129
164
|
services = self._parse_operate_format(operate)
|
|
130
165
|
discovered.extend(services)
|
|
131
166
|
|
|
132
|
-
|
|
133
|
-
|
|
167
|
+
return self._deduplicate_services(discovered)
|
|
168
|
+
|
|
169
|
+
def _deduplicate_services(self, services: List[DiscoveredService]) -> List[DiscoveredService]:
|
|
170
|
+
"""Deduplicate discovered services by chain:service_id."""
|
|
171
|
+
seen_keys: set = set()
|
|
172
|
+
unique_services = []
|
|
173
|
+
duplicates = 0
|
|
174
|
+
for service in services:
|
|
175
|
+
if service.service_id:
|
|
176
|
+
key = f"{service.chain_name}:{service.service_id}"
|
|
177
|
+
if key in seen_keys:
|
|
178
|
+
logger.debug(
|
|
179
|
+
f"Skipping duplicate service {key} from {service.source_folder}"
|
|
180
|
+
)
|
|
181
|
+
duplicates += 1
|
|
182
|
+
continue
|
|
183
|
+
seen_keys.add(key)
|
|
184
|
+
unique_services.append(service)
|
|
185
|
+
|
|
186
|
+
if duplicates:
|
|
187
|
+
logger.info(f"Skipped {duplicates} duplicate service(s)")
|
|
188
|
+
logger.info(f"Discovered {len(unique_services)} unique Olas service(s)")
|
|
189
|
+
return unique_services
|
|
190
|
+
|
|
191
|
+
def _find_trader_name(self, folder: Path) -> str:
|
|
192
|
+
"""Find the trader name by traversing up the directory tree.
|
|
193
|
+
|
|
194
|
+
Handles quickstart format where the .operate folder is nested inside
|
|
195
|
+
a quickstart folder, e.g.: trader_altair/quickstart/.operate/
|
|
196
|
+
|
|
197
|
+
Returns the first folder name starting with 'trader_' or the
|
|
198
|
+
immediate folder name if none found.
|
|
199
|
+
"""
|
|
200
|
+
current = folder
|
|
201
|
+
fallback = folder.name
|
|
202
|
+
|
|
203
|
+
# Traverse up looking for trader_* folder
|
|
204
|
+
for _ in range(5): # Max 5 levels up
|
|
205
|
+
if current.name.startswith("trader_"):
|
|
206
|
+
return current.name
|
|
207
|
+
current = current.parent
|
|
208
|
+
if current == current.parent: # Reached root
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
return fallback
|
|
134
212
|
|
|
135
213
|
def _parse_trader_runner_format(self, folder: Path) -> Optional[DiscoveredService]:
|
|
136
214
|
"""Parse a .trader_runner folder.
|
|
@@ -153,6 +231,9 @@ class OlasServiceImporter:
|
|
|
153
231
|
service.safe_address = self._extract_safe_address(folder)
|
|
154
232
|
service.keys = self._extract_trader_keys(folder)
|
|
155
233
|
|
|
234
|
+
# Extract staking program from .env
|
|
235
|
+
self._extract_staking_from_env(service, folder)
|
|
236
|
+
|
|
156
237
|
if not service.keys and not service.service_id:
|
|
157
238
|
logger.debug(f"No valid data found in {folder}")
|
|
158
239
|
return None
|
|
@@ -187,10 +268,10 @@ class OlasServiceImporter:
|
|
|
187
268
|
if key:
|
|
188
269
|
keys.append(key)
|
|
189
270
|
|
|
190
|
-
# Parse operator_pkey.txt
|
|
271
|
+
# Parse operator_pkey.txt (contains owner key)
|
|
191
272
|
operator_file = folder / "operator_pkey.txt"
|
|
192
273
|
if operator_file.exists():
|
|
193
|
-
key = self._parse_keystore_file(operator_file, role="
|
|
274
|
+
key = self._parse_keystore_file(operator_file, role="owner")
|
|
194
275
|
if key:
|
|
195
276
|
keys.append(key)
|
|
196
277
|
|
|
@@ -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=
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -625,8 +793,6 @@ class OlasServiceImporter:
|
|
|
625
793
|
Tags follow the pattern: {service_name}_{role}
|
|
626
794
|
Example: trader_alpha_agent, trader_alpha_operator
|
|
627
795
|
"""
|
|
628
|
-
import re
|
|
629
|
-
|
|
630
796
|
# Use service name as prefix, or 'imported' as fallback
|
|
631
797
|
prefix = service_name or "imported"
|
|
632
798
|
|
|
@@ -667,7 +833,6 @@ class OlasServiceImporter:
|
|
|
667
833
|
signers.append(key.address)
|
|
668
834
|
|
|
669
835
|
# Generate tag
|
|
670
|
-
import re
|
|
671
836
|
|
|
672
837
|
prefix = service.service_name or "imported"
|
|
673
838
|
prefix = re.sub(r"[^a-z0-9]+", "_", prefix.lower()).strip("_")
|
|
@@ -698,6 +863,7 @@ class OlasServiceImporter:
|
|
|
698
863
|
def _import_service_config(self, service: DiscoveredService) -> Tuple[bool, str]:
|
|
699
864
|
"""Import service config to OlasConfig."""
|
|
700
865
|
try:
|
|
866
|
+
from iwa.plugins.olas.constants import OLAS_TOKEN_ADDRESS_GNOSIS
|
|
701
867
|
from iwa.plugins.olas.models import OlasConfig, Service
|
|
702
868
|
|
|
703
869
|
# Get or create OlasConfig
|
|
@@ -711,13 +877,16 @@ class OlasServiceImporter:
|
|
|
711
877
|
if key in olas_config.services:
|
|
712
878
|
return False, "duplicate"
|
|
713
879
|
|
|
714
|
-
# Create service model
|
|
880
|
+
# Create service model with all fields
|
|
715
881
|
olas_service = Service(
|
|
716
882
|
service_name=service.service_name or f"service_{service.service_id}",
|
|
717
883
|
chain_name=service.chain_name,
|
|
718
884
|
service_id=service.service_id,
|
|
719
|
-
agent_ids=[], #
|
|
885
|
+
agent_ids=[25], # Trader agents always use agent ID 25
|
|
720
886
|
multisig_address=service.safe_address,
|
|
887
|
+
service_owner_address=service.service_owner_address,
|
|
888
|
+
staking_contract_address=service.staking_contract_address,
|
|
889
|
+
token_address=str(OLAS_TOKEN_ADDRESS_GNOSIS),
|
|
721
890
|
)
|
|
722
891
|
|
|
723
892
|
# Set agent address if we have one
|
|
@@ -726,7 +895,7 @@ class OlasServiceImporter:
|
|
|
726
895
|
olas_service.agent_address = agent_key.address
|
|
727
896
|
|
|
728
897
|
olas_config.add_service(olas_service)
|
|
729
|
-
self.config.
|
|
898
|
+
self.config.save_config()
|
|
730
899
|
logger.info(f"Imported service {key}")
|
|
731
900
|
return True, "ok"
|
|
732
901
|
|
|
@@ -734,3 +903,58 @@ class OlasServiceImporter:
|
|
|
734
903
|
return False, "Olas plugin not available"
|
|
735
904
|
except Exception as e:
|
|
736
905
|
return False, str(e)
|
|
906
|
+
|
|
907
|
+
def _attempt_decryption(self, key: DiscoveredKey) -> None:
|
|
908
|
+
"""Attempt to decrypt an encrypted keystore using the provided password."""
|
|
909
|
+
if not self.password or not key.encrypted_keystore:
|
|
910
|
+
return
|
|
911
|
+
|
|
912
|
+
try:
|
|
913
|
+
logger.debug(f"Attempting decryption for {key.address}")
|
|
914
|
+
|
|
915
|
+
# Use Account.decrypt to handle standard web3 keystores
|
|
916
|
+
private_key_bytes = Account.decrypt(key.encrypted_keystore, self.password)
|
|
917
|
+
key.private_key = private_key_bytes.hex()
|
|
918
|
+
key.is_encrypted = False
|
|
919
|
+
# If we successfully decrypted, it's no longer "encrypted" for verification purposes
|
|
920
|
+
logger.debug(f"Successfully decrypted key for {key.address}")
|
|
921
|
+
except ValueError as e:
|
|
922
|
+
# Password incorrect
|
|
923
|
+
logger.warning(f"Decryption failed (ValueError) for {key.address}: {e}")
|
|
924
|
+
except Exception as e:
|
|
925
|
+
logger.warning(f"Error decrypting key {key.address}: {type(e).__name__} - {e}")
|
|
926
|
+
|
|
927
|
+
def _verify_key_signature(self, key: DiscoveredKey) -> None:
|
|
928
|
+
"""Verify that the plaintext private key can sign a message and recover the address."""
|
|
929
|
+
if not key.private_key or not key.address:
|
|
930
|
+
return
|
|
931
|
+
|
|
932
|
+
try:
|
|
933
|
+
from eth_account.messages import encode_defunct
|
|
934
|
+
|
|
935
|
+
message = "Hello, world!"
|
|
936
|
+
encoded_message = encode_defunct(text=message)
|
|
937
|
+
signed_message = Account.sign_message(encoded_message, private_key=key.private_key)
|
|
938
|
+
recovered_address = Account.recover_message(
|
|
939
|
+
encoded_message, signature=signed_message.signature
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
# Normalize address to lowercase with 0x prefix
|
|
943
|
+
key_addr = key.address.lower()
|
|
944
|
+
if not key_addr.startswith("0x"):
|
|
945
|
+
key_addr = "0x" + key_addr
|
|
946
|
+
recovered_addr = recovered_address.lower()
|
|
947
|
+
|
|
948
|
+
if recovered_addr == key_addr:
|
|
949
|
+
key.signature_verified = True
|
|
950
|
+
logger.debug(f"Signature verified for key {key.address}")
|
|
951
|
+
else:
|
|
952
|
+
key.signature_failed = True
|
|
953
|
+
logger.warning(
|
|
954
|
+
f"Signature verification FAILED for key {key.address}. "
|
|
955
|
+
f"Recovered: {recovered_address}"
|
|
956
|
+
)
|
|
957
|
+
except Exception as e:
|
|
958
|
+
key.signature_failed = True
|
|
959
|
+
logger.warning(f"Error verifying signature for key {key.address}: {e}")
|
|
960
|
+
|
iwa/plugins/olas/plugin.py
CHANGED
|
@@ -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
|
|
@@ -86,7 +87,18 @@ class OlasPlugin(Plugin):
|
|
|
86
87
|
# Query failed - Safe likely doesn't exist
|
|
87
88
|
return [], False
|
|
88
89
|
|
|
89
|
-
def
|
|
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(
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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("
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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 == "
|
|
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
|
|
@@ -185,15 +185,15 @@ def test_import_services_cli_complex_display(plugin, runner):
|
|
|
185
185
|
# Mock Safe exists with Agent as signer
|
|
186
186
|
mock_get_signers.return_value = (["0xAgent"], True)
|
|
187
187
|
|
|
188
|
-
result = runner.invoke(app, ["/tmp/test", "--dry-run"])
|
|
188
|
+
result = runner.invoke(app, ["/tmp/test", "--dry-run"], input="\n")
|
|
189
189
|
assert "0xSafe" in result.output
|
|
190
190
|
assert "✓" in result.output
|
|
191
191
|
assert "0xAgent 🔓 plaintext" in result.output # Not a warning
|
|
192
192
|
|
|
193
193
|
# 2. Service where agent is NOT a signer
|
|
194
194
|
mock_get_signers.return_value = (["0xOther"], True)
|
|
195
|
-
result = runner.invoke(app, ["/tmp/test", "--dry-run"])
|
|
196
|
-
assert "NOT A SIGNER
|
|
195
|
+
result = runner.invoke(app, ["/tmp/test", "--dry-run"], input="\n")
|
|
196
|
+
assert "NOT A SIGNER!" in result.output
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
def test_import_services_cli_password_prompt(plugin, runner):
|
|
@@ -61,10 +61,10 @@ iwa/plugins/gnosis/tests/test_safe.py,sha256=hQHVHBWQhGnuvzvx4U9fOWEwASJWwql42q6
|
|
|
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
|
|
64
|
+
iwa/plugins/olas/importer.py,sha256=-zI0Fqmf6E7w3OpqEuTR8vuQOaAGNykGWzHZgadrHmE,37362
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
@@ -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.
|
|
160
|
+
iwa-0.0.24.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
|
|
@@ -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.
|
|
214
|
-
iwa-0.0.
|
|
215
|
-
iwa-0.0.
|
|
216
|
-
iwa-0.0.
|
|
217
|
-
iwa-0.0.
|
|
213
|
+
iwa-0.0.24.dist-info/METADATA,sha256=N-qgeL8ZAmzJTvBthIa65x8zoZpsmqVuFG4YZ_K7-kA,7295
|
|
214
|
+
iwa-0.0.24.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
215
|
+
iwa-0.0.24.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
|
|
216
|
+
iwa-0.0.24.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
|
|
217
|
+
iwa-0.0.24.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|