iwa 0.0.58__py3-none-any.whl → 0.0.60__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 +118 -53
- iwa/core/chain/rate_limiter.py +35 -12
- 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.60.dist-info}/METADATA +1 -1
- {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/RECORD +60 -59
- tests/test_balance_service.py +0 -2
- tests/test_chain.py +1 -2
- tests/test_chain_interface.py +3 -3
- tests/test_rate_limiter.py +7 -5
- tests/test_rate_limiter_retry.py +34 -33
- tests/test_rpc_efficiency.py +4 -1
- tests/test_rpc_rate_limit.py +4 -3
- 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.60.dist-info}/WHEEL +0 -0
- {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.58.dist-info → iwa-0.0.60.dist-info}/top_level.txt +0 -0
iwa/core/pricing.py
CHANGED
|
@@ -4,9 +4,9 @@ import time
|
|
|
4
4
|
from datetime import datetime, timedelta
|
|
5
5
|
from typing import Dict, Optional
|
|
6
6
|
|
|
7
|
-
import requests
|
|
8
7
|
from loguru import logger
|
|
9
8
|
|
|
9
|
+
from iwa.core.http import create_retry_session
|
|
10
10
|
from iwa.core.secrets import secrets
|
|
11
11
|
|
|
12
12
|
# Global cache shared across all PriceService instances
|
|
@@ -28,28 +28,12 @@ class PriceService:
|
|
|
28
28
|
if self.secrets.coingecko_api_key
|
|
29
29
|
else None
|
|
30
30
|
)
|
|
31
|
-
self.session =
|
|
32
|
-
# Configure retry strategy
|
|
33
|
-
from requests.adapters import HTTPAdapter
|
|
34
|
-
from urllib3.util.retry import Retry
|
|
35
|
-
|
|
36
|
-
retry_strategy = Retry(
|
|
37
|
-
total=3,
|
|
38
|
-
backoff_factor=1,
|
|
39
|
-
status_forcelist=[429, 500, 502, 503, 504],
|
|
40
|
-
)
|
|
41
|
-
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
42
|
-
self.session.mount("https://", adapter)
|
|
43
|
-
self.session.mount("http://", adapter)
|
|
31
|
+
self.session = create_retry_session()
|
|
44
32
|
|
|
45
33
|
def close(self):
|
|
46
34
|
"""Close the session."""
|
|
47
35
|
self.session.close()
|
|
48
36
|
|
|
49
|
-
def __del__(self):
|
|
50
|
-
"""Cleanup on deletion."""
|
|
51
|
-
self.close()
|
|
52
|
-
|
|
53
37
|
def get_token_price(self, token_id: str, vs_currency: str = "eur") -> Optional[float]:
|
|
54
38
|
"""Get token price in specified currency.
|
|
55
39
|
|
|
@@ -115,9 +99,7 @@ class PriceService:
|
|
|
115
99
|
return float(data[token_id][vs_currency])
|
|
116
100
|
|
|
117
101
|
# If we got response but price not found, it's likely a wrong ID
|
|
118
|
-
logger.debug(
|
|
119
|
-
f"Price for {token_id} in {vs_currency} not found in response: {data}"
|
|
120
|
-
)
|
|
102
|
+
logger.debug(f"Price for {token_id} in {vs_currency} not found in response: {data}")
|
|
121
103
|
return None
|
|
122
104
|
|
|
123
105
|
except Exception as e:
|
iwa/core/rpc_monitor.py
CHANGED
iwa/core/services/balance.py
CHANGED
iwa/core/services/safe.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Safe service module."""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, List, Optional, Tuple
|
|
3
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
from safe_eth.eth import EthereumClient
|
|
@@ -35,6 +35,7 @@ class SafeService:
|
|
|
35
35
|
"""Initialize SafeService."""
|
|
36
36
|
self.key_storage = key_storage
|
|
37
37
|
self.account_service = account_service
|
|
38
|
+
self._client_cache: Dict[str, EthereumClient] = {}
|
|
38
39
|
|
|
39
40
|
def create_safe(
|
|
40
41
|
self,
|
|
@@ -102,7 +103,12 @@ class SafeService:
|
|
|
102
103
|
|
|
103
104
|
# Use ChainInterface which has proper RPC rotation and parsing
|
|
104
105
|
chain_interface = ChainInterfaces().get(chain_name)
|
|
105
|
-
|
|
106
|
+
rpc_url = chain_interface.current_rpc
|
|
107
|
+
|
|
108
|
+
if rpc_url not in self._client_cache:
|
|
109
|
+
self._client_cache[rpc_url] = EthereumClient(rpc_url)
|
|
110
|
+
|
|
111
|
+
return self._client_cache[rpc_url]
|
|
106
112
|
|
|
107
113
|
def _deploy_safe_contract(
|
|
108
114
|
self,
|
|
@@ -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}")
|