iwa 0.0.58__py3-none-any.whl → 0.0.59__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 +32 -21
- iwa/core/chain/rate_limiter.py +0 -6
- iwa/core/chainlist.py +15 -10
- iwa/core/cli.py +3 -0
- iwa/core/contracts/cache.py +1 -1
- iwa/core/contracts/contract.py +1 -0
- iwa/core/contracts/decoder.py +10 -4
- iwa/core/http.py +31 -0
- iwa/core/ipfs.py +11 -19
- iwa/core/keys.py +10 -4
- iwa/core/models.py +1 -3
- iwa/core/pricing.py +3 -21
- iwa/core/rpc_monitor.py +1 -0
- iwa/core/services/balance.py +0 -1
- iwa/core/services/safe.py +8 -2
- iwa/core/services/safe_executor.py +52 -18
- iwa/core/services/transaction.py +32 -12
- iwa/core/services/transfer/erc20.py +0 -1
- iwa/core/services/transfer/native.py +1 -1
- iwa/core/tests/test_gnosis_fee.py +6 -2
- iwa/core/tests/test_ipfs.py +1 -1
- iwa/core/tests/test_regression_fixes.py +3 -6
- iwa/core/utils.py +2 -0
- iwa/core/wallet.py +3 -1
- iwa/plugins/olas/constants.py +15 -5
- iwa/plugins/olas/contracts/activity_checker.py +3 -3
- iwa/plugins/olas/contracts/staking.py +0 -1
- iwa/plugins/olas/events.py +15 -13
- iwa/plugins/olas/importer.py +26 -20
- iwa/plugins/olas/plugin.py +16 -14
- iwa/plugins/olas/service_manager/drain.py +1 -3
- iwa/plugins/olas/service_manager/lifecycle.py +9 -9
- iwa/plugins/olas/service_manager/staking.py +11 -6
- iwa/plugins/olas/tests/test_olas_archiving.py +25 -15
- iwa/plugins/olas/tests/test_olas_integration.py +49 -29
- iwa/plugins/olas/tests/test_service_manager.py +8 -10
- iwa/plugins/olas/tests/test_service_manager_errors.py +5 -4
- iwa/plugins/olas/tests/test_service_manager_flows.py +6 -5
- iwa/plugins/olas/tests/test_service_staking.py +64 -38
- iwa/tools/drain_accounts.py +2 -1
- iwa/tools/reset_env.py +2 -1
- iwa/tools/test_chainlist.py +5 -1
- iwa/tui/screens/wallets.py +1 -3
- iwa/web/routers/olas/services.py +10 -5
- {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/METADATA +1 -1
- {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/RECORD +58 -57
- tests/test_balance_service.py +0 -2
- tests/test_chain.py +1 -2
- tests/test_rate_limiter_retry.py +2 -7
- tests/test_rpc_efficiency.py +4 -1
- tests/test_rpc_rate_limit.py +1 -0
- tests/test_rpc_rotation.py +4 -4
- tests/test_safe_executor.py +76 -50
- tests/test_safe_integration.py +11 -6
- {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/WHEEL +0 -0
- {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.58.dist-info → iwa-0.0.59.dist-info}/top_level.txt +0 -0
|
@@ -52,6 +52,7 @@ class SafeTransactionExecutor:
|
|
|
52
52
|
config = Config().core
|
|
53
53
|
self.max_retries = max_retries or config.safe_tx_max_retries
|
|
54
54
|
self.gas_buffer = gas_buffer or config.safe_tx_gas_buffer
|
|
55
|
+
self._client_cache: Dict[str, EthereumClient] = {}
|
|
55
56
|
|
|
56
57
|
def execute_with_retry(
|
|
57
58
|
self,
|
|
@@ -81,17 +82,27 @@ class SafeTransactionExecutor:
|
|
|
81
82
|
try:
|
|
82
83
|
# Prepare and execute attempt
|
|
83
84
|
tx_hash = self._execute_attempt(
|
|
84
|
-
safe_address,
|
|
85
|
+
safe_address,
|
|
86
|
+
safe_tx,
|
|
87
|
+
signer_keys,
|
|
88
|
+
operation_name,
|
|
89
|
+
attempt,
|
|
90
|
+
current_gas,
|
|
91
|
+
base_estimate,
|
|
85
92
|
)
|
|
86
93
|
|
|
87
94
|
# Check receipt
|
|
88
95
|
receipt = self.chain_interface.web3.eth.wait_for_transaction_receipt(tx_hash)
|
|
89
96
|
if self._check_receipt_status(receipt):
|
|
90
97
|
SAFE_TX_STATS["final_successes"] += 1
|
|
91
|
-
logger.info(
|
|
98
|
+
logger.info(
|
|
99
|
+
f"[{operation_name}] Success on attempt {attempt + 1}. Tx Hash: {tx_hash}"
|
|
100
|
+
)
|
|
92
101
|
return True, tx_hash, receipt
|
|
93
102
|
|
|
94
|
-
logger.error(
|
|
103
|
+
logger.error(
|
|
104
|
+
f"[{operation_name}] Mined but failed (status 0) on attempt {attempt + 1}."
|
|
105
|
+
)
|
|
95
106
|
raise ValueError("Transaction reverted on-chain")
|
|
96
107
|
|
|
97
108
|
except Exception as e:
|
|
@@ -112,7 +123,14 @@ class SafeTransactionExecutor:
|
|
|
112
123
|
return False, str(last_error), None
|
|
113
124
|
|
|
114
125
|
def _execute_attempt(
|
|
115
|
-
self,
|
|
126
|
+
self,
|
|
127
|
+
safe_address,
|
|
128
|
+
safe_tx,
|
|
129
|
+
signer_keys,
|
|
130
|
+
operation_name,
|
|
131
|
+
attempt,
|
|
132
|
+
current_gas,
|
|
133
|
+
base_estimate,
|
|
116
134
|
) -> str:
|
|
117
135
|
"""Prepare client, estimate gas, simulate, and execute."""
|
|
118
136
|
# 1. (Re)Create Safe client
|
|
@@ -186,7 +204,12 @@ class SafeTransactionExecutor:
|
|
|
186
204
|
return status == 1
|
|
187
205
|
|
|
188
206
|
def _handle_execution_failure(
|
|
189
|
-
self,
|
|
207
|
+
self,
|
|
208
|
+
error: Exception,
|
|
209
|
+
safe_address: str,
|
|
210
|
+
safe_tx: SafeTx,
|
|
211
|
+
attempt: int,
|
|
212
|
+
operation_name: str,
|
|
190
213
|
) -> Tuple[SafeTx, bool]:
|
|
191
214
|
"""Handle execution failure and determine next steps."""
|
|
192
215
|
classification = self._classify_error(error)
|
|
@@ -220,7 +243,9 @@ class SafeTransactionExecutor:
|
|
|
220
243
|
"""Estimate gas for a Safe transaction with buffer and hard cap."""
|
|
221
244
|
try:
|
|
222
245
|
# Use on-chain simulation via safe-eth-py
|
|
223
|
-
estimated = safe.estimate_tx_gas(
|
|
246
|
+
estimated = safe.estimate_tx_gas(
|
|
247
|
+
safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation
|
|
248
|
+
)
|
|
224
249
|
with_buffer = int(estimated * self.gas_buffer)
|
|
225
250
|
|
|
226
251
|
# Apply x10 hard cap if we have a base estimate
|
|
@@ -237,16 +262,17 @@ class SafeTransactionExecutor:
|
|
|
237
262
|
|
|
238
263
|
def _recreate_safe_client(self, safe_address: str) -> Safe:
|
|
239
264
|
"""Recreate Safe with current (possibly rotated) RPC."""
|
|
240
|
-
|
|
265
|
+
rpc_url = self.chain_interface.current_rpc
|
|
266
|
+
if rpc_url not in self._client_cache:
|
|
267
|
+
self._client_cache[rpc_url] = EthereumClient(rpc_url)
|
|
268
|
+
ethereum_client = self._client_cache[rpc_url]
|
|
241
269
|
return Safe(safe_address, ethereum_client)
|
|
242
270
|
|
|
243
271
|
def _is_nonce_error(self, error: Exception) -> bool:
|
|
244
272
|
"""Check if error is due to Safe nonce conflict."""
|
|
245
273
|
error_text = str(error).lower()
|
|
246
274
|
# GS025 = Invalid nonce (NOT GS026 which is invalid signatures)
|
|
247
|
-
return any(x in error_text for x in [
|
|
248
|
-
"nonce", "gs025", "already executed", "duplicate"
|
|
249
|
-
])
|
|
275
|
+
return any(x in error_text for x in ["nonce", "gs025", "already executed", "duplicate"])
|
|
250
276
|
|
|
251
277
|
def _is_signature_error(self, error: Exception) -> bool:
|
|
252
278
|
"""Check if error is due to invalid Safe signatures.
|
|
@@ -257,10 +283,17 @@ class SafeTransactionExecutor:
|
|
|
257
283
|
GS026 = Invalid owner (signature from non-owner)
|
|
258
284
|
"""
|
|
259
285
|
error_text = str(error).lower()
|
|
260
|
-
return any(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
286
|
+
return any(
|
|
287
|
+
x in error_text
|
|
288
|
+
for x in [
|
|
289
|
+
"gs020",
|
|
290
|
+
"gs021",
|
|
291
|
+
"gs024",
|
|
292
|
+
"gs026",
|
|
293
|
+
"invalid signatures",
|
|
294
|
+
"signatures data too short",
|
|
295
|
+
]
|
|
296
|
+
)
|
|
264
297
|
|
|
265
298
|
def _refresh_nonce(self, safe: Safe, safe_tx: SafeTx) -> SafeTx:
|
|
266
299
|
"""Re-fetch nonce and rebuild transaction."""
|
|
@@ -283,7 +316,9 @@ class SafeTransactionExecutor:
|
|
|
283
316
|
def _classify_error(self, error: Exception) -> dict:
|
|
284
317
|
"""Classify Safe transaction errors for retry decisions."""
|
|
285
318
|
err_text = str(error).lower()
|
|
286
|
-
is_rpc = self.chain_interface._is_rate_limit_error(
|
|
319
|
+
is_rpc = self.chain_interface._is_rate_limit_error(
|
|
320
|
+
error
|
|
321
|
+
) or self.chain_interface._is_connection_error(error)
|
|
287
322
|
|
|
288
323
|
return {
|
|
289
324
|
"is_gas_error": any(x in err_text for x in ["gas", "out of gas", "intrinsic"]),
|
|
@@ -296,6 +331,7 @@ class SafeTransactionExecutor:
|
|
|
296
331
|
def _decode_revert_reason(self, error: Exception) -> Optional[str]:
|
|
297
332
|
"""Attempt to decode the revert reason."""
|
|
298
333
|
import re
|
|
334
|
+
|
|
299
335
|
error_text = str(error)
|
|
300
336
|
hex_match = re.search(r"0x[0-9a-fA-F]{8,}", error_text)
|
|
301
337
|
if hex_match:
|
|
@@ -311,6 +347,4 @@ class SafeTransactionExecutor:
|
|
|
311
347
|
|
|
312
348
|
def _log_retry(self, attempt: int, error: Exception, strategy: str):
|
|
313
349
|
"""Log a retry attempt."""
|
|
314
|
-
logger.warning(
|
|
315
|
-
f"Safe TX attempt {attempt} failed, strategy: {strategy}. Error: {error}"
|
|
316
|
-
)
|
|
350
|
+
logger.warning(f"Safe TX attempt {attempt} failed, strategy: {strategy}. Error: {error}")
|
iwa/core/services/transaction.py
CHANGED
|
@@ -45,19 +45,29 @@ class TransferLogger:
|
|
|
45
45
|
if tx_hash:
|
|
46
46
|
try:
|
|
47
47
|
tx = self.chain_interface.web3.eth.get_transaction(tx_hash)
|
|
48
|
-
native_value =
|
|
48
|
+
native_value = (
|
|
49
|
+
getattr(tx, "value", 0) or tx.get("value", 0)
|
|
50
|
+
if isinstance(tx, dict)
|
|
51
|
+
else getattr(tx, "value", 0)
|
|
52
|
+
)
|
|
49
53
|
if native_value and int(native_value) > 0:
|
|
50
|
-
from_addr =
|
|
54
|
+
from_addr = (
|
|
55
|
+
getattr(tx, "from", "") if hasattr(tx, "from") else tx.get("from", "")
|
|
56
|
+
)
|
|
51
57
|
# Handle AttributeDict's special 'from' attribute
|
|
52
58
|
if not from_addr and hasattr(tx, "__getitem__"):
|
|
53
59
|
from_addr = tx["from"]
|
|
54
|
-
to_addr = getattr(tx, "to", "") or (
|
|
60
|
+
to_addr = getattr(tx, "to", "") or (
|
|
61
|
+
tx.get("to", "") if isinstance(tx, dict) else ""
|
|
62
|
+
)
|
|
55
63
|
self._log_native_transfer(from_addr, to_addr, native_value)
|
|
56
64
|
except Exception as e:
|
|
57
65
|
logger.debug(f"Could not get tx for native transfer logging: {e}")
|
|
58
66
|
|
|
59
67
|
# Log ERC20 transfers from event logs
|
|
60
|
-
logs =
|
|
68
|
+
logs = (
|
|
69
|
+
receipt.get("logs", []) if isinstance(receipt, dict) else getattr(receipt, "logs", [])
|
|
70
|
+
)
|
|
61
71
|
|
|
62
72
|
for log in logs:
|
|
63
73
|
self._process_log(log)
|
|
@@ -156,7 +166,9 @@ class TransferLogger:
|
|
|
156
166
|
else:
|
|
157
167
|
# Likely an NFT (ERC721) - the amount is the token ID
|
|
158
168
|
if amount_wei > 0:
|
|
159
|
-
logger.info(
|
|
169
|
+
logger.info(
|
|
170
|
+
f"[NFT TRANSFER] Token #{amount_wei} {token_label}: {from_label} → {to_label}"
|
|
171
|
+
)
|
|
160
172
|
else:
|
|
161
173
|
logger.debug(f"[NFT TRANSFER] {token_label}: {from_label} → {to_label}")
|
|
162
174
|
|
|
@@ -265,9 +277,7 @@ class TransactionService:
|
|
|
265
277
|
"""Inner operation wrapped by with_retry."""
|
|
266
278
|
try:
|
|
267
279
|
signed_txn = self.key_storage.sign_transaction(tx, signer_address_or_tag)
|
|
268
|
-
txn_hash = chain_interface.web3.eth.send_raw_transaction(
|
|
269
|
-
signed_txn.raw_transaction
|
|
270
|
-
)
|
|
280
|
+
txn_hash = chain_interface.web3.eth.send_raw_transaction(signed_txn.raw_transaction)
|
|
271
281
|
receipt = chain_interface.web3.eth.wait_for_transaction_receipt(txn_hash)
|
|
272
282
|
|
|
273
283
|
status = getattr(receipt, "status", None)
|
|
@@ -412,10 +422,12 @@ class TransactionService:
|
|
|
412
422
|
signer_account: StoredSafeAccount,
|
|
413
423
|
chain_interface,
|
|
414
424
|
chain_name: str,
|
|
415
|
-
tags: List[str] = None
|
|
425
|
+
tags: List[str] = None,
|
|
416
426
|
) -> Tuple[bool, Dict]:
|
|
417
427
|
"""Execute transaction via SafeService."""
|
|
418
|
-
logger.info(
|
|
428
|
+
logger.info(
|
|
429
|
+
f"Routing transaction via Safe {self._resolve_label(signer_account.address, chain_name)}..."
|
|
430
|
+
)
|
|
419
431
|
|
|
420
432
|
try:
|
|
421
433
|
# Extract basic params
|
|
@@ -431,7 +443,7 @@ class TransactionService:
|
|
|
431
443
|
to=to_addr,
|
|
432
444
|
value=value,
|
|
433
445
|
chain_name=chain_name,
|
|
434
|
-
data=data
|
|
446
|
+
data=data,
|
|
435
447
|
)
|
|
436
448
|
|
|
437
449
|
# Receipt is already waited for inside execute_safe_transaction/executor
|
|
@@ -445,7 +457,13 @@ class TransactionService:
|
|
|
445
457
|
if receipt and status == 1:
|
|
446
458
|
logger.info(f"Safe transaction executed successfully. Tx Hash: {tx_hash}")
|
|
447
459
|
self._log_successful_transaction(
|
|
448
|
-
receipt,
|
|
460
|
+
receipt,
|
|
461
|
+
tx,
|
|
462
|
+
signer_account,
|
|
463
|
+
chain_name,
|
|
464
|
+
bytes.fromhex(tx_hash.replace("0x", "")),
|
|
465
|
+
tags,
|
|
466
|
+
chain_interface,
|
|
449
467
|
)
|
|
450
468
|
return True, receipt
|
|
451
469
|
else:
|
|
@@ -460,11 +478,13 @@ class TransactionService:
|
|
|
460
478
|
# Extract hex data from common error patterns
|
|
461
479
|
# Pattern 1: ('execution reverted', '0x...')
|
|
462
480
|
import re
|
|
481
|
+
|
|
463
482
|
hex_match = re.search(r"0x[0-9a-fA-F]{8,}", error_text)
|
|
464
483
|
|
|
465
484
|
if hex_match:
|
|
466
485
|
try:
|
|
467
486
|
from iwa.core.contracts.decoder import ErrorDecoder
|
|
487
|
+
|
|
468
488
|
data = hex_match.group(0)
|
|
469
489
|
decoded = ErrorDecoder().decode(data)
|
|
470
490
|
if decoded:
|
|
@@ -41,7 +41,9 @@ class TestGnosisFeeFix(unittest.TestCase):
|
|
|
41
41
|
|
|
42
42
|
# CRITICAL ASSERTION: maxPriorityFeePerGas must be >= 1
|
|
43
43
|
# If the fix works, it should be 1. If it fails (old behavior), it would be 0.
|
|
44
|
-
self.assertEqual(
|
|
44
|
+
self.assertEqual(
|
|
45
|
+
params["maxPriorityFeePerGas"], 1, "Priority fee should be forced to 1 wei"
|
|
46
|
+
)
|
|
45
47
|
|
|
46
48
|
# Verify max fee calculation: (base * 1.5) + priority
|
|
47
49
|
expected_max_fee = int(5000 * 1.5) + 1
|
|
@@ -84,4 +86,6 @@ class TestGnosisFeeFix(unittest.TestCase):
|
|
|
84
86
|
|
|
85
87
|
params = eth_interface.calculate_transaction_params(mock_func, {"from": "0x123"})
|
|
86
88
|
|
|
87
|
-
self.assertEqual(
|
|
89
|
+
self.assertEqual(
|
|
90
|
+
params["maxPriorityFeePerGas"], 1, "Generic fallback should apply to all chains"
|
|
91
|
+
)
|
iwa/core/tests/test_ipfs.py
CHANGED
|
@@ -61,7 +61,7 @@ def test_push_to_ipfs_sync_uses_session(mock_config, mock_cid_decode):
|
|
|
61
61
|
|
|
62
62
|
# Verify second call reuses session
|
|
63
63
|
push_to_ipfs_sync(b"test data 2")
|
|
64
|
-
mock_session_cls.assert_called_once()
|
|
64
|
+
mock_session_cls.assert_called_once() # Should still be 1 call
|
|
65
65
|
assert mock_session.post.call_count == 2
|
|
66
66
|
|
|
67
67
|
|
|
@@ -34,18 +34,14 @@ class TestRegressionFixes(unittest.TestCase):
|
|
|
34
34
|
# This is what we are testing: get_suggested_fees() provides the safety net
|
|
35
35
|
mock_chain_interface.get_suggested_fees.return_value = {
|
|
36
36
|
"maxFeePerGas": 1500,
|
|
37
|
-
"maxPriorityFeePerGas": 10
|
|
37
|
+
"maxPriorityFeePerGas": 10,
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
with patch("iwa.core.services.transaction.ChainInterfaces") as mock_interfaces:
|
|
41
41
|
mock_interfaces.return_value.get.return_value = mock_chain_interface
|
|
42
42
|
|
|
43
43
|
# 3. Prepare transaction WITHOUT fees
|
|
44
|
-
tx = {
|
|
45
|
-
"to": "0x09312C66A14a024B4e903D986Ca7E2C0dDD06227",
|
|
46
|
-
"value": 1000,
|
|
47
|
-
"gas": 21000
|
|
48
|
-
}
|
|
44
|
+
tx = {"to": "0x09312C66A14a024B4e903D986Ca7E2C0dDD06227", "value": 1000, "gas": 21000}
|
|
49
45
|
|
|
50
46
|
# 4. Run internal preparation
|
|
51
47
|
service._prepare_transaction(tx, "signer", mock_chain_interface)
|
|
@@ -96,5 +92,6 @@ class TestRegressionFixes(unittest.TestCase):
|
|
|
96
92
|
self.assertIsInstance(accounts[key]["address"], str)
|
|
97
93
|
self.assertEqual(accounts[key]["address"].lower(), addr_str.lower())
|
|
98
94
|
|
|
95
|
+
|
|
99
96
|
if __name__ == "__main__":
|
|
100
97
|
unittest.main()
|
iwa/core/utils.py
CHANGED
|
@@ -85,9 +85,11 @@ def configure_logger():
|
|
|
85
85
|
configure_logger.configured = True
|
|
86
86
|
return logger
|
|
87
87
|
|
|
88
|
+
|
|
88
89
|
def get_version(package_name: str) -> str:
|
|
89
90
|
"""Get package version."""
|
|
90
91
|
from importlib.metadata import PackageNotFoundError, version
|
|
92
|
+
|
|
91
93
|
try:
|
|
92
94
|
return version(package_name)
|
|
93
95
|
except PackageNotFoundError:
|
iwa/core/wallet.py
CHANGED
|
@@ -37,7 +37,9 @@ class Wallet:
|
|
|
37
37
|
self.balance_service = BalanceService(self.key_storage, self.account_service)
|
|
38
38
|
self.safe_service = SafeService(self.key_storage, self.account_service)
|
|
39
39
|
# self.transaction_manager = TransactionManager(self.key_storage, self.account_service)
|
|
40
|
-
self.transaction_service = TransactionService(
|
|
40
|
+
self.transaction_service = TransactionService(
|
|
41
|
+
self.key_storage, self.account_service, self.safe_service
|
|
42
|
+
)
|
|
41
43
|
|
|
42
44
|
self.transfer_service = TransferService(
|
|
43
45
|
self.key_storage,
|
iwa/plugins/olas/constants.py
CHANGED
|
@@ -92,8 +92,12 @@ OLAS_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
|
|
|
92
92
|
OLAS_TRADER_STAKING_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
|
|
93
93
|
"gnosis": {
|
|
94
94
|
# === LEGACY (no marketplace) ===
|
|
95
|
-
"Hobbyist 1 Legacy (100 OLAS)": EthereumAddress(
|
|
96
|
-
|
|
95
|
+
"Hobbyist 1 Legacy (100 OLAS)": EthereumAddress(
|
|
96
|
+
"0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C"
|
|
97
|
+
),
|
|
98
|
+
"Hobbyist 2 Legacy (500 OLAS)": EthereumAddress(
|
|
99
|
+
"0x238EB6993b90A978ec6AAD7530D6429c949C08DA"
|
|
100
|
+
),
|
|
97
101
|
"Expert Legacy (1k OLAS)": EthereumAddress("0x5344B7DD311e5d3DdDd46A4f71481Bd7b05AAA3e"),
|
|
98
102
|
"Expert 2 Legacy (1k OLAS)": EthereumAddress("0xb964e44c126410df341ae04B13aB10A985fE3513"),
|
|
99
103
|
"Expert 3 Legacy (2k OLAS)": EthereumAddress("0x80faD33Cadb5F53f9D29F02Db97D682E8B101618"),
|
|
@@ -103,9 +107,15 @@ OLAS_TRADER_STAKING_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
|
|
|
103
107
|
"Expert 7 Legacy (10k OLAS)": EthereumAddress("0xD7A3C8b975f71030135f1a66E9e23164d54fF455"),
|
|
104
108
|
"Expert 8 Legacy (2k OLAS)": EthereumAddress("0x356C108D49C5eebd21c84c04E9162de41933030c"),
|
|
105
109
|
"Expert 9 Legacy (10k OLAS)": EthereumAddress("0x17dBAe44BC5618Cc254055B386A29576b4F87015"),
|
|
106
|
-
"Expert 10 Legacy (10k OLAS)": EthereumAddress(
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
"Expert 10 Legacy (10k OLAS)": EthereumAddress(
|
|
111
|
+
"0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f"
|
|
112
|
+
),
|
|
113
|
+
"Expert 11 Legacy (10k OLAS)": EthereumAddress(
|
|
114
|
+
"0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7"
|
|
115
|
+
),
|
|
116
|
+
"Expert 12 Legacy (10k OLAS)": EthereumAddress(
|
|
117
|
+
"0xF4a75F476801B3fBB2e7093aCDcc3576593Cc1fc"
|
|
118
|
+
),
|
|
109
119
|
# === MM v1 (old marketplace 0x4554fE75...) ===
|
|
110
120
|
"Expert 15 MM v1 (10k OLAS)": EthereumAddress("0x88eB38FF79fBa8C19943C0e5Acfa67D5876AdCC1"),
|
|
111
121
|
"Expert 16 MM v1 (10k OLAS)": EthereumAddress("0x6c65430515c70a3f5E62107CC301685B7D46f991"),
|
|
@@ -48,7 +48,6 @@ class ActivityCheckerContract(ContractInstance):
|
|
|
48
48
|
self._agent_mech: Optional[EthereumAddress] = None
|
|
49
49
|
self._liveness_ratio: Optional[int] = None
|
|
50
50
|
|
|
51
|
-
|
|
52
51
|
def get_multisig_nonces(self, multisig: EthereumAddress) -> Tuple[int, int]:
|
|
53
52
|
"""Get the nonces for a multisig address.
|
|
54
53
|
|
|
@@ -64,7 +63,6 @@ class ActivityCheckerContract(ContractInstance):
|
|
|
64
63
|
nonces = self.contract.functions.getMultisigNonces(multisig).call()
|
|
65
64
|
return (nonces[0], nonces[1])
|
|
66
65
|
|
|
67
|
-
|
|
68
66
|
@property
|
|
69
67
|
def mech_marketplace(self) -> Optional[EthereumAddress]:
|
|
70
68
|
"""Get the mech marketplace address."""
|
|
@@ -83,7 +81,9 @@ class ActivityCheckerContract(ContractInstance):
|
|
|
83
81
|
try:
|
|
84
82
|
agent_mech_function = getattr(self.contract.functions, "agentMech", None)
|
|
85
83
|
self._agent_mech = (
|
|
86
|
-
agent_mech_function().call()
|
|
84
|
+
agent_mech_function().call()
|
|
85
|
+
if agent_mech_function
|
|
86
|
+
else DEFAULT_MECH_CONTRACT_ADDRESS
|
|
87
87
|
)
|
|
88
88
|
except Exception:
|
|
89
89
|
self._agent_mech = DEFAULT_MECH_CONTRACT_ADDRESS
|
|
@@ -82,7 +82,6 @@ class StakingContract(ContractInstance):
|
|
|
82
82
|
self._activity_checker: Optional[ActivityCheckerContract] = None
|
|
83
83
|
self._activity_checker_address: Optional[EthereumAddress] = None
|
|
84
84
|
|
|
85
|
-
|
|
86
85
|
def get_requirements(self) -> Dict[str, Union[str, int]]:
|
|
87
86
|
"""Get the contract requirements for token and deposits.
|
|
88
87
|
|
iwa/plugins/olas/events.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Event-based cache invalidation for Olas contracts."""
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
from loguru import logger
|
|
5
4
|
|
|
6
5
|
from iwa.core.contracts.cache import ContractCache
|
|
@@ -75,7 +74,7 @@ class OlasEventInvalidator:
|
|
|
75
74
|
except Exception as e:
|
|
76
75
|
logger.error(f"Error in OlasEventInvalidator: {e}")
|
|
77
76
|
|
|
78
|
-
time.sleep(10)
|
|
77
|
+
time.sleep(10) # check every 10 seconds
|
|
79
78
|
|
|
80
79
|
def _check_events(self, from_block: int, to_block: int):
|
|
81
80
|
"""Check for relevant events in the block range."""
|
|
@@ -101,14 +100,19 @@ class OlasEventInvalidator:
|
|
|
101
100
|
StakingContract, self.staking_addresses[0], self.chain_name
|
|
102
101
|
)
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
logs = self.web3.eth.get_logs(
|
|
104
|
+
{
|
|
105
|
+
"fromBlock": from_block,
|
|
106
|
+
"toBlock": to_block,
|
|
107
|
+
"address": self.staking_addresses,
|
|
108
|
+
"topics": [
|
|
109
|
+
self.web3.keccak(
|
|
110
|
+
text="Checkpoint(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)"
|
|
111
|
+
).hex()
|
|
112
|
+
],
|
|
113
|
+
# Note: signature might vary, safer to use the event object if ABI allows
|
|
114
|
+
}
|
|
115
|
+
)
|
|
112
116
|
|
|
113
117
|
# If we used the contract event object to filter, it handles the topic generation:
|
|
114
118
|
# logs = checkpoint_event_abi.get_logs(fromBlock=from_block, toBlock=to_block)
|
|
@@ -130,9 +134,7 @@ class OlasEventInvalidator:
|
|
|
130
134
|
# self.contract_cache.invalidate(StakingContract, addr, self.chain_name)
|
|
131
135
|
|
|
132
136
|
# Option B: Get instance and clear specific cache (safe public access)
|
|
133
|
-
instance = self.contract_cache.get_if_cached(
|
|
134
|
-
StakingContract, addr, self.chain_name
|
|
135
|
-
)
|
|
137
|
+
instance = self.contract_cache.get_if_cached(StakingContract, addr, self.chain_name)
|
|
136
138
|
if instance:
|
|
137
139
|
instance.clear_epoch_cache()
|
|
138
140
|
logger.debug(f"Cleared epoch cache for {addr}")
|
iwa/plugins/olas/importer.py
CHANGED
|
@@ -181,9 +181,7 @@ class OlasServiceImporter:
|
|
|
181
181
|
if service.service_id:
|
|
182
182
|
key = f"{service.chain_name}:{service.service_id}"
|
|
183
183
|
if key in seen_keys:
|
|
184
|
-
logger.debug(
|
|
185
|
-
f"Skipping duplicate service {key} from {service.source_folder}"
|
|
186
|
-
)
|
|
184
|
+
logger.debug(f"Skipping duplicate service {key} from {service.source_folder}")
|
|
187
185
|
duplicates += 1
|
|
188
186
|
continue
|
|
189
187
|
seen_keys.add(key)
|
|
@@ -482,9 +480,7 @@ class OlasServiceImporter:
|
|
|
482
480
|
staking_program_id, chain_name
|
|
483
481
|
)
|
|
484
482
|
|
|
485
|
-
def _resolve_staking_contract(
|
|
486
|
-
self, staking_program_id: str, chain_name: str
|
|
487
|
-
) -> Optional[str]:
|
|
483
|
+
def _resolve_staking_contract(self, staking_program_id: str, chain_name: str) -> Optional[str]:
|
|
488
484
|
"""Resolve a staking program ID to a contract address."""
|
|
489
485
|
address = STAKING_PROGRAM_MAP.get(staking_program_id)
|
|
490
486
|
if address:
|
|
@@ -540,8 +536,10 @@ class OlasServiceImporter:
|
|
|
540
536
|
|
|
541
537
|
# Check for "safes" entry which indicates the owner is a Safe
|
|
542
538
|
# Structure: "safes": { "gnosis": "0x..." }
|
|
543
|
-
if
|
|
544
|
-
|
|
539
|
+
if (
|
|
540
|
+
"safes" in data and FLAGS_OWNER_SAFE in data["safes"]
|
|
541
|
+
): # Need to detect chain dynamically or iterate
|
|
542
|
+
pass
|
|
545
543
|
|
|
546
544
|
# Logic update:
|
|
547
545
|
# 1. Capture EOA address always (it's the signer)
|
|
@@ -557,9 +555,13 @@ class OlasServiceImporter:
|
|
|
557
555
|
if safe_owner_address:
|
|
558
556
|
# CASE: Owner is Safe
|
|
559
557
|
service.service_owner_multisig_address = safe_owner_address
|
|
560
|
-
service.service_owner_eoa_address =
|
|
558
|
+
service.service_owner_eoa_address = (
|
|
559
|
+
eoa_address # The EOA is the signer/controller
|
|
560
|
+
)
|
|
561
561
|
|
|
562
|
-
logger.debug(
|
|
562
|
+
logger.debug(
|
|
563
|
+
f"Extracted Safe owner address: {safe_owner_address} (Signer: {eoa_address})"
|
|
564
|
+
)
|
|
563
565
|
elif eoa_address:
|
|
564
566
|
# CASE: Owner is EOA
|
|
565
567
|
service.service_owner_eoa_address = eoa_address
|
|
@@ -767,8 +769,8 @@ class OlasServiceImporter:
|
|
|
767
769
|
safe_result = self._import_safe(
|
|
768
770
|
address=service.safe_address,
|
|
769
771
|
signers=self._get_agent_signers(service),
|
|
770
|
-
tag_suffix="multisig",
|
|
771
|
-
service_name=service.service_name
|
|
772
|
+
tag_suffix="multisig", # e.g. trader_zeta_safe
|
|
773
|
+
service_name=service.service_name,
|
|
772
774
|
)
|
|
773
775
|
if safe_result[0]:
|
|
774
776
|
result.imported_safes.append(service.safe_address)
|
|
@@ -778,19 +780,22 @@ class OlasServiceImporter:
|
|
|
778
780
|
result.errors.append(f"Safe {service.safe_address}: {safe_result[1]}")
|
|
779
781
|
|
|
780
782
|
# 2. Import Owner Safe (if it exists and is different)
|
|
781
|
-
if
|
|
782
|
-
|
|
783
|
+
if (
|
|
784
|
+
service.service_owner_multisig_address
|
|
785
|
+
and service.service_owner_multisig_address != service.safe_address
|
|
786
|
+
):
|
|
787
|
+
# Signer for Owner Safe is the EOA owner key
|
|
783
788
|
owner_signers = self._get_owner_signers(service)
|
|
784
789
|
|
|
785
790
|
safe_result = self._import_safe(
|
|
786
791
|
address=service.service_owner_multisig_address,
|
|
787
792
|
signers=owner_signers,
|
|
788
|
-
tag_suffix="owner_multisig",
|
|
789
|
-
service_name=service.service_name
|
|
793
|
+
tag_suffix="owner_multisig", # e.g. trader_zeta_owner_safe
|
|
794
|
+
service_name=service.service_name,
|
|
790
795
|
)
|
|
791
796
|
if safe_result[0]:
|
|
792
|
-
|
|
793
|
-
|
|
797
|
+
result.imported_safes.append(service.service_owner_multisig_address)
|
|
798
|
+
logger.info(f"Imported Owner Safe {service.service_owner_multisig_address}")
|
|
794
799
|
|
|
795
800
|
def _get_agent_signers(self, service: DiscoveredService) -> List[str]:
|
|
796
801
|
"""Get list of signers for the agent safe."""
|
|
@@ -926,7 +931,7 @@ class OlasServiceImporter:
|
|
|
926
931
|
address: str,
|
|
927
932
|
signers: List[str] = None,
|
|
928
933
|
tag_suffix: str = "multisig",
|
|
929
|
-
service_name: Optional[str] = None
|
|
934
|
+
service_name: Optional[str] = None,
|
|
930
935
|
) -> Tuple[bool, str]:
|
|
931
936
|
"""Import a generic Safe."""
|
|
932
937
|
if not address:
|
|
@@ -1062,4 +1067,5 @@ class OlasServiceImporter:
|
|
|
1062
1067
|
key.signature_failed = True
|
|
1063
1068
|
logger.warning(f"Error verifying signature for key {key.address}: {e}")
|
|
1064
1069
|
|
|
1065
|
-
|
|
1070
|
+
|
|
1071
|
+
FLAGS_OWNER_SAFE = "deprecated"
|
iwa/plugins/olas/plugin.py
CHANGED
|
@@ -151,7 +151,9 @@ class OlasPlugin(Plugin):
|
|
|
151
151
|
|
|
152
152
|
is_signer = key_addr in [s.lower() for s in on_chain_signers]
|
|
153
153
|
if not is_signer:
|
|
154
|
-
safe_text +=
|
|
154
|
+
safe_text += (
|
|
155
|
+
f"\n[bold red]⚠ Agent {agent_key.address} - NOT A SIGNER![/bold red]"
|
|
156
|
+
)
|
|
155
157
|
else:
|
|
156
158
|
safe_text += f" (Signer: {agent_key.address[:6]}...)"
|
|
157
159
|
|
|
@@ -182,21 +184,21 @@ class OlasPlugin(Plugin):
|
|
|
182
184
|
# 1. Display Signer/EOA Owner
|
|
183
185
|
owner_key = next((k for k in service.keys if k.role == "owner"), None)
|
|
184
186
|
if owner_key:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
187
|
+
val = owner_key.address
|
|
188
|
+
if not val.startswith("0x"):
|
|
189
|
+
val = "0x" + val
|
|
190
|
+
|
|
191
|
+
if owner_key.signature_verified:
|
|
192
|
+
val = f"[green]{val}[/green]"
|
|
193
|
+
elif not owner_key.is_encrypted:
|
|
194
|
+
val = f"[red]{val}[/red]"
|
|
195
|
+
status = "🔒 encrypted" if owner_key.is_encrypted else "🔓 plaintext"
|
|
196
|
+
table.add_row("Owner (EOA)", f"{val} {status}")
|
|
195
197
|
elif service.service_owner_eoa_address:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
# Fallback if we have an address but no key object
|
|
199
|
+
table.add_row("Owner (EOA)", service.service_owner_eoa_address)
|
|
198
200
|
else:
|
|
199
|
-
|
|
201
|
+
table.add_row("Owner (EOA)", "[yellow]N/A[/yellow]")
|
|
200
202
|
|
|
201
203
|
# 2. Display Safe Owner
|
|
202
204
|
if service.service_owner_multisig_address:
|
|
@@ -135,9 +135,7 @@ class DrainManagerMixin:
|
|
|
135
135
|
withdrawal_tag = self.wallet.get_tag_by_address(withdrawal_address) or withdrawal_address
|
|
136
136
|
multisig_tag = self.wallet.get_tag_by_address(multisig_address) or multisig_address
|
|
137
137
|
|
|
138
|
-
logger.info(
|
|
139
|
-
f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}"
|
|
140
|
-
)
|
|
138
|
+
logger.info(f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}")
|
|
141
139
|
|
|
142
140
|
# Transfer from Safe to withdrawal address
|
|
143
141
|
tx_hash = self.wallet.send(
|