iwa 0.0.26__py3-none-any.whl → 0.0.28__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/cli.py +18 -0
- iwa/core/contracts/contract.py +12 -1
- iwa/core/contracts/decoder.py +154 -0
- iwa/core/services/transaction.py +63 -2
- iwa/core/wallet.py +1 -1
- iwa/plugins/olas/importer.py +105 -29
- iwa/plugins/olas/models.py +33 -2
- iwa/plugins/olas/plugin.py +68 -17
- iwa/plugins/olas/service_manager/drain.py +5 -2
- iwa/plugins/olas/service_manager/lifecycle.py +14 -6
- iwa/plugins/olas/service_manager/staking.py +20 -9
- iwa/plugins/olas/tests/test_importer_error_handling.py +5 -17
- {iwa-0.0.26.dist-info → iwa-0.0.28.dist-info}/METADATA +1 -1
- {iwa-0.0.26.dist-info → iwa-0.0.28.dist-info}/RECORD +18 -17
- {iwa-0.0.26.dist-info → iwa-0.0.28.dist-info}/WHEEL +0 -0
- {iwa-0.0.26.dist-info → iwa-0.0.28.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.26.dist-info → iwa-0.0.28.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.26.dist-info → iwa-0.0.28.dist-info}/top_level.txt +0 -0
iwa/core/cli.py
CHANGED
|
@@ -7,6 +7,7 @@ from web3 import Web3
|
|
|
7
7
|
|
|
8
8
|
from iwa.core.chain import ChainInterfaces
|
|
9
9
|
from iwa.core.constants import NATIVE_CURRENCY_ADDRESS
|
|
10
|
+
from iwa.core.contracts.decoder import ErrorDecoder
|
|
10
11
|
from iwa.core.keys import KeyStorage
|
|
11
12
|
from iwa.core.services import PluginService
|
|
12
13
|
from iwa.core.tables import list_accounts
|
|
@@ -206,6 +207,23 @@ def web_server(
|
|
|
206
207
|
run_server(host=host, port=server_port)
|
|
207
208
|
|
|
208
209
|
|
|
210
|
+
@iwa_cli.command("decode")
|
|
211
|
+
def decode_hex(
|
|
212
|
+
hex_data: str = typer.Argument(..., help="The hex-encoded error data (e.g., 0xa43d6ada...)"),
|
|
213
|
+
):
|
|
214
|
+
"""Decode a hex error identifier into a human-readable message."""
|
|
215
|
+
decoder = ErrorDecoder()
|
|
216
|
+
results = decoder.decode(hex_data)
|
|
217
|
+
|
|
218
|
+
if not results:
|
|
219
|
+
typer.echo(f"Could not decode error data: {hex_data}")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
typer.echo(f"\nDecoding results for {hex_data[:10]}:")
|
|
223
|
+
for _name, msg, source in results:
|
|
224
|
+
typer.echo(f" [{source}] {msg}")
|
|
225
|
+
|
|
226
|
+
|
|
209
227
|
@wallet_cli.command("drain")
|
|
210
228
|
def drain_wallet(
|
|
211
229
|
from_address_or_tag: str = typer.Option(..., "--from", "-f", help="From address or tag"),
|
iwa/core/contracts/contract.py
CHANGED
|
@@ -11,6 +11,7 @@ from web3.contract import Contract
|
|
|
11
11
|
from web3.exceptions import ContractCustomError
|
|
12
12
|
|
|
13
13
|
from iwa.core.chain import ChainInterfaces
|
|
14
|
+
from iwa.core.contracts.decoder import ErrorDecoder
|
|
14
15
|
from iwa.core.rpc_monitor import RPCMonitor
|
|
15
16
|
from iwa.core.utils import configure_logger
|
|
16
17
|
|
|
@@ -111,7 +112,7 @@ class ContractInstance:
|
|
|
111
112
|
)
|
|
112
113
|
return selectors
|
|
113
114
|
|
|
114
|
-
def decode_error(self, error_data: str) -> Optional[Tuple[str, str]]:
|
|
115
|
+
def decode_error(self, error_data: str) -> Optional[Tuple[str, str]]: # noqa: C901
|
|
115
116
|
"""Decode error data from a failed transaction or call.
|
|
116
117
|
|
|
117
118
|
Handles:
|
|
@@ -172,6 +173,16 @@ class ContractInstance:
|
|
|
172
173
|
logger.debug(f"Failed to decode Panic(uint256): {e}")
|
|
173
174
|
return ("Panic", "Failed to decode panic code")
|
|
174
175
|
|
|
176
|
+
# 4. Global Fallback Decoder
|
|
177
|
+
try:
|
|
178
|
+
global_results = ErrorDecoder().decode(error_data)
|
|
179
|
+
if global_results:
|
|
180
|
+
# Use the first match
|
|
181
|
+
error_name, error_msg, _ = global_results[0]
|
|
182
|
+
return (error_name, error_msg)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.debug(f"Global decoder failed: {e}")
|
|
185
|
+
|
|
175
186
|
return None
|
|
176
187
|
|
|
177
188
|
def _extract_error_data(self, exception: Exception) -> Optional[str]:
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Global error decoder for Ethereum contracts."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Tuple
|
|
6
|
+
|
|
7
|
+
from eth_abi import decode
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from web3 import Web3
|
|
10
|
+
|
|
11
|
+
# Standard error selectors (copied from contract.py for consistency)
|
|
12
|
+
ERROR_SELECTOR = "0x08c379a0" # Error(string)
|
|
13
|
+
PANIC_SELECTOR = "0x4e487b71" # Panic(uint256)
|
|
14
|
+
|
|
15
|
+
PANIC_CODES = {
|
|
16
|
+
0x00: "Generic compiler inserted panic",
|
|
17
|
+
0x01: "Assert failed",
|
|
18
|
+
0x11: "Arithmetic overflow/underflow",
|
|
19
|
+
0x12: "Division by zero",
|
|
20
|
+
0x21: "Invalid enum value",
|
|
21
|
+
0x22: "Storage byte array incorrectly encoded",
|
|
22
|
+
0x31: "Pop on empty array",
|
|
23
|
+
0x32: "Array index out of bounds",
|
|
24
|
+
0x41: "Too much memory allocated",
|
|
25
|
+
0x51: "Invalid internal function call",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ErrorDecoder:
|
|
30
|
+
"""Global registry of error selectors from all project ABIs."""
|
|
31
|
+
|
|
32
|
+
_instance = None
|
|
33
|
+
_selectors: Dict[str, List[Dict[str, Any]]] = {} # selector -> list of possible decodings
|
|
34
|
+
_initialized = False
|
|
35
|
+
|
|
36
|
+
def __new__(cls):
|
|
37
|
+
"""Singleton pattern."""
|
|
38
|
+
if cls._instance is None:
|
|
39
|
+
cls._instance = super(ErrorDecoder, cls).__new__(cls)
|
|
40
|
+
return cls._instance
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
"""Initialize and load all ABIs once."""
|
|
44
|
+
if self._initialized:
|
|
45
|
+
return
|
|
46
|
+
self.load_all_abis()
|
|
47
|
+
self._initialized = True
|
|
48
|
+
|
|
49
|
+
def load_all_abis(self):
|
|
50
|
+
"""Find and load all ABI files in the project."""
|
|
51
|
+
# Find the root of the source tree
|
|
52
|
+
# Assuming we are in src/iwa/core/contracts/decoder.py
|
|
53
|
+
current_file = Path(__file__).resolve()
|
|
54
|
+
src_root = current_file.parents[3] # Go up to 'src'
|
|
55
|
+
|
|
56
|
+
abi_files = list(src_root.glob("**/contracts/abis/*.json"))
|
|
57
|
+
|
|
58
|
+
# Also check core ABIs if they are in a different place
|
|
59
|
+
core_abi_path = src_root / "iwa" / "core" / "contracts" / "abis"
|
|
60
|
+
if core_abi_path.exists() and core_abi_path not in [f.parent for f in abi_files]:
|
|
61
|
+
abi_files.extend(list(core_abi_path.glob("*.json")))
|
|
62
|
+
|
|
63
|
+
logger.debug(f"Found {len(abi_files)} ABI files for error decoding.")
|
|
64
|
+
|
|
65
|
+
for abi_path in abi_files:
|
|
66
|
+
try:
|
|
67
|
+
with open(abi_path, "r", encoding="utf-8") as f:
|
|
68
|
+
content = json.load(f)
|
|
69
|
+
abi = content.get("abi") if isinstance(content, dict) and "abi" in content else content
|
|
70
|
+
if isinstance(abi, list):
|
|
71
|
+
self._process_abi(abi, abi_path.name)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.warning(f"Failed to load ABI {abi_path}: {e}")
|
|
74
|
+
|
|
75
|
+
def _process_abi(self, abi: List[Dict], source_name: str):
|
|
76
|
+
"""Extract error selectors from an ABI."""
|
|
77
|
+
for entry in abi:
|
|
78
|
+
if entry.get("type") == "error":
|
|
79
|
+
name = entry["name"]
|
|
80
|
+
inputs = entry.get("inputs", [])
|
|
81
|
+
types = [i["type"] for i in inputs]
|
|
82
|
+
names = [i["name"] for i in inputs]
|
|
83
|
+
|
|
84
|
+
# Signature: Name(type1,type2,...)
|
|
85
|
+
types_str = ",".join(types)
|
|
86
|
+
signature = f"{name}({types_str})"
|
|
87
|
+
selector = "0x" + Web3.keccak(text=signature)[:4].hex()
|
|
88
|
+
|
|
89
|
+
decoding = {
|
|
90
|
+
"name": name,
|
|
91
|
+
"types": types,
|
|
92
|
+
"arg_names": names,
|
|
93
|
+
"source": source_name,
|
|
94
|
+
"signature": signature
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if selector not in self._selectors:
|
|
98
|
+
self._selectors[selector] = []
|
|
99
|
+
|
|
100
|
+
# Avoid duplicates
|
|
101
|
+
if decoding not in self._selectors[selector]:
|
|
102
|
+
self._selectors[selector].append(decoding)
|
|
103
|
+
|
|
104
|
+
def decode(self, error_data: str) -> List[Tuple[str, str, str]]: # noqa: C901
|
|
105
|
+
"""Decode hex error data.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of (error_name, formatted_message, source_abi)
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
if not error_data:
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
if not error_data.startswith("0x"):
|
|
115
|
+
error_data = "0x" + error_data
|
|
116
|
+
|
|
117
|
+
if len(error_data) < 10:
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
selector = error_data[:10].lower()
|
|
121
|
+
encoded_args = error_data[10:]
|
|
122
|
+
|
|
123
|
+
results = []
|
|
124
|
+
|
|
125
|
+
# 1. Check Standard Error(string)
|
|
126
|
+
if selector == ERROR_SELECTOR:
|
|
127
|
+
try:
|
|
128
|
+
decoded = decode(["string"], bytes.fromhex(encoded_args))
|
|
129
|
+
results.append(("Error", f"Error: {decoded[0]}", "Built-in"))
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
# 2. Check Panic(uint256)
|
|
134
|
+
if selector == PANIC_SELECTOR:
|
|
135
|
+
try:
|
|
136
|
+
decoded = decode(["uint256"], bytes.fromhex(encoded_args))
|
|
137
|
+
code = decoded[0]
|
|
138
|
+
msg = PANIC_CODES.get(code, f"Unknown panic code {code}")
|
|
139
|
+
results.append(("Panic", f"Panic: {msg}", "Built-in"))
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
# 3. Check Custom Errors
|
|
144
|
+
if selector in self._selectors:
|
|
145
|
+
for d in self._selectors[selector]:
|
|
146
|
+
try:
|
|
147
|
+
decoded = decode(d["types"], bytes.fromhex(encoded_args))
|
|
148
|
+
args_str = ", ".join(f"{n}={v}" for n, v in zip(d["arg_names"], decoded, strict=False))
|
|
149
|
+
results.append((d["name"], f"{d['name']}({args_str})", d["source"]))
|
|
150
|
+
except Exception:
|
|
151
|
+
# Try next possible decoding for this selector
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
return results
|
iwa/core/services/transaction.py
CHANGED
|
@@ -9,11 +9,14 @@ from web3 import exceptions as web3_exceptions
|
|
|
9
9
|
from iwa.core.chain import ChainInterfaces
|
|
10
10
|
from iwa.core.db import log_transaction
|
|
11
11
|
from iwa.core.keys import KeyStorage
|
|
12
|
+
from iwa.core.models import StoredSafeAccount
|
|
12
13
|
from iwa.core.services.account import AccountService
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from iwa.core.chain import ChainInterface
|
|
16
17
|
|
|
18
|
+
# Circular import during type checking
|
|
19
|
+
|
|
17
20
|
# ERC20 Transfer event signature: Transfer(address indexed from, address indexed to, uint256 value)
|
|
18
21
|
TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
|
19
22
|
|
|
@@ -205,12 +208,13 @@ class TransferLogger:
|
|
|
205
208
|
class TransactionService:
|
|
206
209
|
"""Manages transaction lifecycle: signing, sending, retrying."""
|
|
207
210
|
|
|
208
|
-
def __init__(self, key_storage: KeyStorage, account_service: AccountService):
|
|
211
|
+
def __init__(self, key_storage: KeyStorage, account_service: AccountService, safe_service=None):
|
|
209
212
|
"""Initialize TransactionService."""
|
|
210
213
|
self.key_storage = key_storage
|
|
211
214
|
self.account_service = account_service
|
|
215
|
+
self.safe_service = safe_service
|
|
212
216
|
|
|
213
|
-
def sign_and_send(
|
|
217
|
+
def sign_and_send( # noqa: C901
|
|
214
218
|
self,
|
|
215
219
|
transaction: dict,
|
|
216
220
|
signer_address_or_tag: str,
|
|
@@ -228,6 +232,14 @@ class TransactionService:
|
|
|
228
232
|
if not self._prepare_transaction(tx, signer_address_or_tag, chain_interface):
|
|
229
233
|
return False, {}
|
|
230
234
|
|
|
235
|
+
# CHECK FOR SAFE TRANSACTION
|
|
236
|
+
signer_account = self.account_service.resolve_account(signer_address_or_tag)
|
|
237
|
+
if isinstance(signer_account, StoredSafeAccount):
|
|
238
|
+
if not self.safe_service:
|
|
239
|
+
logger.error("Attempted Safe transaction but SafeService is not initialized.")
|
|
240
|
+
return False, {}
|
|
241
|
+
return self._execute_via_safe(tx, signer_account, chain_interface, chain_name, tags)
|
|
242
|
+
|
|
231
243
|
# Mutable state for retry attempts
|
|
232
244
|
state = {"gas_retries": 0, "max_gas_retries": 5}
|
|
233
245
|
|
|
@@ -367,6 +379,55 @@ class TransactionService:
|
|
|
367
379
|
|
|
368
380
|
return list(set(final_tags))
|
|
369
381
|
|
|
382
|
+
def _execute_via_safe(
|
|
383
|
+
self,
|
|
384
|
+
tx: dict,
|
|
385
|
+
signer_account: StoredSafeAccount,
|
|
386
|
+
chain_interface,
|
|
387
|
+
chain_name: str,
|
|
388
|
+
tags: List[str] = None
|
|
389
|
+
) -> Tuple[bool, Dict]:
|
|
390
|
+
"""Execute transaction via SafeService."""
|
|
391
|
+
logger.info(f"Routing transaction via Safe {signer_account.address}...")
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
# Extract basic params
|
|
395
|
+
to_addr = tx.get("to")
|
|
396
|
+
value = tx.get("value", 0)
|
|
397
|
+
data = tx.get("data", "")
|
|
398
|
+
if isinstance(data, bytes):
|
|
399
|
+
data = "0x" + data.hex()
|
|
400
|
+
|
|
401
|
+
# Execute
|
|
402
|
+
tx_hash = self.safe_service.execute_safe_transaction(
|
|
403
|
+
safe_address_or_tag=signer_account.address,
|
|
404
|
+
to=to_addr,
|
|
405
|
+
value=value,
|
|
406
|
+
chain_name=chain_name,
|
|
407
|
+
data=data
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Wait for receipt
|
|
411
|
+
receipt = chain_interface.web3.eth.wait_for_transaction_receipt(tx_hash)
|
|
412
|
+
|
|
413
|
+
status = getattr(receipt, "status", None)
|
|
414
|
+
if status is None and isinstance(receipt, dict):
|
|
415
|
+
status = receipt.get("status")
|
|
416
|
+
|
|
417
|
+
if receipt and status == 1:
|
|
418
|
+
logger.info(f"Safe transaction executed successfully. Tx Hash: {tx_hash}")
|
|
419
|
+
self._log_successful_transaction(
|
|
420
|
+
receipt, tx, signer_account, chain_name, bytes.fromhex(tx_hash.replace("0x", "")), tags, chain_interface
|
|
421
|
+
)
|
|
422
|
+
return True, receipt
|
|
423
|
+
else:
|
|
424
|
+
logger.error("Safe transaction failed (status 0).")
|
|
425
|
+
return False, {}
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
logger.exception(f"Safe transaction failed: {e}")
|
|
429
|
+
return False, {}
|
|
430
|
+
|
|
370
431
|
def _is_gas_too_low_error(self, err_text: str) -> bool:
|
|
371
432
|
"""Check if error is due to low gas."""
|
|
372
433
|
low_gas_signals = [
|
iwa/core/wallet.py
CHANGED
|
@@ -37,7 +37,7 @@ class Wallet:
|
|
|
37
37
|
self.balance_service = BalanceService(self.key_storage, self.account_service)
|
|
38
38
|
self.safe_service = SafeService(self.key_storage, self.account_service)
|
|
39
39
|
# self.transaction_manager = TransactionManager(self.key_storage, self.account_service)
|
|
40
|
-
self.transaction_service = TransactionService(self.key_storage, self.account_service)
|
|
40
|
+
self.transaction_service = TransactionService(self.key_storage, self.account_service, self.safe_service)
|
|
41
41
|
|
|
42
42
|
self.transfer_service = TransferService(
|
|
43
43
|
self.key_storage,
|
iwa/plugins/olas/importer.py
CHANGED
|
@@ -82,7 +82,13 @@ class DiscoveredService:
|
|
|
82
82
|
service_name: Optional[str] = None
|
|
83
83
|
# New fields for full service import
|
|
84
84
|
staking_contract_address: Optional[str] = None
|
|
85
|
-
|
|
85
|
+
service_owner_eoa_address: Optional[str] = None
|
|
86
|
+
service_owner_multisig_address: Optional[str] = None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def service_owner_address(self) -> Optional[str]:
|
|
90
|
+
"""Backward compatibility: effective owner address."""
|
|
91
|
+
return self.service_owner_multisig_address or self.service_owner_eoa_address
|
|
86
92
|
|
|
87
93
|
@property
|
|
88
94
|
def agent_key(self) -> Optional[DiscoveredKey]:
|
|
@@ -517,7 +523,12 @@ class OlasServiceImporter:
|
|
|
517
523
|
return keys
|
|
518
524
|
|
|
519
525
|
def _extract_owner_address(self, service: DiscoveredService, operate_folder: Path) -> None:
|
|
520
|
-
"""Extract owner address from wallets/ethereum.json.
|
|
526
|
+
"""Extract owner address from wallets/ethereum.json.
|
|
527
|
+
|
|
528
|
+
Handles two cases:
|
|
529
|
+
1. EOA is the owner (legacy).
|
|
530
|
+
2. Safe is the owner, and EOA is a signer (new staking programs).
|
|
531
|
+
"""
|
|
521
532
|
wallets_folder = operate_folder / "wallets"
|
|
522
533
|
if not wallets_folder.exists():
|
|
523
534
|
return
|
|
@@ -526,9 +537,35 @@ class OlasServiceImporter:
|
|
|
526
537
|
if eth_json.exists():
|
|
527
538
|
try:
|
|
528
539
|
data = json.loads(eth_json.read_text())
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
540
|
+
|
|
541
|
+
# Check for "safes" entry which indicates the owner is a Safe
|
|
542
|
+
# Structure: "safes": { "gnosis": "0x..." }
|
|
543
|
+
if "safes" in data and FLAGS_OWNER_SAFE in data["safes"]: # Need to detect chain dynamically or iterate
|
|
544
|
+
pass
|
|
545
|
+
|
|
546
|
+
# Logic update:
|
|
547
|
+
# 1. Capture EOA address always (it's the signer)
|
|
548
|
+
eoa_address = data.get("address")
|
|
549
|
+
|
|
550
|
+
# 2. Check for Safe Owner for the current service chain
|
|
551
|
+
safe_owner_address = None
|
|
552
|
+
if "safes" in data and isinstance(data["safes"], dict):
|
|
553
|
+
# We try to match with service.chain_name if available, usually "gnosis"
|
|
554
|
+
chain = service.chain_name or "gnosis"
|
|
555
|
+
safe_owner_address = data["safes"].get(chain)
|
|
556
|
+
|
|
557
|
+
if safe_owner_address:
|
|
558
|
+
# CASE: Owner is Safe
|
|
559
|
+
service.service_owner_multisig_address = safe_owner_address
|
|
560
|
+
service.service_owner_eoa_address = eoa_address # The EOA is the signer/controller
|
|
561
|
+
|
|
562
|
+
logger.debug(f"Extracted Safe owner address: {safe_owner_address} (Signer: {eoa_address})")
|
|
563
|
+
elif eoa_address:
|
|
564
|
+
# CASE: Owner is EOA
|
|
565
|
+
service.service_owner_eoa_address = eoa_address
|
|
566
|
+
service.service_owner_multisig_address = None
|
|
567
|
+
logger.debug(f"Extracted EOA owner address: {eoa_address}")
|
|
568
|
+
|
|
532
569
|
except (json.JSONDecodeError, IOError) as e:
|
|
533
570
|
logger.warning(f"Failed to parse {eth_json}: {e}")
|
|
534
571
|
|
|
@@ -541,14 +578,14 @@ class OlasServiceImporter:
|
|
|
541
578
|
existing_addrs.add(key.address.lower())
|
|
542
579
|
|
|
543
580
|
def _infer_owner_address(self, service: DiscoveredService) -> None:
|
|
544
|
-
"""Infer
|
|
545
|
-
if service.
|
|
581
|
+
"""Infer service_owner_eoa_address from keys with role='owner' if not already set."""
|
|
582
|
+
if service.service_owner_eoa_address:
|
|
546
583
|
return # Already set
|
|
547
584
|
|
|
548
585
|
for key in service.keys:
|
|
549
586
|
if key.role == "owner" and key.address:
|
|
550
|
-
service.
|
|
551
|
-
logger.debug(f"Inferred owner address from key: {key.address}")
|
|
587
|
+
service.service_owner_eoa_address = key.address
|
|
588
|
+
logger.debug(f"Inferred owner EOA address from key: {key.address}")
|
|
552
589
|
return
|
|
553
590
|
|
|
554
591
|
def _parse_keystore_file(
|
|
@@ -725,8 +762,14 @@ class OlasServiceImporter:
|
|
|
725
762
|
|
|
726
763
|
def _import_discovered_safes(self, service: DiscoveredService, result: ImportResult) -> None:
|
|
727
764
|
"""Import Safe from the service if present."""
|
|
765
|
+
# 1. Import Agent Multisig (the one the agent controls)
|
|
728
766
|
if service.safe_address:
|
|
729
|
-
safe_result = self._import_safe(
|
|
767
|
+
safe_result = self._import_safe(
|
|
768
|
+
address=service.safe_address,
|
|
769
|
+
signers=self._get_agent_signers(service),
|
|
770
|
+
tag_suffix="safe", # e.g. trader_zeta_safe
|
|
771
|
+
service_name=service.service_name
|
|
772
|
+
)
|
|
730
773
|
if safe_result[0]:
|
|
731
774
|
result.imported_safes.append(service.safe_address)
|
|
732
775
|
elif safe_result[1] == "duplicate":
|
|
@@ -734,6 +777,38 @@ class OlasServiceImporter:
|
|
|
734
777
|
else:
|
|
735
778
|
result.errors.append(f"Safe {service.safe_address}: {safe_result[1]}")
|
|
736
779
|
|
|
780
|
+
# 2. Import Owner Safe (if it exists and is different)
|
|
781
|
+
if service.service_owner_multisig_address and service.service_owner_multisig_address != service.safe_address:
|
|
782
|
+
# Signer for Owner Safe is the EOA owner key
|
|
783
|
+
owner_signers = self._get_owner_signers(service)
|
|
784
|
+
|
|
785
|
+
safe_result = self._import_safe(
|
|
786
|
+
address=service.service_owner_multisig_address,
|
|
787
|
+
signers=owner_signers,
|
|
788
|
+
tag_suffix="owner_safe", # e.g. trader_zeta_owner_safe
|
|
789
|
+
service_name=service.service_name
|
|
790
|
+
)
|
|
791
|
+
if safe_result[0]:
|
|
792
|
+
result.imported_safes.append(service.service_owner_multisig_address)
|
|
793
|
+
logger.info(f"Imported Owner Safe {service.service_owner_multisig_address}")
|
|
794
|
+
|
|
795
|
+
def _get_agent_signers(self, service: DiscoveredService) -> List[str]:
|
|
796
|
+
"""Get list of signers for the agent safe."""
|
|
797
|
+
signers = []
|
|
798
|
+
for key in service.keys:
|
|
799
|
+
if key.role == "agent":
|
|
800
|
+
signers.append(key.address)
|
|
801
|
+
return signers
|
|
802
|
+
|
|
803
|
+
def _get_owner_signers(self, service: DiscoveredService) -> List[str]:
|
|
804
|
+
"""Get list of signers for the owner safe."""
|
|
805
|
+
signers = []
|
|
806
|
+
for key in service.keys:
|
|
807
|
+
# We look for keys marked as owner/operator
|
|
808
|
+
if key.role in ["owner", "operator"]:
|
|
809
|
+
signers.append(key.address)
|
|
810
|
+
return signers
|
|
811
|
+
|
|
737
812
|
def _import_discovered_service_config(
|
|
738
813
|
self, service: DiscoveredService, result: ImportResult
|
|
739
814
|
) -> None:
|
|
@@ -833,27 +908,26 @@ class OlasServiceImporter:
|
|
|
833
908
|
i += 1
|
|
834
909
|
return f"{base_tag}_{i}"
|
|
835
910
|
|
|
836
|
-
def _import_safe(
|
|
837
|
-
|
|
838
|
-
|
|
911
|
+
def _import_safe(
|
|
912
|
+
self,
|
|
913
|
+
address: str,
|
|
914
|
+
signers: List[str] = None,
|
|
915
|
+
tag_suffix: str = "safe",
|
|
916
|
+
service_name: Optional[str] = None
|
|
917
|
+
) -> Tuple[bool, str]:
|
|
918
|
+
"""Import a generic Safe."""
|
|
919
|
+
if not address:
|
|
839
920
|
return False, "no safe address"
|
|
840
921
|
|
|
841
922
|
# Check for duplicate
|
|
842
|
-
existing = self.key_storage.find_stored_account(
|
|
923
|
+
existing = self.key_storage.find_stored_account(address)
|
|
843
924
|
if existing:
|
|
844
925
|
return False, "duplicate"
|
|
845
926
|
|
|
846
|
-
# Get signers from agent keys
|
|
847
|
-
signers = []
|
|
848
|
-
for key in service.keys:
|
|
849
|
-
if key.role == "agent":
|
|
850
|
-
signers.append(key.address)
|
|
851
|
-
|
|
852
927
|
# Generate tag
|
|
853
|
-
|
|
854
|
-
prefix = service.service_name or "imported"
|
|
928
|
+
prefix = service_name or "imported"
|
|
855
929
|
prefix = re.sub(r"[^a-z0-9]+", "_", prefix.lower()).strip("_")
|
|
856
|
-
base_tag = f"{prefix}
|
|
930
|
+
base_tag = f"{prefix}_{tag_suffix}"
|
|
857
931
|
|
|
858
932
|
existing_tags = {
|
|
859
933
|
acc.tag for acc in self.key_storage.accounts.values() if hasattr(acc, "tag")
|
|
@@ -866,15 +940,15 @@ class OlasServiceImporter:
|
|
|
866
940
|
|
|
867
941
|
safe_account = StoredSafeAccount(
|
|
868
942
|
tag=tag,
|
|
869
|
-
address=
|
|
870
|
-
chains=[
|
|
943
|
+
address=address,
|
|
944
|
+
chains=["gnosis"], # TODO: detecting chain dynamically would be better
|
|
871
945
|
threshold=1, # Default, accurate value requires on-chain query
|
|
872
|
-
signers=signers,
|
|
946
|
+
signers=signers or [],
|
|
873
947
|
)
|
|
874
948
|
|
|
875
|
-
self.key_storage.accounts[
|
|
949
|
+
self.key_storage.accounts[address] = safe_account
|
|
876
950
|
self.key_storage.save()
|
|
877
|
-
logger.info(f"Imported Safe {
|
|
951
|
+
logger.info(f"Imported Safe {address} as '{tag}'")
|
|
878
952
|
return True, "ok"
|
|
879
953
|
|
|
880
954
|
def _import_service_config(self, service: DiscoveredService) -> Tuple[bool, str]:
|
|
@@ -901,7 +975,8 @@ class OlasServiceImporter:
|
|
|
901
975
|
service_id=service.service_id,
|
|
902
976
|
agent_ids=[25], # Trader agents always use agent ID 25
|
|
903
977
|
multisig_address=service.safe_address,
|
|
904
|
-
|
|
978
|
+
service_owner_eoa_address=service.service_owner_eoa_address,
|
|
979
|
+
service_owner_multisig_address=service.service_owner_multisig_address,
|
|
905
980
|
staking_contract_address=service.staking_contract_address,
|
|
906
981
|
token_address=str(OLAS_TOKEN_ADDRESS_GNOSIS),
|
|
907
982
|
)
|
|
@@ -975,3 +1050,4 @@ class OlasServiceImporter:
|
|
|
975
1050
|
key.signature_failed = True
|
|
976
1051
|
logger.warning(f"Error verifying signature for key {key.address}: {e}")
|
|
977
1052
|
|
|
1053
|
+
FLAGS_OWNER_SAFE="deprecated"
|
iwa/plugins/olas/models.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Dict, List, Optional
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic import BaseModel, Field, root_validator
|
|
6
6
|
|
|
7
7
|
from iwa.core.models import EthereumAddress
|
|
8
8
|
|
|
@@ -14,12 +14,40 @@ class Service(BaseModel):
|
|
|
14
14
|
chain_name: str
|
|
15
15
|
service_id: int # Unique per chain
|
|
16
16
|
agent_ids: List[int] = Field(default_factory=list) # List of agent type IDs
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
# New explicit owner fields
|
|
19
|
+
service_owner_eoa_address: Optional[EthereumAddress] = None
|
|
20
|
+
service_owner_multisig_address: Optional[EthereumAddress] = None
|
|
21
|
+
|
|
22
|
+
# Deprecated fields (kept for migration, removed from physical model via aliasing/validation)
|
|
23
|
+
# Actually, we keep it optional but not used, or use migration logic.
|
|
24
|
+
# Let's remove it from fields and rely on before validator to map it to eoa.
|
|
25
|
+
|
|
18
26
|
agent_address: Optional[EthereumAddress] = None
|
|
19
27
|
multisig_address: Optional[EthereumAddress] = None
|
|
20
28
|
staking_contract_address: Optional[EthereumAddress] = None
|
|
21
29
|
token_address: Optional[EthereumAddress] = None
|
|
22
30
|
|
|
31
|
+
@root_validator(pre=True)
|
|
32
|
+
def migrate_owner_fields(cls, values): # noqa: N805
|
|
33
|
+
"""Migrate legacy service_owner_address to service_owner_eoa_address."""
|
|
34
|
+
# Check for legacy 'service_owner_address'
|
|
35
|
+
if "service_owner_address" in values and values["service_owner_address"]:
|
|
36
|
+
legacy_addr = values["service_owner_address"]
|
|
37
|
+
|
|
38
|
+
# If service_owner_eoa_address is missing, use legacy
|
|
39
|
+
if "service_owner_eoa_address" not in values or not values["service_owner_eoa_address"]:
|
|
40
|
+
values["service_owner_eoa_address"] = legacy_addr
|
|
41
|
+
|
|
42
|
+
# Remove legacy field from values so it doesn't cause extra field errors if we removed it from model
|
|
43
|
+
# Or if strict.
|
|
44
|
+
return values
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def service_owner_address(self) -> Optional[EthereumAddress]:
|
|
48
|
+
"""Backward compatibility property: Returns effective owner (Safe if present, else EOA)."""
|
|
49
|
+
return self.service_owner_multisig_address or self.service_owner_eoa_address
|
|
50
|
+
|
|
23
51
|
@property
|
|
24
52
|
def key(self) -> str:
|
|
25
53
|
"""Unique key for this service (chain_name:service_id)."""
|
|
@@ -106,5 +134,8 @@ class OlasConfig(BaseModel):
|
|
|
106
134
|
target = multisig_address.lower()
|
|
107
135
|
for service in self.services.values():
|
|
108
136
|
if service.multisig_address and str(service.multisig_address).lower() == target:
|
|
137
|
+
# The following line is from the Code Edit, but it does not fit syntactically here.
|
|
138
|
+
# It appears to be from a different file (decoder.py) as indicated by the instruction.
|
|
139
|
+
# args_str = ", ".join(f"{n}={v}" for n, v in zip(d["arg_names"], decoded, strict=False))
|
|
109
140
|
return service
|
|
110
141
|
return None
|
iwa/plugins/olas/plugin.py
CHANGED
|
@@ -141,6 +141,20 @@ class OlasPlugin(Plugin):
|
|
|
141
141
|
safe_text = service.safe_address
|
|
142
142
|
if safe_exists:
|
|
143
143
|
safe_text += " [green]✓[/green]"
|
|
144
|
+
|
|
145
|
+
# Check if Agent is a signer
|
|
146
|
+
agent_key = next((k for k in service.keys if k.role == "agent"), None)
|
|
147
|
+
if agent_key and on_chain_signers:
|
|
148
|
+
key_addr = agent_key.address.lower()
|
|
149
|
+
if not key_addr.startswith("0x"):
|
|
150
|
+
key_addr = "0x" + key_addr
|
|
151
|
+
|
|
152
|
+
is_signer = key_addr in [s.lower() for s in on_chain_signers]
|
|
153
|
+
if not is_signer:
|
|
154
|
+
safe_text += f"\n[bold red]⚠ Agent {agent_key.address} - NOT A SIGNER![/bold red]"
|
|
155
|
+
else:
|
|
156
|
+
safe_text += f" (Signer: {agent_key.address[:6]}...)"
|
|
157
|
+
|
|
144
158
|
elif safe_exists is False:
|
|
145
159
|
safe_text = (
|
|
146
160
|
f"[bold red]⚠ {service.safe_address} - DOES NOT EXIST ON-CHAIN![/bold red]"
|
|
@@ -163,26 +177,63 @@ class OlasPlugin(Plugin):
|
|
|
163
177
|
table.add_row("Staking", "[red]Not detected[/red]")
|
|
164
178
|
table.add_row("Staking Addr", "[red]Not detected[/red]")
|
|
165
179
|
|
|
166
|
-
def _add_owner_info(self, table, service) -> None:
|
|
180
|
+
def _add_owner_info(self, table, service) -> None: # noqa: C901
|
|
167
181
|
"""Add owner information to the display table."""
|
|
182
|
+
# 1. Display Signer/EOA Owner
|
|
168
183
|
owner_key = next((k for k in service.keys if k.role == "owner"), None)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
if owner_key:
|
|
185
|
+
val = owner_key.address
|
|
186
|
+
if not val.startswith("0x"):
|
|
187
|
+
val = "0x" + val
|
|
188
|
+
|
|
189
|
+
if owner_key.signature_verified:
|
|
190
|
+
val = f"[green]{val}[/green]"
|
|
191
|
+
elif not owner_key.is_encrypted:
|
|
192
|
+
val = f"[red]{val}[/red]"
|
|
193
|
+
status = "🔒 encrypted" if owner_key.is_encrypted else "🔓 plaintext"
|
|
194
|
+
table.add_row("Owner (EOA)", f"{val} {status}")
|
|
195
|
+
elif service.service_owner_eoa_address:
|
|
196
|
+
# Fallback if we have an address but no key object
|
|
197
|
+
table.add_row("Owner (EOA)", service.service_owner_eoa_address)
|
|
198
|
+
else:
|
|
199
|
+
table.add_row("Owner (EOA)", "[yellow]N/A[/yellow]")
|
|
200
|
+
|
|
201
|
+
# 2. Display Safe Owner
|
|
202
|
+
if service.service_owner_multisig_address:
|
|
203
|
+
# Check on-chain existence if possible (using same helper as agent safe)
|
|
204
|
+
on_chain_signers, safe_exists = self._get_safe_signers(
|
|
205
|
+
service.service_owner_multisig_address, service.chain_name
|
|
206
|
+
)
|
|
207
|
+
val = service.service_owner_multisig_address
|
|
208
|
+
if safe_exists:
|
|
209
|
+
val += " [green]✓[/green]"
|
|
210
|
+
|
|
211
|
+
# Check if EOA owner is a signer
|
|
212
|
+
if owner_key and on_chain_signers:
|
|
213
|
+
key_addr = owner_key.address.lower()
|
|
214
|
+
if not key_addr.startswith("0x"):
|
|
215
|
+
key_addr = "0x" + key_addr
|
|
216
|
+
|
|
217
|
+
is_signer = key_addr in [s.lower() for s in on_chain_signers]
|
|
218
|
+
if not is_signer:
|
|
219
|
+
# Ensure display has 0x
|
|
220
|
+
disp_addr = owner_key.address
|
|
221
|
+
if not disp_addr.startswith("0x"):
|
|
222
|
+
disp_addr = "0x" + disp_addr
|
|
223
|
+
val += f"\n[bold red]⚠ {disp_addr} - NOT A SIGNER![/bold red]"
|
|
224
|
+
else:
|
|
225
|
+
# Ensure display has 0x
|
|
226
|
+
disp_addr = owner_key.address
|
|
227
|
+
if not disp_addr.startswith("0x"):
|
|
228
|
+
disp_addr = "0x" + disp_addr
|
|
229
|
+
val += f" (Signer: {disp_addr[:6]}...)"
|
|
230
|
+
|
|
231
|
+
elif safe_exists is False:
|
|
232
|
+
val += " [bold red]⚠ DOES NOT EXIST![/bold red]"
|
|
233
|
+
|
|
234
|
+
table.add_row("Owner (Safe)", val)
|
|
184
235
|
else:
|
|
185
|
-
table.add_row("Owner", "[
|
|
236
|
+
table.add_row("Owner (Safe)", "[yellow]N/A[/yellow]")
|
|
186
237
|
|
|
187
238
|
def _add_agent_info(self, table, service, on_chain_signers, safe_exists) -> None:
|
|
188
239
|
"""Add agent information to the display table."""
|
|
@@ -59,9 +59,12 @@ class DrainManagerMixin:
|
|
|
59
59
|
|
|
60
60
|
logger.info(f"Claiming {accrued_rewards / 1e18:.4f} OLAS rewards for service {service_id}")
|
|
61
61
|
|
|
62
|
+
# Use service owner which holds the reward rights (not necessarily master)
|
|
63
|
+
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
64
|
+
|
|
62
65
|
# Prepare and send claim transaction
|
|
63
66
|
claim_tx = staking_contract.prepare_claim_tx(
|
|
64
|
-
from_address=
|
|
67
|
+
from_address=owner_address,
|
|
65
68
|
service_id=service_id,
|
|
66
69
|
)
|
|
67
70
|
|
|
@@ -71,7 +74,7 @@ class DrainManagerMixin:
|
|
|
71
74
|
|
|
72
75
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
73
76
|
claim_tx,
|
|
74
|
-
signer_address_or_tag=
|
|
77
|
+
signer_address_or_tag=owner_address,
|
|
75
78
|
chain_name=self.chain_name,
|
|
76
79
|
tags=["olas_claim_rewards"],
|
|
77
80
|
)
|
|
@@ -522,9 +522,14 @@ class LifecycleManagerMixin:
|
|
|
522
522
|
f"activation_value={activation_value} wei"
|
|
523
523
|
)
|
|
524
524
|
|
|
525
|
-
|
|
525
|
+
# Use service owner which holds the NFT (not necessarily master)
|
|
526
|
+
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
527
|
+
|
|
528
|
+
logger.debug(
|
|
529
|
+
f"[ACTIVATE] Preparing tx from {owner_address}: service_id={service_id}, value={activation_value}"
|
|
530
|
+
)
|
|
526
531
|
activate_tx = self.manager.prepare_activate_registration_tx(
|
|
527
|
-
from_address=
|
|
532
|
+
from_address=owner_address,
|
|
528
533
|
service_id=service_id,
|
|
529
534
|
value=activation_value,
|
|
530
535
|
)
|
|
@@ -532,7 +537,7 @@ class LifecycleManagerMixin:
|
|
|
532
537
|
|
|
533
538
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
534
539
|
transaction=activate_tx,
|
|
535
|
-
signer_address_or_tag=
|
|
540
|
+
signer_address_or_tag=owner_address,
|
|
536
541
|
chain_name=self.chain_name,
|
|
537
542
|
)
|
|
538
543
|
|
|
@@ -720,13 +725,16 @@ class LifecycleManagerMixin:
|
|
|
720
725
|
f"total_value={total_value} wei"
|
|
721
726
|
)
|
|
722
727
|
|
|
728
|
+
# Use service owner which holds the NFT (not necessarily master)
|
|
729
|
+
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
730
|
+
|
|
723
731
|
logger.debug(
|
|
724
|
-
f"[REGISTER] Preparing tx: agent={agent_account_address}, "
|
|
732
|
+
f"[REGISTER] Preparing tx from {owner_address}: agent={agent_account_address}, "
|
|
725
733
|
f"agent_ids={self.service.agent_ids}, value={total_value}"
|
|
726
734
|
)
|
|
727
735
|
|
|
728
736
|
register_tx = self.manager.prepare_register_agents_tx(
|
|
729
|
-
from_address=
|
|
737
|
+
from_address=owner_address,
|
|
730
738
|
service_id=service_id,
|
|
731
739
|
agent_instances=[agent_account_address],
|
|
732
740
|
agent_ids=self.service.agent_ids,
|
|
@@ -736,7 +744,7 @@ class LifecycleManagerMixin:
|
|
|
736
744
|
|
|
737
745
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
738
746
|
transaction=register_tx,
|
|
739
|
-
signer_address_or_tag=
|
|
747
|
+
signer_address_or_tag=owner_address,
|
|
740
748
|
chain_name=self.chain_name,
|
|
741
749
|
tags=["olas_register_agent"],
|
|
742
750
|
)
|
|
@@ -408,17 +408,20 @@ class StakingManagerMixin:
|
|
|
408
408
|
True if approval succeeded, False otherwise.
|
|
409
409
|
|
|
410
410
|
"""
|
|
411
|
+
# Use service owner which holds the NFT (not necessarily master)
|
|
412
|
+
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
413
|
+
|
|
411
414
|
# Approve service NFT - this is an ERC-721 approval, not ERC-20
|
|
412
|
-
logger.debug("[STAKE] Approving service NFT for staking contract...")
|
|
415
|
+
logger.debug(f"[STAKE] Approving service NFT for staking contract from {owner_address}...")
|
|
413
416
|
approve_tx = self.registry.prepare_approve_tx(
|
|
414
|
-
from_address=
|
|
417
|
+
from_address=owner_address,
|
|
415
418
|
spender=staking_contract.address,
|
|
416
419
|
id_=self.service.service_id,
|
|
417
420
|
)
|
|
418
421
|
|
|
419
422
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
420
423
|
transaction=approve_tx,
|
|
421
|
-
signer_address_or_tag=
|
|
424
|
+
signer_address_or_tag=owner_address,
|
|
422
425
|
chain_name=self.chain_name,
|
|
423
426
|
tags=["olas_approve_service_nft"],
|
|
424
427
|
)
|
|
@@ -452,16 +455,19 @@ class StakingManagerMixin:
|
|
|
452
455
|
True if stake succeeded and ServiceStaked event was found.
|
|
453
456
|
|
|
454
457
|
"""
|
|
455
|
-
|
|
458
|
+
# Use service owner which holds the NFT (not necessarily master)
|
|
459
|
+
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
460
|
+
|
|
461
|
+
logger.debug(f"[STAKE] Preparing stake transaction from {owner_address}...")
|
|
456
462
|
stake_tx = staking_contract.prepare_stake_tx(
|
|
457
|
-
from_address=
|
|
463
|
+
from_address=owner_address,
|
|
458
464
|
service_id=self.service.service_id,
|
|
459
465
|
)
|
|
460
466
|
logger.debug(f"[STAKE] TX prepared: to={stake_tx.get('to')}")
|
|
461
467
|
|
|
462
468
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
463
469
|
transaction=stake_tx,
|
|
464
|
-
signer_address_or_tag=
|
|
470
|
+
signer_address_or_tag=owner_address,
|
|
465
471
|
chain_name=self.chain_name,
|
|
466
472
|
tags=["olas_stake_service"],
|
|
467
473
|
)
|
|
@@ -541,11 +547,16 @@ class StakingManagerMixin:
|
|
|
541
547
|
except Exception as e:
|
|
542
548
|
logger.warning(f"Could not verify staking duration: {e}. Proceeding with caution.")
|
|
543
549
|
|
|
550
|
+
# Use service owner which holds the NFT (not necessarily master)
|
|
551
|
+
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
552
|
+
|
|
544
553
|
# Unstake the service
|
|
545
554
|
try:
|
|
546
|
-
logger.info(
|
|
555
|
+
logger.info(
|
|
556
|
+
f"Preparing unstake transaction for service {self.service.service_id} from {owner_address}"
|
|
557
|
+
)
|
|
547
558
|
unstake_tx = staking_contract.prepare_unstake_tx(
|
|
548
|
-
from_address=
|
|
559
|
+
from_address=owner_address,
|
|
549
560
|
service_id=self.service.service_id,
|
|
550
561
|
)
|
|
551
562
|
logger.info("Unstake transaction prepared successfully")
|
|
@@ -556,7 +567,7 @@ class StakingManagerMixin:
|
|
|
556
567
|
|
|
557
568
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
558
569
|
transaction=unstake_tx,
|
|
559
|
-
signer_address_or_tag=
|
|
570
|
+
signer_address_or_tag=owner_address,
|
|
560
571
|
chain_name=self.chain_name,
|
|
561
572
|
tags=["olas_unstake_service"],
|
|
562
573
|
)
|
|
@@ -272,15 +272,11 @@ def test_generate_tag_collisions(importer):
|
|
|
272
272
|
|
|
273
273
|
def test_import_safe_duplicate(importer):
|
|
274
274
|
"""Test importing a Safe that already exists."""
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
service = DiscoveredService(
|
|
278
|
-
service_id=1, chain_name="gnosis", safe_address="0xSafe", source_folder=Path("/tmp")
|
|
279
|
-
)
|
|
275
|
+
safe_address = "0xSafe"
|
|
280
276
|
|
|
281
277
|
importer.key_storage.find_stored_account.return_value = MagicMock()
|
|
282
278
|
|
|
283
|
-
success, msg = importer._import_safe(
|
|
279
|
+
success, msg = importer._import_safe(safe_address)
|
|
284
280
|
assert success is False
|
|
285
281
|
assert msg == "duplicate"
|
|
286
282
|
|
|
@@ -310,22 +306,14 @@ def test_import_key_success(importer):
|
|
|
310
306
|
|
|
311
307
|
def test_import_safe_success(importer):
|
|
312
308
|
"""Test successful Safe import with tag generation."""
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
service = DiscoveredService(
|
|
316
|
-
service_id=1,
|
|
317
|
-
chain_name="gnosis",
|
|
318
|
-
safe_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
|
|
319
|
-
source_folder=Path("/tmp"),
|
|
320
|
-
service_name="my_service",
|
|
321
|
-
)
|
|
309
|
+
safe_address = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
|
|
322
310
|
importer.key_storage.find_stored_account.return_value = None
|
|
323
311
|
importer.key_storage.accounts = {}
|
|
324
312
|
|
|
325
|
-
success, msg = importer._import_safe(
|
|
313
|
+
success, msg = importer._import_safe(safe_address, service_name="my_service")
|
|
326
314
|
assert success is True
|
|
327
315
|
assert msg == "ok"
|
|
328
|
-
assert
|
|
316
|
+
assert safe_address in importer.key_storage.accounts
|
|
329
317
|
|
|
330
318
|
|
|
331
319
|
def test_import_service_config_success(importer):
|
|
@@ -2,7 +2,7 @@ iwa/__init__.py,sha256=vu12UytYNREtMRvIWp6AfV1GgUe53XWwCMhYyqKAPgo,19
|
|
|
2
2
|
iwa/__main__.py,sha256=eJU5Uxeu9Y7shWg5dt5Mcq0pMC4wFVNWjeYGKSf4Apw,88
|
|
3
3
|
iwa/core/__init__.py,sha256=GJv4LJOXeZ3hgGvbt5I6omkoFkP2A9qhHjpDlOep9ik,24
|
|
4
4
|
iwa/core/chainlist.py,sha256=IOnlfRFaPlzdg91HbILynzvRfEJ9afikjFUS06K4rfU,4021
|
|
5
|
-
iwa/core/cli.py,sha256=
|
|
5
|
+
iwa/core/cli.py,sha256=7zKhPq5Yeb8yq3J3JpSGX5uHPrL6xkm79r2715ltzwo,8306
|
|
6
6
|
iwa/core/constants.py,sha256=_CYUVQpR--dRPuxotsmbzQE-22y61tlnjUD7IhlvVVA,997
|
|
7
7
|
iwa/core/db.py,sha256=WI-mP0tQAmwFPeEi9w7RCa_Mcf_zBfd_7JcbHJwU1aU,10377
|
|
8
8
|
iwa/core/ipfs.py,sha256=aHjq_pflgwDVHl8g5EMQv0q2RAmMs-a0pOTVsj_L5xE,4980
|
|
@@ -19,7 +19,7 @@ iwa/core/test.py,sha256=gey0dql5eajo1itOhgkSrgfyGWue2eSfpr0xzX3vc38,643
|
|
|
19
19
|
iwa/core/types.py,sha256=EfDfIwLajTNK-BP9K17QLOIsGCs8legplKI_bUD_NjM,1992
|
|
20
20
|
iwa/core/ui.py,sha256=DglmrI7XhUmOpLn9Nog9Cej4r-VT0JGFkuSNBx-XorQ,3131
|
|
21
21
|
iwa/core/utils.py,sha256=shJuANkXSWVO3NF49syPA9hCG7H5AzaMJOG8V4fo6IM,4279
|
|
22
|
-
iwa/core/wallet.py,sha256=
|
|
22
|
+
iwa/core/wallet.py,sha256=GI83BOFGTto6u8LPKepJDqWxxNsHqfNF-vCxy4pTk3U,13057
|
|
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
25
|
iwa/core/chain/interface.py,sha256=04eGlhonHAvxFnqLoHRWUaQBzys6jW6BppUuNNjlnSk,18809
|
|
@@ -28,7 +28,8 @@ iwa/core/chain/models.py,sha256=0OgBo08FZEQisOdd00YUMXSAV7BC0CcWpqJ2y-gs0cI,4863
|
|
|
28
28
|
iwa/core/chain/rate_limiter.py,sha256=gU7TmWdH9D_wbXKT1X7mIgoIUCWVuebgvRhxiyLGAmI,6613
|
|
29
29
|
iwa/core/contracts/__init__.py,sha256=P5GFY_pnuI02teqVY2U0t98bn1_SSPAbcAzRMpCdTi4,34
|
|
30
30
|
iwa/core/contracts/cache.py,sha256=7wGQgyXAmofvx-irbxSnGgIDzQik6Pcpm7up6wMQcoo,4454
|
|
31
|
-
iwa/core/contracts/contract.py,sha256=
|
|
31
|
+
iwa/core/contracts/contract.py,sha256=RJaKh05p1MdgsCd8v799LwW1apkDboRDCu8Kah02760,13254
|
|
32
|
+
iwa/core/contracts/decoder.py,sha256=HaPw43ZAGswaiLPasMNU2rXKSvozb5t-O46VzJ-YH-U,5488
|
|
32
33
|
iwa/core/contracts/erc20.py,sha256=VqriOdUXej0ilTgpukpm1FUF_9sSrVMAPuEpIvyZ2SQ,2646
|
|
33
34
|
iwa/core/contracts/multisend.py,sha256=tSdBCWe7LSdBoKZ7z2QebmRFK4M2ln7H3kmJBeEb4Ho,2431
|
|
34
35
|
iwa/core/contracts/abis/erc20.json,sha256=vrdExMWcIogg_nO59j1Pmipmpa2Ulj3oCCdcdrrFVCE,16995
|
|
@@ -39,7 +40,7 @@ iwa/core/services/account.py,sha256=01MoEvl6FJlMnMB4fGwsPtnGa4kgA-d5hJeKu_ACg7Y,
|
|
|
39
40
|
iwa/core/services/balance.py,sha256=mPE12CuOFfCaJXaQXWOcQM1O03ZF3ghpy_-oOjNk_GE,4104
|
|
40
41
|
iwa/core/services/plugin.py,sha256=GNNlbtELyHl7MNVChrypF76GYphxXduxDog4kx1MLi8,3277
|
|
41
42
|
iwa/core/services/safe.py,sha256=ZmgVwbQhYlH5r3qhlY5uP8nCPtkkvV3sNnYG7_UCWUQ,14831
|
|
42
|
-
iwa/core/services/transaction.py,sha256=
|
|
43
|
+
iwa/core/services/transaction.py,sha256=1O8FAmXZak2oVu15ooUQsc0ByIS2F-O0vWxcAV2ylqk,17525
|
|
43
44
|
iwa/core/services/transfer/__init__.py,sha256=ZJfshFxJRsp8rkOqfVvd1cqEzIJ9tqBJh8pc0l90GLk,5576
|
|
44
45
|
iwa/core/services/transfer/base.py,sha256=sohz-Ss2i-pGYGl4x9bD93cnYKcSvsXaXyvyRawvgQs,9043
|
|
45
46
|
iwa/core/services/transfer/erc20.py,sha256=958ctXPWxq_KSQNoaG7RqWbC8SRb9NB3MzhtC2dp_NU,8960
|
|
@@ -61,10 +62,10 @@ iwa/plugins/gnosis/tests/test_safe.py,sha256=hQHVHBWQhGnuvzvx4U9fOWEwASJWwql42q6
|
|
|
61
62
|
iwa/plugins/olas/__init__.py,sha256=_NhBczzM61fhGYwGhnWfEeL8Jywyy_730GASe2BxzeQ,106
|
|
62
63
|
iwa/plugins/olas/constants.py,sha256=iTFoO2QW3KbhL5k5sKsJxxyDytl9wVIb_9hAih55KrE,7728
|
|
63
64
|
iwa/plugins/olas/events.py,sha256=SWD3wYdQ-l6dLUJSkfh_WsLmedH4Vsw_EvYXg7QC3yc,5970
|
|
64
|
-
iwa/plugins/olas/importer.py,sha256=
|
|
65
|
+
iwa/plugins/olas/importer.py,sha256=HvPHnY4pcHNm2Zr7ELYvFdzK-d52HM1Dm6PVvWhZSlQ,41570
|
|
65
66
|
iwa/plugins/olas/mech_reference.py,sha256=CaSCpQnQL4F7wOG6Ox6Zdoy-uNEQ78YBwVLILQZKL8Q,5782
|
|
66
|
-
iwa/plugins/olas/models.py,sha256=
|
|
67
|
-
iwa/plugins/olas/plugin.py,sha256=
|
|
67
|
+
iwa/plugins/olas/models.py,sha256=36N2Wuia7DVfpekJJ2pXPeS0lqhe5ED-Hjdox9YQG2c,5230
|
|
68
|
+
iwa/plugins/olas/plugin.py,sha256=zOPWyoVkSVh6guJ3TZj5enJFuiIbP3fRM8FkziPB-c0,15606
|
|
68
69
|
iwa/plugins/olas/contracts/activity_checker.py,sha256=WXxuzbpXGVqIfEiMPiiqN3Z_UxIY-Lvx0raa1ErBfPA,5323
|
|
69
70
|
iwa/plugins/olas/contracts/base.py,sha256=y73aQbDq6l4zUpz_eQAg4MsLkTAEqjjupXlcvxjfgCI,240
|
|
70
71
|
iwa/plugins/olas/contracts/mech.py,sha256=dXYtyORc-oiu9ga5PtTquOFkoakb6BLGKvlUsteygIg,2767
|
|
@@ -86,13 +87,13 @@ iwa/plugins/olas/scripts/test_full_mech_flow.py,sha256=Fqoq5bn7Z_3YyRrnuqNAZy9cw
|
|
|
86
87
|
iwa/plugins/olas/scripts/test_simple_lifecycle.py,sha256=8T50tOZx3afeECSfCNAb0rAHNtYOsBaeXlMwKXElCk8,2099
|
|
87
88
|
iwa/plugins/olas/service_manager/__init__.py,sha256=GXiThMEY3nPgHUl1i-DLrF4h96z9jPxxI8Jepo2E1PM,1926
|
|
88
89
|
iwa/plugins/olas/service_manager/base.py,sha256=EBPg0ymqgtAb7ZvVSfTt31QYgv_6gp4UAc6je00NLAg,5009
|
|
89
|
-
iwa/plugins/olas/service_manager/drain.py,sha256=
|
|
90
|
-
iwa/plugins/olas/service_manager/lifecycle.py,sha256=
|
|
90
|
+
iwa/plugins/olas/service_manager/drain.py,sha256=1Ku7axThwLtKxaNTkwhP4j1yjIXbFXAqNFDrCSmgfto,12569
|
|
91
|
+
iwa/plugins/olas/service_manager/lifecycle.py,sha256=Aibd4D-R_UjFEqSTEsVaMFKbrtEOY8fo0Td0eFYjcDI,48732
|
|
91
92
|
iwa/plugins/olas/service_manager/mech.py,sha256=NVzVbEmyOe3wK92VEzCCOSuy3HDkEP1MSoVt7Av8Psk,27949
|
|
92
|
-
iwa/plugins/olas/service_manager/staking.py,sha256=
|
|
93
|
+
iwa/plugins/olas/service_manager/staking.py,sha256=4WS1m1E1ddX5EbQuvNnNswZjT20nc7I9IKzjrBuUCFw,28701
|
|
93
94
|
iwa/plugins/olas/tests/conftest.py,sha256=4vM7EI00SrTGyeP0hNzsGSQHEj2-iznVgzlNh2_OGfo,739
|
|
94
95
|
iwa/plugins/olas/tests/test_importer.py,sha256=i9LKov7kNRECB3hmRnhKBwcfx3uxtjWe4BB77bOOpeo,4282
|
|
95
|
-
iwa/plugins/olas/tests/test_importer_error_handling.py,sha256=
|
|
96
|
+
iwa/plugins/olas/tests/test_importer_error_handling.py,sha256=yHpdxfN20gnZ1cHPJqk9xHC_26jyVN8kiziY-sl4FuA,11696
|
|
96
97
|
iwa/plugins/olas/tests/test_mech_contracts.py,sha256=wvxuigPafF-ySIHVBdWVei3AO418iPh7cSVdAlUGm_s,3566
|
|
97
98
|
iwa/plugins/olas/tests/test_olas_contracts.py,sha256=B8X-5l1KfYMoZOiM94_rcNzbILLl78rqt_jhyxzAOqE,10835
|
|
98
99
|
iwa/plugins/olas/tests/test_olas_integration.py,sha256=vjL8-RNdxXu6RFR5F1Bn7xqnxnUVWTzl2--Pp7-0r5A,22973
|
|
@@ -157,7 +158,7 @@ iwa/web/tests/test_web_endpoints.py,sha256=C264MH-CTyDW4GLUrTXBgLJKUk4-89pFAScBd
|
|
|
157
158
|
iwa/web/tests/test_web_olas.py,sha256=0CVSsrncOeJ3x0ECV7mVLQV_CXZRrOqGiVjgLIi6hZ8,16308
|
|
158
159
|
iwa/web/tests/test_web_swap.py,sha256=7A4gBJFL01kIXPtW1E1J17SCsVc_0DmUn-R8kKrnnVA,2974
|
|
159
160
|
iwa/web/tests/test_web_swap_coverage.py,sha256=zGNrzlhZ_vWDCvWmLcoUwFgqxnrp_ACbo49AtWBS_Kw,5584
|
|
160
|
-
iwa-0.0.
|
|
161
|
+
iwa-0.0.28.dist-info/licenses/LICENSE,sha256=eIubm_IlBHPYRQlLNZKbBNKhJUUP3JH0A2miZUhAVfI,1078
|
|
161
162
|
tests/legacy_cow.py,sha256=oOkZvIxL70ReEoD9oHQbOD5GpjIr6AGNHcOCgfPlerU,8389
|
|
162
163
|
tests/legacy_safe.py,sha256=AssM2g13E74dNGODu_H0Q0y412lgqsrYnEzI97nm_Ts,2972
|
|
163
164
|
tests/legacy_transaction_retry_logic.py,sha256=D9RqZ7DBu61Xr2djBAodU2p9UE939LL-DnQXswX5iQk,1497
|
|
@@ -210,8 +211,8 @@ tests/test_utils.py,sha256=vkP49rYNI8BRzLpWR3WnKdDr8upeZjZcs7Rx0pjbQMo,1292
|
|
|
210
211
|
tests/test_workers.py,sha256=MInwdkFY5LdmFB3o1odIaSD7AQZb3263hNafO1De5PE,2793
|
|
211
212
|
tools/create_and_stake_service.py,sha256=1xwy_bJQI1j9yIQ968Oc9Db_F6mk1659LuuZntTASDE,3742
|
|
212
213
|
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.
|
|
214
|
+
iwa-0.0.28.dist-info/METADATA,sha256=400-yQdPxQzEfg4h9h4C6UrD5gcjUYp0DhY4T965xnM,7295
|
|
215
|
+
iwa-0.0.28.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
216
|
+
iwa-0.0.28.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
|
|
217
|
+
iwa-0.0.28.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
|
|
218
|
+
iwa-0.0.28.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|