iwa 0.0.17__py3-none-any.whl → 0.0.18__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 +1 -65
- iwa/core/pricing.py +1 -4
- iwa/core/services/transaction.py +72 -70
- iwa/core/services/transfer/erc20.py +8 -0
- iwa/core/services/transfer/native.py +47 -13
- iwa/plugins/olas/contracts/activity_checker.py +2 -51
- iwa/plugins/olas/service_manager/mech.py +43 -25
- {iwa-0.0.17.dist-info → iwa-0.0.18.dist-info}/METADATA +1 -1
- {iwa-0.0.17.dist-info → iwa-0.0.18.dist-info}/RECORD +18 -18
- tests/test_chain.py +2 -97
- tests/test_legacy_wallet.py +6 -6
- tests/test_service_transaction.py +23 -8
- tests/test_staking_router.py +14 -6
- tests/test_transaction_service.py +20 -6
- {iwa-0.0.17.dist-info → iwa-0.0.18.dist-info}/WHEEL +0 -0
- {iwa-0.0.17.dist-info → iwa-0.0.18.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.17.dist-info → iwa-0.0.18.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.17.dist-info → iwa-0.0.18.dist-info}/top_level.txt +0 -0
iwa/core/chain/interface.py
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import threading
|
|
4
4
|
import time
|
|
5
|
-
from typing import Callable, Dict, Optional,
|
|
5
|
+
from typing import Callable, Dict, Optional, TypeVar, Union
|
|
6
6
|
|
|
7
|
-
from eth_account.datastructures import SignedTransaction
|
|
8
7
|
from web3 import Web3
|
|
9
8
|
|
|
10
9
|
from iwa.core.chain.errors import TenderlyQuotaExceededError, sanitize_rpc_url
|
|
@@ -449,69 +448,6 @@ class ChainInterface:
|
|
|
449
448
|
|
|
450
449
|
return False
|
|
451
450
|
|
|
452
|
-
def send_native_transfer(
|
|
453
|
-
self,
|
|
454
|
-
from_address: EthereumAddress,
|
|
455
|
-
to_address: EthereumAddress,
|
|
456
|
-
value_wei: int,
|
|
457
|
-
sign_callback: Callable[[dict], SignedTransaction],
|
|
458
|
-
) -> Tuple[bool, Optional[str]]:
|
|
459
|
-
"""Send native currency transaction with retry logic."""
|
|
460
|
-
|
|
461
|
-
def _do_transfer() -> Tuple[bool, Optional[str]]:
|
|
462
|
-
tx = {
|
|
463
|
-
"from": from_address,
|
|
464
|
-
"to": to_address,
|
|
465
|
-
"value": value_wei,
|
|
466
|
-
"nonce": self.web3.eth.get_transaction_count(from_address),
|
|
467
|
-
"chainId": self.chain.chain_id,
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
balance_wei = self.get_native_balance_wei(from_address)
|
|
471
|
-
gas_price = self.web3.eth.gas_price
|
|
472
|
-
gas_estimate = self.web3.eth.estimate_gas(tx)
|
|
473
|
-
required_wei = value_wei + (gas_estimate * gas_price)
|
|
474
|
-
|
|
475
|
-
if balance_wei < required_wei:
|
|
476
|
-
logger.error(
|
|
477
|
-
f"Insufficient balance. "
|
|
478
|
-
f"Balance: {self.web3.from_wei(balance_wei, 'ether'):.4f} "
|
|
479
|
-
f"{self.chain.native_currency}, "
|
|
480
|
-
f"Required: {self.web3.from_wei(required_wei, 'ether'):.4f} "
|
|
481
|
-
f"{self.chain.native_currency}"
|
|
482
|
-
)
|
|
483
|
-
return False, None
|
|
484
|
-
|
|
485
|
-
tx["gas"] = gas_estimate
|
|
486
|
-
tx["gasPrice"] = gas_price
|
|
487
|
-
|
|
488
|
-
signed_tx = sign_callback(tx)
|
|
489
|
-
txn_hash = self.web3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
490
|
-
receipt = self.web3.eth.wait_for_transaction_receipt(txn_hash)
|
|
491
|
-
|
|
492
|
-
status = getattr(receipt, "status", None)
|
|
493
|
-
if status is None and isinstance(receipt, dict):
|
|
494
|
-
status = receipt.get("status")
|
|
495
|
-
|
|
496
|
-
if receipt and status == 1:
|
|
497
|
-
self.wait_for_no_pending_tx(from_address)
|
|
498
|
-
logger.info(f"Transaction sent successfully. Tx Hash: {txn_hash.hex()}")
|
|
499
|
-
# Check Tenderly block limit after each successful transaction
|
|
500
|
-
self.check_block_limit()
|
|
501
|
-
return True, receipt["transactionHash"].hex()
|
|
502
|
-
|
|
503
|
-
logger.error("Transaction failed (status != 1)")
|
|
504
|
-
return False, None
|
|
505
|
-
|
|
506
|
-
try:
|
|
507
|
-
return self.with_retry(
|
|
508
|
-
_do_transfer,
|
|
509
|
-
operation_name=f"native_transfer to {str(to_address)[:10]}...",
|
|
510
|
-
)
|
|
511
|
-
except Exception as e:
|
|
512
|
-
logger.exception(f"Native transfer failed: {e}")
|
|
513
|
-
return False, None
|
|
514
|
-
|
|
515
451
|
def get_token_address(self, token_name: str) -> Optional[EthereumAddress]:
|
|
516
452
|
"""Get token address by name"""
|
|
517
453
|
return self.chain.get_token_address(token_name)
|
iwa/core/pricing.py
CHANGED
|
@@ -19,7 +19,6 @@ class PriceService:
|
|
|
19
19
|
"""Service to fetch token prices from CoinGecko."""
|
|
20
20
|
|
|
21
21
|
BASE_URL = "https://api.coingecko.com/api/v3"
|
|
22
|
-
DEMO_URL = "https://demo-api.coingecko.com/api/v3"
|
|
23
22
|
|
|
24
23
|
def __init__(self):
|
|
25
24
|
"""Initialize PriceService."""
|
|
@@ -60,9 +59,7 @@ class PriceService:
|
|
|
60
59
|
max_retries = 2
|
|
61
60
|
for attempt in range(max_retries + 1):
|
|
62
61
|
try:
|
|
63
|
-
|
|
64
|
-
# NOTE: Demo URL is significantly more reliable for demo keys
|
|
65
|
-
base_url = self.DEMO_URL if self.api_key else self.BASE_URL
|
|
62
|
+
base_url = self.BASE_URL
|
|
66
63
|
url = f"{base_url}/simple/price"
|
|
67
64
|
params = {"ids": token_id, "vs_currencies": vs_currency}
|
|
68
65
|
headers = {}
|
iwa/core/services/transaction.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Transaction service module."""
|
|
2
2
|
|
|
3
|
-
import time
|
|
4
3
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
|
5
4
|
|
|
6
5
|
from loguru import logger
|
|
@@ -31,32 +30,37 @@ class TransferLogger:
|
|
|
31
30
|
self.account_service = account_service
|
|
32
31
|
self.chain_interface = chain_interface
|
|
33
32
|
|
|
34
|
-
def log_transfers(self, receipt: Dict
|
|
33
|
+
def log_transfers(self, receipt: Dict) -> None:
|
|
35
34
|
"""Log all transfers (ERC20 and native) from a transaction receipt.
|
|
36
35
|
|
|
37
36
|
Args:
|
|
38
37
|
receipt: Transaction receipt containing logs.
|
|
39
|
-
tx: Original transaction dict.
|
|
40
38
|
|
|
41
39
|
"""
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
if
|
|
45
|
-
|
|
40
|
+
# Get the original transaction to check for native value transfer
|
|
41
|
+
tx_hash = receipt.get("transactionHash") or getattr(receipt, "transactionHash", None)
|
|
42
|
+
if tx_hash:
|
|
43
|
+
try:
|
|
44
|
+
tx = self.chain_interface.web3.eth.get_transaction(tx_hash)
|
|
45
|
+
native_value = getattr(tx, "value", 0) or tx.get("value", 0) if isinstance(tx, dict) else getattr(tx, "value", 0)
|
|
46
|
+
if native_value and int(native_value) > 0:
|
|
47
|
+
from_addr = getattr(tx, "from", "") if hasattr(tx, "from") else tx.get("from", "")
|
|
48
|
+
# Handle AttributeDict's special 'from' attribute
|
|
49
|
+
if not from_addr and hasattr(tx, "__getitem__"):
|
|
50
|
+
from_addr = tx["from"]
|
|
51
|
+
to_addr = getattr(tx, "to", "") or (tx.get("to", "") if isinstance(tx, dict) else "")
|
|
52
|
+
self._log_native_transfer(from_addr, to_addr, native_value)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.debug(f"Could not get tx for native transfer logging: {e}")
|
|
46
55
|
|
|
47
56
|
# Log ERC20 transfers from event logs
|
|
48
|
-
logs = receipt.get("logs", [])
|
|
49
|
-
if hasattr(receipt, "logs"):
|
|
50
|
-
logs = receipt.logs
|
|
57
|
+
logs = receipt.get("logs", []) if isinstance(receipt, dict) else getattr(receipt, "logs", [])
|
|
51
58
|
|
|
52
59
|
for log in logs:
|
|
53
60
|
self._process_log(log)
|
|
54
61
|
|
|
55
|
-
def _log_native_transfer(self,
|
|
62
|
+
def _log_native_transfer(self, from_addr: str, to_addr: str, value_wei: int) -> None:
|
|
56
63
|
"""Log a native currency transfer."""
|
|
57
|
-
from_addr = tx.get("from", "")
|
|
58
|
-
to_addr = tx.get("to", "")
|
|
59
|
-
|
|
60
64
|
from_label = self._resolve_address_label(from_addr)
|
|
61
65
|
to_label = self._resolve_address_label(to_addr)
|
|
62
66
|
|
|
@@ -213,53 +217,67 @@ class TransactionService:
|
|
|
213
217
|
chain_name: str = "gnosis",
|
|
214
218
|
tags: Optional[List[str]] = None,
|
|
215
219
|
) -> Tuple[bool, Dict]:
|
|
216
|
-
"""Sign and send a transaction
|
|
220
|
+
"""Sign and send a transaction using unified retry mechanism.
|
|
221
|
+
|
|
222
|
+
Uses ChainInterface.with_retry() for consistent RPC rotation and retry logic.
|
|
223
|
+
Gas errors are handled by increasing gas and retrying within the same mechanism.
|
|
224
|
+
"""
|
|
217
225
|
chain_interface = ChainInterfaces().get(chain_name)
|
|
218
226
|
tx = dict(transaction)
|
|
219
|
-
max_retries = 10
|
|
220
227
|
|
|
221
228
|
if not self._prepare_transaction(tx, signer_address_or_tag, chain_interface):
|
|
222
229
|
return False, {}
|
|
223
230
|
|
|
224
|
-
|
|
231
|
+
# Mutable state for retry attempts
|
|
232
|
+
state = {"gas_retries": 0, "max_gas_retries": 5}
|
|
233
|
+
|
|
234
|
+
def _do_sign_send_wait() -> Tuple[bool, Dict, bytes]:
|
|
235
|
+
"""Inner operation wrapped by with_retry."""
|
|
225
236
|
try:
|
|
226
237
|
signed_txn = self.key_storage.sign_transaction(tx, signer_address_or_tag)
|
|
227
|
-
txn_hash = chain_interface.web3.eth.send_raw_transaction(
|
|
228
|
-
|
|
229
|
-
# Use chain_interface.with_retry for waiting for receipt to handle timeouts/RPC errors
|
|
230
|
-
def wait_for_receipt(tx_h=txn_hash):
|
|
231
|
-
return chain_interface.web3.eth.wait_for_transaction_receipt(tx_h)
|
|
232
|
-
|
|
233
|
-
receipt = chain_interface.with_retry(
|
|
234
|
-
wait_for_receipt, operation_name="wait_for_receipt"
|
|
238
|
+
txn_hash = chain_interface.web3.eth.send_raw_transaction(
|
|
239
|
+
signed_txn.raw_transaction
|
|
235
240
|
)
|
|
241
|
+
receipt = chain_interface.web3.eth.wait_for_transaction_receipt(txn_hash)
|
|
236
242
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
logger.info(f"Transaction sent successfully. Tx Hash: {txn_hash.hex()}")
|
|
241
|
-
|
|
242
|
-
self._log_successful_transaction(
|
|
243
|
-
receipt, tx, signer_account, chain_name, txn_hash, tags, chain_interface
|
|
244
|
-
)
|
|
245
|
-
return True, receipt
|
|
243
|
+
status = getattr(receipt, "status", None)
|
|
244
|
+
if status is None and isinstance(receipt, dict):
|
|
245
|
+
status = receipt.get("status")
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
if receipt and status == 1:
|
|
248
|
+
return True, receipt, txn_hash
|
|
249
|
+
# Transaction mined but reverted - don't retry
|
|
248
250
|
logger.error("Transaction failed (status 0).")
|
|
249
|
-
|
|
251
|
+
raise ValueError("Transaction reverted")
|
|
250
252
|
|
|
251
253
|
except web3_exceptions.Web3RPCError as e:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
# Handle gas errors by increasing gas and re-raising
|
|
255
|
+
self._handle_gas_retry(e, tx, state)
|
|
256
|
+
raise # Re-raise to trigger with_retry's retry mechanism
|
|
255
257
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
try:
|
|
259
|
+
success, receipt, txn_hash = chain_interface.with_retry(
|
|
260
|
+
_do_sign_send_wait,
|
|
261
|
+
operation_name=f"sign_and_send to {tx.get('to', 'unknown')[:10]}...",
|
|
262
|
+
)
|
|
263
|
+
if success:
|
|
264
|
+
signer_account = self.account_service.resolve_account(signer_address_or_tag)
|
|
265
|
+
chain_interface.wait_for_no_pending_tx(signer_account.address)
|
|
266
|
+
logger.info(f"Transaction sent successfully. Tx Hash: {txn_hash.hex()}")
|
|
267
|
+
self._log_successful_transaction(
|
|
268
|
+
receipt, tx, signer_account, chain_name, txn_hash, tags, chain_interface
|
|
269
|
+
)
|
|
270
|
+
return True, receipt
|
|
271
|
+
return False, {}
|
|
272
|
+
except ValueError as e:
|
|
273
|
+
# Transaction reverted - already logged
|
|
274
|
+
if "reverted" in str(e).lower():
|
|
260
275
|
return False, {}
|
|
261
|
-
|
|
262
|
-
|
|
276
|
+
logger.exception(f"Transaction failed: {e}")
|
|
277
|
+
return False, {}
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.exception(f"Transaction failed after retries: {e}")
|
|
280
|
+
return False, {}
|
|
263
281
|
|
|
264
282
|
def _prepare_transaction(self, tx: dict, signer_tag: str, chain_interface) -> bool:
|
|
265
283
|
"""Ensure nonce and chainId are set."""
|
|
@@ -274,32 +292,16 @@ class TransactionService:
|
|
|
274
292
|
tx["chainId"] = chain_interface.chain.chain_id
|
|
275
293
|
return True
|
|
276
294
|
|
|
277
|
-
def
|
|
278
|
-
|
|
279
|
-
if self._is_gas_too_low_error(
|
|
280
|
-
logger.warning(
|
|
281
|
-
f"Gas too low error detected. Retrying with increased gas (Attempt {attempt}/{max_retries})..."
|
|
282
|
-
)
|
|
295
|
+
def _handle_gas_retry(self, e: Exception, tx: dict, state: dict) -> None:
|
|
296
|
+
"""Increase gas if error is gas-related and retries remaining."""
|
|
297
|
+
if self._is_gas_too_low_error(str(e)) and state["gas_retries"] < state["max_gas_retries"]:
|
|
283
298
|
current_gas = int(tx.get("gas", 30_000))
|
|
284
299
|
tx["gas"] = int(current_gas * 1.5)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return False
|
|
291
|
-
|
|
292
|
-
def _handle_generic_error(self, e, chain_interface, attempt, max_retries) -> bool:
|
|
293
|
-
if attempt < max_retries:
|
|
294
|
-
logger.warning(f"Error encountered: {e}. Attempting to rotate RPC...")
|
|
295
|
-
|
|
296
|
-
if chain_interface.rotate_rpc():
|
|
297
|
-
logger.info("Retrying with new RPC...")
|
|
298
|
-
# Exponential backoff
|
|
299
|
-
time.sleep(min(2**attempt, 30))
|
|
300
|
-
return True
|
|
301
|
-
logger.exception(f"Unexpected error sending transaction: {e}")
|
|
302
|
-
return False
|
|
300
|
+
state["gas_retries"] += 1
|
|
301
|
+
logger.warning(
|
|
302
|
+
f"Gas too low, increasing to {tx['gas']} "
|
|
303
|
+
f"(attempt {state['gas_retries']}/{state['max_gas_retries']})"
|
|
304
|
+
)
|
|
303
305
|
|
|
304
306
|
def _log_successful_transaction(
|
|
305
307
|
self, receipt, tx, signer_account, chain_name, txn_hash, tags, chain_interface
|
|
@@ -323,7 +325,7 @@ class TransactionService:
|
|
|
323
325
|
|
|
324
326
|
# Log transfer events (ERC20 and native value)
|
|
325
327
|
transfer_logger = TransferLogger(self.account_service, chain_interface)
|
|
326
|
-
transfer_logger.log_transfers(receipt
|
|
328
|
+
transfer_logger.log_transfers(receipt)
|
|
327
329
|
|
|
328
330
|
except Exception as log_err:
|
|
329
331
|
logger.warning(f"Failed to log transaction: {log_err}")
|
|
@@ -64,6 +64,14 @@ class ERC20TransferMixin:
|
|
|
64
64
|
value_eur=v_eur,
|
|
65
65
|
tags=["erc20-transfer", "safe-transaction"],
|
|
66
66
|
)
|
|
67
|
+
|
|
68
|
+
# Log transfers extracted from receipt events
|
|
69
|
+
if receipt:
|
|
70
|
+
from iwa.core.services.transaction import TransferLogger
|
|
71
|
+
|
|
72
|
+
transfer_logger = TransferLogger(self.account_service, interface)
|
|
73
|
+
transfer_logger.log_transfers(receipt)
|
|
74
|
+
|
|
67
75
|
return tx_hash
|
|
68
76
|
|
|
69
77
|
def _send_erc20_via_eoa(
|
|
@@ -61,6 +61,15 @@ class NativeTransferMixin:
|
|
|
61
61
|
value_eur=v_eur,
|
|
62
62
|
tags=["native-transfer", "safe-transaction"],
|
|
63
63
|
)
|
|
64
|
+
|
|
65
|
+
# Log transfers extracted from receipt events
|
|
66
|
+
if receipt:
|
|
67
|
+
from iwa.core.services.transaction import TransferLogger
|
|
68
|
+
|
|
69
|
+
interface = ChainInterfaces().get(chain_name)
|
|
70
|
+
transfer_logger = TransferLogger(self.account_service, interface)
|
|
71
|
+
transfer_logger.log_transfers(receipt)
|
|
72
|
+
|
|
64
73
|
return tx_hash
|
|
65
74
|
|
|
66
75
|
def _send_native_via_eoa(
|
|
@@ -74,20 +83,38 @@ class NativeTransferMixin:
|
|
|
74
83
|
to_tag: Optional[str],
|
|
75
84
|
token_symbol: str,
|
|
76
85
|
) -> Optional[str]:
|
|
77
|
-
"""Send native currency via EOA
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
"""Send native currency via EOA using unified TransactionService."""
|
|
87
|
+
# Build transaction dict
|
|
88
|
+
try:
|
|
89
|
+
gas_price = chain_interface.web3.eth.gas_price
|
|
90
|
+
gas_estimate = chain_interface.web3.eth.estimate_gas({
|
|
91
|
+
"from": from_account.address,
|
|
92
|
+
"to": to_address,
|
|
93
|
+
"value": amount_wei,
|
|
94
|
+
})
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Failed to estimate gas for native transfer: {e}")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
tx = {
|
|
100
|
+
"from": from_account.address,
|
|
101
|
+
"to": to_address,
|
|
102
|
+
"value": amount_wei,
|
|
103
|
+
"gas": gas_estimate,
|
|
104
|
+
"gasPrice": gas_price,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Use unified TransactionService
|
|
108
|
+
success, receipt = self.transaction_service.sign_and_send(
|
|
109
|
+
tx, from_account.address, chain_name, tags=["native-transfer"]
|
|
83
110
|
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
|
|
112
|
+
if success and receipt:
|
|
113
|
+
tx_hash = receipt.get("transactionHash", b"")
|
|
114
|
+
if hasattr(tx_hash, "hex"):
|
|
115
|
+
tx_hash = tx_hash.hex()
|
|
116
|
+
elif isinstance(tx_hash, bytes):
|
|
117
|
+
tx_hash = tx_hash.hex()
|
|
91
118
|
|
|
92
119
|
gas_cost, gas_value_eur = self._calculate_gas_info(receipt, chain_name)
|
|
93
120
|
p_eur, v_eur = self._get_token_price_info(token_symbol, amount_wei, chain_name)
|
|
@@ -106,6 +133,13 @@ class NativeTransferMixin:
|
|
|
106
133
|
value_eur=v_eur,
|
|
107
134
|
tags=["native-transfer"],
|
|
108
135
|
)
|
|
136
|
+
|
|
137
|
+
# Log transfers extracted from receipt events
|
|
138
|
+
from iwa.core.services.transaction import TransferLogger
|
|
139
|
+
|
|
140
|
+
transfer_logger = TransferLogger(self.account_service, chain_interface)
|
|
141
|
+
transfer_logger.log_transfers(receipt)
|
|
142
|
+
|
|
109
143
|
return tx_hash
|
|
110
144
|
return None
|
|
111
145
|
|
|
@@ -10,8 +10,6 @@ requests relative to the time elapsed since the last checkpoint.
|
|
|
10
10
|
|
|
11
11
|
from typing import Tuple
|
|
12
12
|
|
|
13
|
-
from web3 import Web3
|
|
14
|
-
|
|
15
13
|
from iwa.core.constants import DEFAULT_MECH_CONTRACT_ADDRESS
|
|
16
14
|
from iwa.core.types import EthereumAddress
|
|
17
15
|
from iwa.plugins.olas.contracts.base import OLAS_ABI_PATH, ContractInstance
|
|
@@ -70,9 +68,6 @@ class ActivityCheckerContract(ContractInstance):
|
|
|
70
68
|
def get_multisig_nonces(self, multisig: EthereumAddress) -> Tuple[int, int]:
|
|
71
69
|
"""Get the nonces for a multisig address.
|
|
72
70
|
|
|
73
|
-
This method reads directly from the source contracts to ensure fresh data
|
|
74
|
-
and compatibility with Legacy/MM contracts.
|
|
75
|
-
|
|
76
71
|
Args:
|
|
77
72
|
multisig: The multisig address to check.
|
|
78
73
|
|
|
@@ -82,52 +77,8 @@ class ActivityCheckerContract(ContractInstance):
|
|
|
82
77
|
- mech_requests_count: Total mech requests made
|
|
83
78
|
|
|
84
79
|
"""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
# Minimal ABI for Safe nonce
|
|
89
|
-
safe_abi = [{"name": "nonce", "type": "function", "inputs": [], "outputs": [{"type": "uint256"}]}]
|
|
90
|
-
safe_contract = self.chain_interface.web3._web3.eth.contract(
|
|
91
|
-
address=Web3.to_checksum_address(multisig), abi=safe_abi
|
|
92
|
-
)
|
|
93
|
-
safe_nonce = safe_contract.functions.nonce().call()
|
|
94
|
-
except Exception as e:
|
|
95
|
-
# Fallback or log error? If safe read fails, something is very wrong.
|
|
96
|
-
# But we don't want to crash the whole status check if possible.
|
|
97
|
-
# For now, let's log and keep 0 or re-raise if critical.
|
|
98
|
-
# safe_nonce is critical for liveness check (diffNonces).
|
|
99
|
-
from loguru import logger
|
|
100
|
-
logger.warning(f"Failed to read Safe nonce for {multisig}: {e}")
|
|
101
|
-
|
|
102
|
-
# 2. Get Mech Requests Count
|
|
103
|
-
mech_requests = 0
|
|
104
|
-
|
|
105
|
-
if self.mech_marketplace:
|
|
106
|
-
# Case A: Marketplace (MM)
|
|
107
|
-
try:
|
|
108
|
-
# Minimal ABI for Marketplace mapRequestCounts
|
|
109
|
-
mp_abi = [{"name": "mapRequestCounts", "type": "function", "inputs": [{"type": "address"}], "outputs": [{"type": "uint256"}]}]
|
|
110
|
-
mp_contract = self.chain_interface.web3._web3.eth.contract(
|
|
111
|
-
address=Web3.to_checksum_address(self.mech_marketplace), abi=mp_abi
|
|
112
|
-
)
|
|
113
|
-
mech_requests = mp_contract.functions.mapRequestCounts(multisig).call()
|
|
114
|
-
except Exception as e:
|
|
115
|
-
from loguru import logger
|
|
116
|
-
logger.warning(f"Failed to read Marketplace requests for {multisig}: {e}")
|
|
117
|
-
else:
|
|
118
|
-
# Case B: Legacy (AgentMech)
|
|
119
|
-
try:
|
|
120
|
-
# Minimal ABI for AgentMech getRequestsCount
|
|
121
|
-
mech_abi = [{"name": "getRequestsCount", "type": "function", "inputs": [{"type": "address"}], "outputs": [{"type": "uint256"}]}]
|
|
122
|
-
mech_contract = self.chain_interface.web3._web3.eth.contract(
|
|
123
|
-
address=Web3.to_checksum_address(self.agent_mech), abi=mech_abi
|
|
124
|
-
)
|
|
125
|
-
mech_requests = mech_contract.functions.getRequestsCount(multisig).call()
|
|
126
|
-
except Exception as e:
|
|
127
|
-
from loguru import logger
|
|
128
|
-
logger.warning(f"Failed to read AgentMech requests for {multisig}: {e}")
|
|
129
|
-
|
|
130
|
-
return (safe_nonce, mech_requests)
|
|
80
|
+
nonces = self.contract.functions.getMultisigNonces(multisig).call()
|
|
81
|
+
return (nonces[0], nonces[1])
|
|
131
82
|
|
|
132
83
|
def is_ratio_pass(
|
|
133
84
|
self,
|
|
@@ -140,11 +140,13 @@ class MechManagerMixin:
|
|
|
140
140
|
)
|
|
141
141
|
if use_marketplace:
|
|
142
142
|
priority_mech = priority_mech or detected_priority_mech
|
|
143
|
+
mech_address = mech_address or detected_marketplace
|
|
143
144
|
|
|
144
145
|
if use_marketplace:
|
|
145
146
|
return self._send_marketplace_mech_request(
|
|
146
147
|
data=data,
|
|
147
148
|
value=value,
|
|
149
|
+
marketplace_address=mech_address,
|
|
148
150
|
priority_mech=priority_mech,
|
|
149
151
|
max_delivery_rate=max_delivery_rate,
|
|
150
152
|
payment_type=payment_type,
|
|
@@ -260,10 +262,39 @@ class MechManagerMixin:
|
|
|
260
262
|
|
|
261
263
|
return True
|
|
262
264
|
|
|
265
|
+
def _resolve_marketplace_config(
|
|
266
|
+
self, marketplace_addr: Optional[str], priority_addr: Optional[str]
|
|
267
|
+
) -> tuple[str, str]:
|
|
268
|
+
"""Resolve marketplace and priority mech addresses. Returns (marketplace, priority)."""
|
|
269
|
+
chain_name = self.chain_name if self.service else getattr(self, "chain_name", "gnosis")
|
|
270
|
+
protocol_contracts = OLAS_CONTRACTS.get(chain_name, {})
|
|
271
|
+
|
|
272
|
+
resolved_mp = marketplace_addr or protocol_contracts.get("OLAS_MECH_MARKETPLACE")
|
|
273
|
+
if not resolved_mp:
|
|
274
|
+
raise ValueError(f"Mech Marketplace address not found for chain {chain_name}")
|
|
275
|
+
|
|
276
|
+
if not priority_addr:
|
|
277
|
+
raise ValueError("priority_mech is required for marketplace requests")
|
|
278
|
+
|
|
279
|
+
return str(resolved_mp), Web3.to_checksum_address(priority_addr)
|
|
280
|
+
|
|
281
|
+
def _prepare_marketplace_params(
|
|
282
|
+
self,
|
|
283
|
+
value: Optional[int],
|
|
284
|
+
max_delivery_rate: Optional[int],
|
|
285
|
+
payment_type: Optional[bytes],
|
|
286
|
+
) -> tuple[int, int, bytes]:
|
|
287
|
+
"""Prepare default values for marketplace parameters."""
|
|
288
|
+
p_type = payment_type or bytes.fromhex(PAYMENT_TYPE_NATIVE)
|
|
289
|
+
val = value if value is not None else 10_000_000_000_000_000
|
|
290
|
+
rate = max_delivery_rate if max_delivery_rate is not None else val
|
|
291
|
+
return val, rate, p_type
|
|
292
|
+
|
|
263
293
|
def _send_marketplace_mech_request(
|
|
264
294
|
self,
|
|
265
295
|
data: bytes,
|
|
266
296
|
value: Optional[int] = None,
|
|
297
|
+
marketplace_address: Optional[str] = None,
|
|
267
298
|
priority_mech: Optional[str] = None,
|
|
268
299
|
max_delivery_rate: Optional[int] = None,
|
|
269
300
|
payment_type: Optional[bytes] = None,
|
|
@@ -275,43 +306,30 @@ class MechManagerMixin:
|
|
|
275
306
|
logger.error("No active service")
|
|
276
307
|
return None
|
|
277
308
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
logger.error(f"Mech Marketplace address not found for chain {chain_name}")
|
|
285
|
-
return None
|
|
286
|
-
|
|
287
|
-
if not priority_mech:
|
|
288
|
-
logger.error("priority_mech is required for marketplace requests")
|
|
309
|
+
try:
|
|
310
|
+
marketplace_address, priority_mech = self._resolve_marketplace_config(
|
|
311
|
+
marketplace_address, priority_mech
|
|
312
|
+
)
|
|
313
|
+
except ValueError as e:
|
|
314
|
+
logger.error(e)
|
|
289
315
|
return None
|
|
290
316
|
|
|
291
|
-
|
|
292
|
-
marketplace = MechMarketplaceContract(str(marketplace_address), chain_name=chain_name)
|
|
317
|
+
marketplace = MechMarketplaceContract(marketplace_address, chain_name=self.chain_name)
|
|
293
318
|
|
|
294
319
|
if not self._validate_priority_mech(marketplace, priority_mech):
|
|
295
320
|
return None
|
|
296
321
|
|
|
297
|
-
# Set defaults for payment
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if value is None:
|
|
302
|
-
value = 10_000_000_000_000_000
|
|
303
|
-
logger.info(f"Using default value: {value} wei (0.01 xDAI)")
|
|
304
|
-
|
|
305
|
-
if max_delivery_rate is None:
|
|
306
|
-
max_delivery_rate = value
|
|
307
|
-
logger.info(f"Using value as max_delivery_rate: {max_delivery_rate}")
|
|
322
|
+
# Set defaults for payment and delivery
|
|
323
|
+
value, max_delivery_rate, payment_type = self._prepare_marketplace_params(
|
|
324
|
+
value, max_delivery_rate, payment_type
|
|
325
|
+
)
|
|
308
326
|
|
|
309
327
|
if not self._validate_marketplace_params(marketplace, response_timeout, payment_type):
|
|
310
328
|
return None
|
|
311
329
|
|
|
312
330
|
# Prepare transaction
|
|
313
331
|
tx_data = marketplace.prepare_request_tx(
|
|
314
|
-
from_address=multisig_address,
|
|
332
|
+
from_address=self.service.multisig_address,
|
|
315
333
|
request_data=data,
|
|
316
334
|
priority_mech=priority_mech,
|
|
317
335
|
response_timeout=response_timeout,
|
|
@@ -10,7 +10,7 @@ iwa/core/mnemonic.py,sha256=LiG1VmpydQoHQ0pHUJ1OIlrWJry47VSMnOqPM_Yk-O8,12930
|
|
|
10
10
|
iwa/core/models.py,sha256=kBQ0cBe6uFmL2QfW7mjKiMFeZxhT-FRN-RyK3Ko0vE8,12849
|
|
11
11
|
iwa/core/monitor.py,sha256=OmhKVMkfhvtxig3wDUL6iGwBIClTx0YUqMncCao4SqI,7953
|
|
12
12
|
iwa/core/plugins.py,sha256=FLvOG4S397fKi0aTH1fWBEtexn4yvGv_QzGWqFrhSKE,1102
|
|
13
|
-
iwa/core/pricing.py,sha256=
|
|
13
|
+
iwa/core/pricing.py,sha256=uENpqVMmuogZHctsLuEsU7WJ1cLSNAI-rZTtbpTDjeQ,4048
|
|
14
14
|
iwa/core/secrets.py,sha256=U7DZKrwKuSOFV00Ij3ISrrO1cWn_t1GBW_0PyAqjcD4,2588
|
|
15
15
|
iwa/core/tables.py,sha256=y7Cg67PAGHYVMVyAjbo_CQ9t2iz7UXE-OTuUHRyFRTo,2021
|
|
16
16
|
iwa/core/test.py,sha256=gey0dql5eajo1itOhgkSrgfyGWue2eSfpr0xzX3vc38,643
|
|
@@ -20,7 +20,7 @@ iwa/core/utils.py,sha256=shJuANkXSWVO3NF49syPA9hCG7H5AzaMJOG8V4fo6IM,4279
|
|
|
20
20
|
iwa/core/wallet.py,sha256=sNFK-_0y-EgeLpNHt9o5tCqTM0oVqJra-eAWjR7AgyU,13038
|
|
21
21
|
iwa/core/chain/__init__.py,sha256=XJMmn0ed-_aVkY2iEMKpuTxPgIKBd41dexSVmEZTa-o,1604
|
|
22
22
|
iwa/core/chain/errors.py,sha256=9SEbhxZ-qASPkzt-DoI51qq0GRJVqRgqgL720gO7a64,1275
|
|
23
|
-
iwa/core/chain/interface.py,sha256=
|
|
23
|
+
iwa/core/chain/interface.py,sha256=uUIUo9Q6Sr4h9AjqFe6WqQzdbMs9VkJcUj0mgd6lfx4,18602
|
|
24
24
|
iwa/core/chain/manager.py,sha256=cFEzh6pK5OyVhjhpeMAqhc9RnRDQR1DjIGiGKp-FXBI,1159
|
|
25
25
|
iwa/core/chain/models.py,sha256=0OgBo08FZEQisOdd00YUMXSAV7BC0CcWpqJ2y-gs0cI,4863
|
|
26
26
|
iwa/core/chain/rate_limiter.py,sha256=gU7TmWdH9D_wbXKT1X7mIgoIUCWVuebgvRhxiyLGAmI,6613
|
|
@@ -36,12 +36,12 @@ iwa/core/services/account.py,sha256=01MoEvl6FJlMnMB4fGwsPtnGa4kgA-d5hJeKu_ACg7Y,
|
|
|
36
36
|
iwa/core/services/balance.py,sha256=mPE12CuOFfCaJXaQXWOcQM1O03ZF3ghpy_-oOjNk_GE,4104
|
|
37
37
|
iwa/core/services/plugin.py,sha256=GNNlbtELyHl7MNVChrypF76GYphxXduxDog4kx1MLi8,3277
|
|
38
38
|
iwa/core/services/safe.py,sha256=ytNJMndXrzTMHwhDZKYLIh4Q0UTWDBgQgTpof-UqIkA,14827
|
|
39
|
-
iwa/core/services/transaction.py,sha256=
|
|
39
|
+
iwa/core/services/transaction.py,sha256=DiEVwE1L_UpCyC5UmknaRwRYRxsDlAkwMQRN64NiwIQ,15162
|
|
40
40
|
iwa/core/services/transfer/__init__.py,sha256=ZJfshFxJRsp8rkOqfVvd1cqEzIJ9tqBJh8pc0l90GLk,5576
|
|
41
41
|
iwa/core/services/transfer/base.py,sha256=sohz-Ss2i-pGYGl4x9bD93cnYKcSvsXaXyvyRawvgQs,9043
|
|
42
|
-
iwa/core/services/transfer/erc20.py,sha256=
|
|
42
|
+
iwa/core/services/transfer/erc20.py,sha256=958ctXPWxq_KSQNoaG7RqWbC8SRb9NB3MzhtC2dp_NU,8960
|
|
43
43
|
iwa/core/services/transfer/multisend.py,sha256=MuOTjzUQoYg1eSixXKhJGBmB1c0ymLelvk4puHm_VGE,15194
|
|
44
|
-
iwa/core/services/transfer/native.py,sha256=
|
|
44
|
+
iwa/core/services/transfer/native.py,sha256=2CiUOP1gHEXAtLG0-8FaykV3u3jclq5y71gXQNEoc3w,10433
|
|
45
45
|
iwa/core/services/transfer/swap.py,sha256=exOJdzwkZaGbrFWfmQT_2JMcZUxnkiehXca8TH-vlF0,12269
|
|
46
46
|
iwa/core/tests/test_wallet.py,sha256=N8_gO7KkV5nqk_KcHqW_xOwNNKpDuXHeFgnala3bB84,9361
|
|
47
47
|
iwa/plugins/__init__.py,sha256=zy-DjOZn8GSgIETN2X_GAb9O6yk71t6ZRzeUgoZ52KA,23
|
|
@@ -61,7 +61,7 @@ iwa/plugins/olas/importer.py,sha256=f8KlZ9dGcNbpg8uoTYbO9sDvbluZoslhpWFLqPjPnLA,
|
|
|
61
61
|
iwa/plugins/olas/mech_reference.py,sha256=CaSCpQnQL4F7wOG6Ox6Zdoy-uNEQ78YBwVLILQZKL8Q,5782
|
|
62
62
|
iwa/plugins/olas/models.py,sha256=xC5hYakX53pBT6zZteM9cyiC7t6XRLLpobjQmDYueOo,3520
|
|
63
63
|
iwa/plugins/olas/plugin.py,sha256=S_vnvZ02VdVWD7N5kp7u5JIRQ2JLtfwGDZ7OHkAN0M8,9390
|
|
64
|
-
iwa/plugins/olas/contracts/activity_checker.py,sha256=
|
|
64
|
+
iwa/plugins/olas/contracts/activity_checker.py,sha256=PTLvsFdi3PdsFMxRVcXfwlQMRyJYHIzrHf3OaPVtFqU,3943
|
|
65
65
|
iwa/plugins/olas/contracts/base.py,sha256=y73aQbDq6l4zUpz_eQAg4MsLkTAEqjjupXlcvxjfgCI,240
|
|
66
66
|
iwa/plugins/olas/contracts/mech.py,sha256=dXYtyORc-oiu9ga5PtTquOFkoakb6BLGKvlUsteygIg,2767
|
|
67
67
|
iwa/plugins/olas/contracts/mech_marketplace.py,sha256=hMADl5MQGvT2wLRKu4vHGe4RrAZVq8Y2M_EvXWWz528,1554
|
|
@@ -82,7 +82,7 @@ iwa/plugins/olas/service_manager/__init__.py,sha256=GXiThMEY3nPgHUl1i-DLrF4h96z9
|
|
|
82
82
|
iwa/plugins/olas/service_manager/base.py,sha256=CCTH7RiYtgyFwRszrMLxNf1rNM_6leWHuJJmse4m2wI,4854
|
|
83
83
|
iwa/plugins/olas/service_manager/drain.py,sha256=IS7YYKuQdkULcNdxfHVzjcq95pXKdpajolzLL78u4jc,12430
|
|
84
84
|
iwa/plugins/olas/service_manager/lifecycle.py,sha256=DIB6yrP0VPICu6558uQJuFp2sgrA66iVNTzZVUUowGw,47159
|
|
85
|
-
iwa/plugins/olas/service_manager/mech.py,sha256=
|
|
85
|
+
iwa/plugins/olas/service_manager/mech.py,sha256=eMynvChrUSaTZBk0XP9KkcI6xYA2KXh9sHPtekixqro,16810
|
|
86
86
|
iwa/plugins/olas/service_manager/staking.py,sha256=Z9GzlPfY7qSSjc9xPhWvb9qywxu_7OB3Gc1eBli07pY,28058
|
|
87
87
|
iwa/plugins/olas/tests/conftest.py,sha256=4vM7EI00SrTGyeP0hNzsGSQHEj2-iznVgzlNh2_OGfo,739
|
|
88
88
|
iwa/plugins/olas/tests/test_importer.py,sha256=i9LKov7kNRECB3hmRnhKBwcfx3uxtjWe4BB77bOOpeo,4282
|
|
@@ -150,7 +150,7 @@ iwa/web/tests/test_web_endpoints.py,sha256=C264MH-CTyDW4GLUrTXBgLJKUk4-89pFAScBd
|
|
|
150
150
|
iwa/web/tests/test_web_olas.py,sha256=0CVSsrncOeJ3x0ECV7mVLQV_CXZRrOqGiVjgLIi6hZ8,16308
|
|
151
151
|
iwa/web/tests/test_web_swap.py,sha256=7A4gBJFL01kIXPtW1E1J17SCsVc_0DmUn-R8kKrnnVA,2974
|
|
152
152
|
iwa/web/tests/test_web_swap_coverage.py,sha256=zGNrzlhZ_vWDCvWmLcoUwFgqxnrp_ACbo49AtWBS_Kw,5584
|
|
153
|
-
iwa-0.0.
|
|
153
|
+
iwa-0.0.18.dist-info/licenses/LICENSE,sha256=eIubm_IlBHPYRQlLNZKbBNKhJUUP3JH0A2miZUhAVfI,1078
|
|
154
154
|
tests/legacy_cow.py,sha256=oOkZvIxL70ReEoD9oHQbOD5GpjIr6AGNHcOCgfPlerU,8389
|
|
155
155
|
tests/legacy_safe.py,sha256=AssM2g13E74dNGODu_H0Q0y412lgqsrYnEzI97nm_Ts,2972
|
|
156
156
|
tests/legacy_transaction_retry_logic.py,sha256=D9RqZ7DBu61Xr2djBAodU2p9UE939LL-DnQXswX5iQk,1497
|
|
@@ -159,7 +159,7 @@ tests/legacy_wallets_screen.py,sha256=9hZnX-VhKgwH9w8MxbNdboRyNxLDhOakLKJECsw_vh
|
|
|
159
159
|
tests/legacy_web.py,sha256=q2ERIriaDHT3Q8axG2N3ucO7f2VSvV_WkuPR00DVko4,8577
|
|
160
160
|
tests/test_account_service.py,sha256=g_AIVT2jhlvUtbFTaCd-d15x4CmXJQaV66tlAgnaXwY,3745
|
|
161
161
|
tests/test_balance_service.py,sha256=86iEkPd2M1-UFy3qOxV1EguQOEYbboy2-2mAyS3ctGs,6549
|
|
162
|
-
tests/test_chain.py,sha256=
|
|
162
|
+
tests/test_chain.py,sha256=cWSLUHRl2Iz55wQA08bk8RvAQnp_ZHCqA829KJ3Ur6c,13609
|
|
163
163
|
tests/test_chain_interface.py,sha256=Wu0q0sREtmYBp7YvWrBIrrSTtqeQj18oJp2VmMUEMec,8312
|
|
164
164
|
tests/test_chain_interface_coverage.py,sha256=fvrVvw8-DMwdsSFKQHUhpbfutrVRxnnTc-tjB7Bb-jo,3327
|
|
165
165
|
tests/test_cli.py,sha256=WW6EDeHLws5-BqFNOy11pH_D5lttuyspD5hrDCFpR0Q,3968
|
|
@@ -169,7 +169,7 @@ tests/test_drain_coverage.py,sha256=jtN5tIXzSTlS2IjwLS60azyMYsjFDlSTUa98JM1bMic,
|
|
|
169
169
|
tests/test_erc20.py,sha256=kNEw1afpm5EbXRNXkjpkBNZIy7Af1nqGlztKH5IWAwU,3074
|
|
170
170
|
tests/test_gnosis_plugin.py,sha256=XMoHBCTrnVBq9bXYPzMUIrhr95caucMVRxooCjKrzjg,3454
|
|
171
171
|
tests/test_keys.py,sha256=pxlGlHB-NaTbC8qar093ttdHLLtkM8irVNmCbH2KVf4,17421
|
|
172
|
-
tests/test_legacy_wallet.py,sha256=
|
|
172
|
+
tests/test_legacy_wallet.py,sha256=Caj3xP1FijRyLKa-xKiZu6NCAsQRHjWbsnAarncAoA0,49798
|
|
173
173
|
tests/test_main.py,sha256=y2xr7HjCt4rHsxm8y6n24FKCteSHPyxC3DFuMcUgX1Y,475
|
|
174
174
|
tests/test_migration.py,sha256=fYoxzI3KqGh0cPV0bFcbvGrAnKcNlvnwjggG_uD0QGo,1789
|
|
175
175
|
tests/test_mnemonic.py,sha256=BFtXMMg17uHWh_H-ZwAOn0qzgbUCqL8BRLkgRjzfzxo,7379
|
|
@@ -187,11 +187,11 @@ tests/test_safe_coverage.py,sha256=g9Bdrpkc-Mc8HBjk07lYNRkzxWZiF922uiVLZqhehBE,5
|
|
|
187
187
|
tests/test_safe_service.py,sha256=nxDYmGd6p2gGe7BEeMxsqS8CgeJarPofV38HC6Cop44,5770
|
|
188
188
|
tests/test_service_manager_integration.py,sha256=I_BLUzEKrVTyg_8jqsUK0oFD3aQVPCRJ7z0gY8P-j04,2354
|
|
189
189
|
tests/test_service_manager_structure.py,sha256=zK506ucCXCBHcjPYKrKEuK1bgq0xsbawyL8Y-wahXf8,868
|
|
190
|
-
tests/test_service_transaction.py,sha256=
|
|
191
|
-
tests/test_staking_router.py,sha256=
|
|
190
|
+
tests/test_service_transaction.py,sha256=IeqYhmRD-pIXffBJrBQwfPx-qnfNEJs0iPM3eCb8MLo,7054
|
|
191
|
+
tests/test_staking_router.py,sha256=cnOtwWeQPu09kecVhlCf1WA4ONqs13OcQJhJCx2EOPY,3067
|
|
192
192
|
tests/test_staking_simple.py,sha256=NHyZ1pcVQEJGFiGseC5m6Y9Y6FJGnRIFJUwhd1hAV9g,1138
|
|
193
193
|
tests/test_tables.py,sha256=1KQHgxuizoOrRxpubDdnzk9iaU5Lwyp3bcWP_hZD5uU,2686
|
|
194
|
-
tests/test_transaction_service.py,sha256=
|
|
194
|
+
tests/test_transaction_service.py,sha256=TXhIleUNOnp3DXi-RrKJ1Y_6dA6de5TQLOc9qndMHm4,5765
|
|
195
195
|
tests/test_transfer_multisend.py,sha256=PErjNqNwN66TMh4oVa307re64Ucccg1LkXqB0KlkmsI,6677
|
|
196
196
|
tests/test_transfer_native.py,sha256=cDbb4poV_veIw6eHpokrHe9yUndOjA6rQhrHd_IY3HQ,7445
|
|
197
197
|
tests/test_transfer_security.py,sha256=gdpC6ybdXQbQgILbAQ0GqjWdwn9AJRNR3B_7TYg0NxI,3617
|
|
@@ -202,8 +202,8 @@ tests/test_utils.py,sha256=vkP49rYNI8BRzLpWR3WnKdDr8upeZjZcs7Rx0pjbQMo,1292
|
|
|
202
202
|
tests/test_workers.py,sha256=MInwdkFY5LdmFB3o1odIaSD7AQZb3263hNafO1De5PE,2793
|
|
203
203
|
tools/create_and_stake_service.py,sha256=1xwy_bJQI1j9yIQ968Oc9Db_F6mk1659LuuZntTASDE,3742
|
|
204
204
|
tools/verify_drain.py,sha256=PkMjblyOOAuQge88FwfEzRtCYeEtJxXhPBmtQYCoQ-8,6743
|
|
205
|
-
iwa-0.0.
|
|
206
|
-
iwa-0.0.
|
|
207
|
-
iwa-0.0.
|
|
208
|
-
iwa-0.0.
|
|
209
|
-
iwa-0.0.
|
|
205
|
+
iwa-0.0.18.dist-info/METADATA,sha256=dyyGDv8h7tk7l8RPTf4F69Guby5BxTzbOOGEOuWOYVI,7295
|
|
206
|
+
iwa-0.0.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
207
|
+
iwa-0.0.18.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
|
|
208
|
+
iwa-0.0.18.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
|
|
209
|
+
iwa-0.0.18.dist-info/RECORD,,
|
tests/test_chain.py
CHANGED
|
@@ -195,43 +195,8 @@ def test_wait_for_no_pending_tx(mock_web3):
|
|
|
195
195
|
assert ci.wait_for_no_pending_tx("0xSender") is False
|
|
196
196
|
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
chain.name = "TestChain"
|
|
201
|
-
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
202
|
-
ci = ChainInterface(chain)
|
|
203
|
-
account = MagicMock(address="0xSender", key="key")
|
|
204
|
-
|
|
205
|
-
ci.web3.eth.get_transaction_count.return_value = 0
|
|
206
|
-
ci.web3.eth.gas_price = 10
|
|
207
|
-
ci.web3.eth.estimate_gas.return_value = 21000
|
|
208
|
-
|
|
209
|
-
# Sufficient balance
|
|
210
|
-
ci.web3.eth.get_balance.return_value = 10**18 # plenty
|
|
211
|
-
ci.web3.eth.get_balance.return_value = 10**18 # plenty
|
|
212
|
-
# Valid mock return for success: (True, dict_receipt)
|
|
213
|
-
# The actual method returns tx_hash.hex().
|
|
214
|
-
mock_signed_tx = MagicMock()
|
|
215
|
-
mock_signed_tx.raw_transaction = b"raw"
|
|
216
|
-
mock_receipt = {"transactionHash": b"hash", "status": 1}
|
|
217
|
-
|
|
218
|
-
with (
|
|
219
|
-
patch.object(ci.web3.eth, "send_raw_transaction", return_value=b"hash"),
|
|
220
|
-
patch.object(ci.web3.eth, "wait_for_transaction_receipt", return_value=mock_receipt),
|
|
221
|
-
patch.object(ci, "wait_for_no_pending_tx", return_value=True),
|
|
222
|
-
):
|
|
223
|
-
success, tx_hash = ci.send_native_transfer(
|
|
224
|
-
account.address, "0xReceiver", 1000, sign_callback=lambda tx: mock_signed_tx
|
|
225
|
-
)
|
|
226
|
-
assert success is True
|
|
227
|
-
assert tx_hash == "68617368"
|
|
228
|
-
|
|
229
|
-
# Insufficient balance
|
|
230
|
-
ci.web3.eth.get_balance.return_value = 0
|
|
231
|
-
ci.web3.from_wei.return_value = 0.0
|
|
232
|
-
assert ci.send_native_transfer(
|
|
233
|
-
account.address, "0xReceiver", 1000, sign_callback=lambda tx: mock_signed_tx
|
|
234
|
-
) == (False, None)
|
|
198
|
+
# NOTE: test_send_native_transfer was removed because the method was removed
|
|
199
|
+
# from ChainInterface. Native transfers now go through TransactionService.
|
|
235
200
|
|
|
236
201
|
|
|
237
202
|
def test_chain_interfaces_get():
|
|
@@ -352,66 +317,6 @@ def test_chain_interface_with_real_chains():
|
|
|
352
317
|
# --- Negative Tests ---
|
|
353
318
|
|
|
354
319
|
|
|
355
|
-
def test_send_native_transfer_insufficient_balance(mock_web3):
|
|
356
|
-
"""Test send_native_transfer fails with insufficient balance."""
|
|
357
|
-
chain = MagicMock(spec=SupportedChain)
|
|
358
|
-
chain.name = "TestChain"
|
|
359
|
-
chain.rpcs = ["https://rpc"]
|
|
360
|
-
chain.chain_id = 1
|
|
361
|
-
chain.native_currency = "ETH"
|
|
362
|
-
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
363
|
-
|
|
364
|
-
ci = ChainInterface(chain)
|
|
365
|
-
ci.web3.eth.get_transaction_count.return_value = 0
|
|
366
|
-
ci.web3.eth.gas_price = 1000000000 # 1 gwei
|
|
367
|
-
ci.web3.eth.estimate_gas.return_value = 21000
|
|
368
|
-
ci.web3.eth.get_balance.return_value = 1000 # Very low balance
|
|
369
|
-
ci.web3.from_wei.return_value = 0.000001
|
|
370
|
-
|
|
371
|
-
sign_callback = MagicMock()
|
|
372
|
-
|
|
373
|
-
success, tx_hash = ci.send_native_transfer(
|
|
374
|
-
from_address="0x1111111111111111111111111111111111111111",
|
|
375
|
-
to_address="0x2222222222222222222222222222222222222222",
|
|
376
|
-
value_wei=10**18, # 1 ETH - more than available
|
|
377
|
-
sign_callback=sign_callback,
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
assert success is False
|
|
381
|
-
assert tx_hash is None
|
|
382
|
-
sign_callback.assert_not_called()
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def test_send_native_transfer_rpc_error(mock_web3):
|
|
386
|
-
"""Test send_native_transfer handles RPC errors."""
|
|
387
|
-
chain = MagicMock(spec=SupportedChain)
|
|
388
|
-
chain.name = "TestChain"
|
|
389
|
-
chain.rpcs = ["https://rpc"]
|
|
390
|
-
chain.chain_id = 1
|
|
391
|
-
chain.native_currency = "ETH"
|
|
392
|
-
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
393
|
-
|
|
394
|
-
ci = ChainInterface(chain)
|
|
395
|
-
ci.web3.eth.get_transaction_count.return_value = 0
|
|
396
|
-
ci.web3.eth.gas_price = 1000000000
|
|
397
|
-
ci.web3.eth.estimate_gas.return_value = 21000
|
|
398
|
-
ci.web3.eth.get_balance.return_value = 10**19 # Enough balance
|
|
399
|
-
ci.web3.from_wei.return_value = 10.0
|
|
400
|
-
ci.web3.eth.send_raw_transaction.side_effect = Exception("Connection refused")
|
|
401
|
-
|
|
402
|
-
sign_callback = MagicMock()
|
|
403
|
-
sign_callback.return_value = MagicMock(raw_transaction=b"signed")
|
|
404
|
-
|
|
405
|
-
success, tx_hash = ci.send_native_transfer(
|
|
406
|
-
from_address="0x1111111111111111111111111111111111111111",
|
|
407
|
-
to_address="0x2222222222222222222222222222222222222222",
|
|
408
|
-
value_wei=10**17,
|
|
409
|
-
sign_callback=sign_callback,
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
assert success is False
|
|
413
|
-
assert tx_hash is None
|
|
414
|
-
|
|
415
320
|
|
|
416
321
|
def test_get_token_symbol_fallback_on_error(mock_web3):
|
|
417
322
|
"""Test get_token_symbol returns truncated address on error."""
|
tests/test_legacy_wallet.py
CHANGED
|
@@ -345,23 +345,23 @@ def test_send_native_success(wallet, mock_key_storage, mock_chain_interfaces, mo
|
|
|
345
345
|
mock_key_storage.get_account.return_value = account
|
|
346
346
|
|
|
347
347
|
chain_interface = mock_chain_interfaces.get.return_value
|
|
348
|
-
# chain_interface.get_native_balance_wei.return_value = ... # Ignored
|
|
349
348
|
mock_balance_service.get_native_balance_wei.return_value = 2000000000000000000
|
|
350
349
|
|
|
351
350
|
chain_interface.web3.eth.gas_price = 1000000000
|
|
352
351
|
chain_interface.web3.eth.estimate_gas.return_value = 21000
|
|
353
352
|
chain_interface.web3.from_wei.side_effect = lambda val, unit: float(val) / 10**18
|
|
354
353
|
|
|
355
|
-
# Mock return
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
# Mock TransactionService return (native transfers now go through TransactionService)
|
|
355
|
+
wallet.transaction_service.sign_and_send.return_value = (
|
|
356
|
+
True,
|
|
357
|
+
{"status": 1, "transactionHash": b"hash"},
|
|
358
|
+
)
|
|
358
359
|
|
|
359
360
|
wallet.send(
|
|
360
361
|
"sender", VALID_ADDR_2, amount_wei=1000000000000000000, token_address_or_name="native"
|
|
361
362
|
) # 1 ETH
|
|
362
363
|
|
|
363
|
-
|
|
364
|
-
chain_interface.send_native_transfer.assert_called_once()
|
|
364
|
+
wallet.transaction_service.sign_and_send.assert_called_once()
|
|
365
365
|
|
|
366
366
|
|
|
367
367
|
def test_send_erc20_success(wallet, mock_key_storage, mock_chain_interfaces):
|
|
@@ -18,8 +18,19 @@ def mock_chain_interfaces():
|
|
|
18
18
|
gnosis_interface.web3 = MagicMock()
|
|
19
19
|
instance.get.return_value = gnosis_interface
|
|
20
20
|
|
|
21
|
-
# Mock with_retry to
|
|
22
|
-
|
|
21
|
+
# Mock with_retry to simulate retry behavior (up to 6 attempts)
|
|
22
|
+
def mock_with_retry(op, max_retries=6, **kwargs):
|
|
23
|
+
last_error = None
|
|
24
|
+
for attempt in range(max_retries + 1):
|
|
25
|
+
try:
|
|
26
|
+
return op()
|
|
27
|
+
except Exception as e:
|
|
28
|
+
last_error = e
|
|
29
|
+
if attempt >= max_retries:
|
|
30
|
+
raise
|
|
31
|
+
raise last_error
|
|
32
|
+
|
|
33
|
+
gnosis_interface.with_retry.side_effect = mock_with_retry
|
|
23
34
|
|
|
24
35
|
yield instance
|
|
25
36
|
|
|
@@ -100,7 +111,7 @@ def test_sign_and_send_retry_on_low_gas(
|
|
|
100
111
|
def test_sign_and_send_max_retries_exhausted(
|
|
101
112
|
transaction_service, mock_key_storage, mock_chain_interfaces
|
|
102
113
|
):
|
|
103
|
-
"""Test sign_and_send fails after max
|
|
114
|
+
"""Test sign_and_send fails after max retries (with_retry default of 6 + 1)."""
|
|
104
115
|
tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 10000}
|
|
105
116
|
mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
|
|
106
117
|
|
|
@@ -119,8 +130,8 @@ def test_sign_and_send_max_retries_exhausted(
|
|
|
119
130
|
# Should fail after max retries
|
|
120
131
|
assert success is False
|
|
121
132
|
assert receipt == {} # Returns empty dict on failure
|
|
122
|
-
# Should have tried
|
|
123
|
-
assert chain_interface.web3.eth.send_raw_transaction.call_count ==
|
|
133
|
+
# Should have tried 7 times (6 retries + 1 initial = max_retries+1 from with_retry)
|
|
134
|
+
assert chain_interface.web3.eth.send_raw_transaction.call_count == 7
|
|
124
135
|
|
|
125
136
|
|
|
126
137
|
def test_sign_and_send_transaction_reverted(
|
|
@@ -144,7 +155,11 @@ def test_sign_and_send_transaction_reverted(
|
|
|
144
155
|
def test_sign_and_send_rpc_error_triggers_rotation(
|
|
145
156
|
transaction_service, mock_key_storage, mock_chain_interfaces
|
|
146
157
|
):
|
|
147
|
-
"""Test sign_and_send
|
|
158
|
+
"""Test sign_and_send retries on connection error via with_retry.
|
|
159
|
+
|
|
160
|
+
RPC rotation is now handled internally by with_retry, so we just verify
|
|
161
|
+
that the operation retries and eventually succeeds.
|
|
162
|
+
"""
|
|
148
163
|
tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 21000}
|
|
149
164
|
mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
|
|
150
165
|
|
|
@@ -156,13 +171,13 @@ def test_sign_and_send_rpc_error_triggers_rotation(
|
|
|
156
171
|
b"tx_hash",
|
|
157
172
|
]
|
|
158
173
|
chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
|
|
159
|
-
chain_interface.rotate_rpc.return_value = True
|
|
160
174
|
|
|
161
175
|
with patch("time.sleep"):
|
|
162
176
|
success, receipt = transaction_service.sign_and_send(tx, "signer")
|
|
163
177
|
|
|
164
178
|
assert success is True
|
|
165
|
-
|
|
179
|
+
# Verify retry happened - send_raw_transaction called twice
|
|
180
|
+
assert chain_interface.web3.eth.send_raw_transaction.call_count == 2
|
|
166
181
|
|
|
167
182
|
|
|
168
183
|
def test_sign_and_send_signer_not_found(
|
tests/test_staking_router.py
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
"""Tests for staking.py router coverage."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from unittest.mock import MagicMock, patch
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
@pytest.fixture(autouse=True)
|
|
9
|
-
def
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
def mock_dependencies():
|
|
11
|
+
"""Mock wallet dependency before importing router modules.
|
|
12
|
+
|
|
13
|
+
The iwa.web.dependencies module instantiates Wallet() at module level,
|
|
14
|
+
which fails in test environment. We pre-populate sys.modules to prevent
|
|
15
|
+
the actual import.
|
|
16
|
+
"""
|
|
17
|
+
# Create mock module
|
|
18
|
+
mock_dep_module = MagicMock()
|
|
19
|
+
mock_dep_module.wallet = MagicMock()
|
|
20
|
+
|
|
21
|
+
# Pre-populate sys.modules to prevent real import
|
|
22
|
+
with patch.dict(sys.modules, {"iwa.web.dependencies": mock_dep_module}):
|
|
15
23
|
yield
|
|
16
24
|
|
|
17
25
|
|
|
@@ -56,8 +56,19 @@ def mock_chain_interfaces():
|
|
|
56
56
|
|
|
57
57
|
instance.get.return_value = gnosis_interface
|
|
58
58
|
|
|
59
|
-
# Mock with_retry to
|
|
60
|
-
|
|
59
|
+
# Mock with_retry to simulate retry behavior (up to 6 attempts)
|
|
60
|
+
def mock_with_retry(op, max_retries=6, **kwargs):
|
|
61
|
+
last_error = None
|
|
62
|
+
for attempt in range(max_retries + 1):
|
|
63
|
+
try:
|
|
64
|
+
return op()
|
|
65
|
+
except Exception as e:
|
|
66
|
+
last_error = e
|
|
67
|
+
if attempt >= max_retries:
|
|
68
|
+
raise
|
|
69
|
+
raise last_error
|
|
70
|
+
|
|
71
|
+
gnosis_interface.with_retry.side_effect = mock_with_retry
|
|
61
72
|
|
|
62
73
|
yield instance
|
|
63
74
|
|
|
@@ -68,7 +79,6 @@ def mock_external_deps():
|
|
|
68
79
|
with (
|
|
69
80
|
patch("iwa.core.services.transaction.log_transaction") as mock_log,
|
|
70
81
|
patch("iwa.core.pricing.PriceService") as mock_price,
|
|
71
|
-
patch("iwa.core.services.transaction.time.sleep") as _, # speed up tests
|
|
72
82
|
):
|
|
73
83
|
mock_price.return_value.get_token_price.return_value = 1.0 # 1 EUR per Token
|
|
74
84
|
yield {
|
|
@@ -146,7 +156,11 @@ def test_sign_and_send_low_gas_retry(
|
|
|
146
156
|
def test_sign_and_send_rpc_rotation(
|
|
147
157
|
mock_key_storage, mock_account_service, mock_chain_interfaces, mock_external_deps
|
|
148
158
|
):
|
|
149
|
-
"""Test
|
|
159
|
+
"""Test retry on generic error via with_retry.
|
|
160
|
+
|
|
161
|
+
RPC rotation is now handled internally by with_retry, so we just verify
|
|
162
|
+
that the operation retries and eventually succeeds.
|
|
163
|
+
"""
|
|
150
164
|
service = TransactionService(mock_key_storage, mock_account_service)
|
|
151
165
|
chain_interface = mock_chain_interfaces.get.return_value
|
|
152
166
|
|
|
@@ -155,11 +169,11 @@ def test_sign_and_send_rpc_rotation(
|
|
|
155
169
|
Exception("Connection reset"),
|
|
156
170
|
b"tx_hash_bytes",
|
|
157
171
|
]
|
|
158
|
-
chain_interface.rotate_rpc.return_value = True
|
|
159
172
|
|
|
160
173
|
tx = {"to": "0xDest", "value": 100}
|
|
161
174
|
|
|
162
175
|
success, receipt = service.sign_and_send(tx, "signer")
|
|
163
176
|
|
|
164
177
|
assert success is True
|
|
165
|
-
|
|
178
|
+
# Verify retry happened - send_raw_transaction called twice
|
|
179
|
+
assert chain_interface.web3.eth.send_raw_transaction.call_count == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|