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.
@@ -2,9 +2,8 @@
2
2
 
3
3
  import threading
4
4
  import time
5
- from typing import Callable, Dict, Optional, Tuple, TypeVar, Union
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
- # Use demo URL if API key is present, otherwise standard URL
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 = {}
@@ -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, tx: Dict) -> None:
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
- # Log native value transfer if present
43
- native_value = tx.get("value", 0)
44
- if native_value and int(native_value) > 0:
45
- self._log_native_transfer(tx, native_value)
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, tx: Dict, value_wei: int) -> None:
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 with retry logic for gas."""
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
- for attempt in range(1, max_retries + 1):
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(signed_txn.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
- if receipt and getattr(receipt, "status", None) == 1:
238
- signer_account = self.account_service.resolve_account(signer_address_or_tag)
239
- chain_interface.wait_for_no_pending_tx(signer_account.address)
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
- # Transaction reverted
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
- return False, {}
251
+ raise ValueError("Transaction reverted")
250
252
 
251
253
  except web3_exceptions.Web3RPCError as e:
252
- if self._handle_gas_error(e, tx, attempt, max_retries):
253
- continue
254
- return False, {}
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
- except Exception as e:
257
- # Attempt RPC rotation
258
- if self._handle_generic_error(e, chain_interface, attempt, max_retries):
259
- continue
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
- return False, {}
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 _handle_gas_error(self, e, tx, attempt, max_retries) -> bool:
278
- err_text = str(e)
279
- if self._is_gas_too_low_error(err_text) and attempt < max_retries:
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
- tx["gas"] = int(current_gas * 1.5)
286
- # Exponential backoff for gas errors
287
- time.sleep(min(2**attempt, 30))
288
- return True
289
- logger.exception(f"Error sending transaction: {e}")
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, tx)
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 (externally owned account)."""
78
- success, tx_hash = chain_interface.send_native_transfer(
79
- from_address=from_account.address,
80
- to_address=to_address,
81
- value_wei=amount_wei,
82
- sign_callback=lambda tx: self.key_storage.sign_transaction(tx, from_account.address),
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
- if success and tx_hash:
85
- # Get receipt for gas calculation
86
- receipt = None
87
- try:
88
- receipt = chain_interface.web3.eth.get_transaction_receipt(tx_hash)
89
- except Exception as e:
90
- logger.warning(f"Could not get receipt for {tx_hash}: {e}")
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
- # 1. Get Safe Nonce
86
- safe_nonce = 0
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
- multisig_address = self.service.multisig_address
279
- chain_name = self.chain_name if self.service else getattr(self, "chain_name", "gnosis")
280
- protocol_contracts = OLAS_CONTRACTS.get(chain_name, {})
281
- marketplace_address = protocol_contracts.get("OLAS_MECH_MARKETPLACE")
282
-
283
- if not marketplace_address:
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
- priority_mech = Web3.to_checksum_address(priority_mech)
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
- if payment_type is None:
299
- payment_type = bytes.fromhex(PAYMENT_TYPE_NATIVE)
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iwa
3
- Version: 0.0.17
3
+ Version: 0.0.18
4
4
  Summary: A secure, modular, and plugin-based framework for crypto agents and ops
5
5
  Requires-Python: <4.0,>=3.12
6
6
  Description-Content-Type: text/markdown
@@ -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=U-vIjJwzh6O7MZTAFZx6mFFxaiLcwn6PnsZc4g0z4IQ,4293
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=BIUPJUAV8oeeiUtZcj0zi2B0mGad2qS5ajy1PhBT-1k,21182
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=3IeTLntewa9XevPTMrNhjOUwdv0nMdU3KqlxljjL0Z0,14286
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=0GvlfETgdJg15azs1apH1gI6Vm3gAv22o1saNbIfiAY,8685
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=Z0v41OMbbcTjwdYLDwMO-8G81UMPYDdQ3y8BdbsJimM,9350
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=sQSzBWF7meXzCzVw4G0wowjTTRddraGPppQ8PyLcuaU,6429
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=Rl4FJIYrT9L4gzTd7-7xBZaxBEHWFyMmN5TuSNfEdZc,15964
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.17.dist-info/licenses/LICENSE,sha256=eIubm_IlBHPYRQlLNZKbBNKhJUUP3JH0A2miZUhAVfI,1078
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=z3Mo9mHNQZ0aXSlrHUcdtNgGqsNyOwWYQAmmKa_dqiM,17221
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=j7AorUMvuChyxy18y9j8Ux8wpZa2_ZvKutp5obKLLI0,49895
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=SecU2Fy32jLH4lg0CZHNrNkqM1pzPnYB0qZtxqaDnFE,6416
191
- tests/test_staking_router.py,sha256=e8bI_u24SsORn9bkf4QWOtFoGxi86WTyu_Zl_QnUvpA,2769
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=iuRMyOsMS4fRNIhudbW0ovSfA6vGzZpjIApVryOpO3U,5283
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.17.dist-info/METADATA,sha256=vzZKb09UmfMfeWTcLbJnUCxWHIr-DGWNvSvUM5k9RcQ,7295
206
- iwa-0.0.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
207
- iwa-0.0.17.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
208
- iwa-0.0.17.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
209
- iwa-0.0.17.dist-info/RECORD,,
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
- def test_send_native_transfer(mock_web3):
199
- chain = MagicMock(spec=SupportedChain, rpcs=["https://rpc"], chain_id=1, native_currency="ETH")
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."""
@@ -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 values for success
356
- chain_interface.sign_and_send_transaction.return_value = (True, {})
357
- chain_interface.send_native_transfer.return_value = (True, "0xHash")
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
- # wallet.transaction_service.sign_and_send.assert_called_once()
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 execute the operation
22
- gnosis_interface.with_retry.side_effect = lambda op, **kwargs: op()
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 gas retries."""
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 10 times (max_retries)
123
- assert chain_interface.web3.eth.send_raw_transaction.call_count == 10
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 rotates RPC on connection error."""
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
- chain_interface.rotate_rpc.assert_called()
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(
@@ -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 mock_key_storage():
10
- """Bypass KeyStorage security check."""
11
- with (
12
- patch("iwa.core.keys.KeyStorage.__init__", return_value=None),
13
- patch("iwa.web.dependencies.wallet", MagicMock()),
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 execute the operation
60
- gnosis_interface.with_retry.side_effect = lambda op, **kwargs: op()
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 RPC rotation on generic error."""
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
- chain_interface.rotate_rpc.assert_called_once()
178
+ # Verify retry happened - send_raw_transaction called twice
179
+ assert chain_interface.web3.eth.send_raw_transaction.call_count == 2
File without changes