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.
- iwa/core/chain/interface.py +8 -1
- iwa/core/monitor.py +2 -2
- iwa/core/services/safe.py +2 -2
- iwa/plugins/gnosis/safe.py +1 -1
- iwa/plugins/gnosis/tests/test_safe.py +1 -1
- iwa/plugins/olas/importer.py +261 -33
- iwa/plugins/olas/plugin.py +129 -43
- iwa/plugins/olas/tests/test_importer_error_handling.py +1 -1
- iwa/plugins/olas/tests/test_plugin_full.py +7 -9
- iwa/tui/rpc.py +1 -1
- iwa/tui/screens/wallets.py +2 -2
- iwa/tui/tests/test_rpc.py +2 -2
- iwa/tui/widgets/base.py +1 -1
- {iwa-0.0.20.dist-info → iwa-0.0.23.dist-info}/METADATA +1 -1
- {iwa-0.0.20.dist-info → iwa-0.0.23.dist-info}/RECORD +20 -20
- {iwa-0.0.20.dist-info → iwa-0.0.23.dist-info}/WHEEL +1 -1
- tests/test_monitor.py +3 -3
- {iwa-0.0.20.dist-info → iwa-0.0.23.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.20.dist-info → iwa-0.0.23.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.20.dist-info → iwa-0.0.23.dist-info}/top_level.txt +0 -0
iwa/core/chain/interface.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
257
|
+
ethereum_client = EthereumClient(chain_interface.current_rpc)
|
|
258
258
|
|
|
259
259
|
code = ethereum_client.w3.eth.get_code(account.address)
|
|
260
260
|
|
iwa/plugins/gnosis/safe.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
iwa/plugins/olas/importer.py
CHANGED
|
@@ -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", "
|
|
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
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
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="
|
|
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=
|
|
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:
|
|
@@ -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=[], #
|
|
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
|
+
|
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
|
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
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
iwa/tui/screens/wallets.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
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=
|
|
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.
|
|
214
|
-
iwa-0.0.
|
|
215
|
-
iwa-0.0.
|
|
216
|
-
iwa-0.0.
|
|
217
|
-
iwa-0.0.
|
|
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,,
|
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.
|
|
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.
|
|
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.
|
|
57
|
+
chain_interface.current_rpc = ""
|
|
58
58
|
|
|
59
59
|
monitor = EventMonitor(["0x1234567890123456789012345678901234567890"], mock_callback)
|
|
60
60
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|