iwa 0.0.19__py3-none-any.whl → 0.0.20__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.
@@ -0,0 +1,141 @@
1
+ """Event-based cache invalidation for Olas contracts."""
2
+
3
+
4
+ from loguru import logger
5
+
6
+ from iwa.core.contracts.cache import ContractCache
7
+ from iwa.plugins.olas.contracts.staking import StakingContract
8
+
9
+
10
+ class OlasEventInvalidator:
11
+ """Monitors OLAS events and invalidates caches."""
12
+
13
+ def __init__(self, chain_name: str = "gnosis"):
14
+ """Initialize the invalidator."""
15
+ self.chain_name = chain_name
16
+ self.contract_cache = ContractCache()
17
+
18
+ # We need to find active staking contracts to monitor
19
+ # For now, we'll use a dynamic approach or configuration
20
+ # Ideally, this service would know about all deployed staking contracts
21
+ # But efficiently, we might just want to monitor the ones in our cache?
22
+ # A simple approach for this MVP: Monitor contracts currently in the cache
23
+ # OR monitor a known set of staking contracts from constants.
24
+
25
+ # Since EventMonitor requires a list of addresses upfront, let's use the ones
26
+ # defined in constants if available, or just rely on what's active.
27
+ # However, EventMonitor checks transfers, not generic logs for specific events.
28
+ # The base EventMonitor in iwa.core.monitor is too specific (transfers).
29
+ # We should implement a specific loop here or extend EventMonitor.
30
+ # To avoid complex inheritance of a class not designed for extension (EventMonitor),
31
+ # we will implement a focused loop using the same pattern.
32
+
33
+ from iwa.core.chain import ChainInterfaces
34
+ from iwa.plugins.olas.constants import OLAS_TRADER_STAKING_CONTRACTS
35
+
36
+ self.chain_interface = ChainInterfaces().get(chain_name)
37
+ self.web3 = self.chain_interface.web3
38
+
39
+ # Get addresses to monitor
40
+ contracts = OLAS_TRADER_STAKING_CONTRACTS.get(chain_name, {})
41
+ self.staking_addresses = [addr for _, addr in contracts.items()]
42
+
43
+ self.running = False
44
+
45
+ def start(self):
46
+ """Start the event monitoring loop."""
47
+ import threading
48
+
49
+ self.running = True
50
+ thread = threading.Thread(target=self._monitor_loop, daemon=True)
51
+ thread.start()
52
+ logger.info(f"Started OlasEventInvalidator for {len(self.staking_addresses)} contracts")
53
+
54
+ def stop(self):
55
+ """Stop the monitoring loop."""
56
+ self.running = False
57
+
58
+ def _monitor_loop(self):
59
+ """Main monitoring loop."""
60
+ import time
61
+
62
+ try:
63
+ last_block = self.web3.eth.block_number
64
+ except Exception:
65
+ last_block = 0
66
+
67
+ while self.running:
68
+ try:
69
+ current_block = self.web3.eth.block_number
70
+
71
+ if current_block > last_block:
72
+ self._check_events(last_block + 1, current_block)
73
+ last_block = current_block
74
+
75
+ except Exception as e:
76
+ logger.error(f"Error in OlasEventInvalidator: {e}")
77
+
78
+ time.sleep(10) # check every 10 seconds
79
+
80
+ def _check_events(self, from_block: int, to_block: int):
81
+ """Check for relevant events in the block range."""
82
+ # Cap range
83
+ if to_block - from_block > 100:
84
+ from_block = to_block - 100
85
+
86
+ # We care about Checkpoint events on StakingContracts
87
+ # Event signature for Checkpoint: Checkpoint(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)
88
+ # Actually easier to use the contract instance to get the topic or event object
89
+
90
+ # Need ABI for this. Let's assume we can get it from a dummy contract instance
91
+ if not self.staking_addresses:
92
+ return
93
+
94
+ # 1. Checkpoint events
95
+ # Topic 0 for Checkpoint event
96
+ # We can construct a filter for all staking addresses
97
+
98
+ try:
99
+ # Ensure contract is cached for later use
100
+ self.contract_cache.get_contract(
101
+ StakingContract, self.staking_addresses[0], self.chain_name
102
+ )
103
+
104
+
105
+ logs = self.web3.eth.get_logs({
106
+ "fromBlock": from_block,
107
+ "toBlock": to_block,
108
+ "address": self.staking_addresses,
109
+ "topics": [self.web3.keccak(text="Checkpoint(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)").hex()]
110
+ # Note: signature might vary, safer to use the event object if ABI allows
111
+ })
112
+
113
+ # If we used the contract event object to filter, it handles the topic generation:
114
+ # logs = checkpoint_event_abi.get_logs(fromBlock=from_block, toBlock=to_block)
115
+ # But get_logs on the contract object filters by THAT contract address usually?
116
+ # web3.eth.get_logs is broader.
117
+
118
+ for log in logs:
119
+ addr = log["address"]
120
+ logger.info(f"Checkpoint detected on {addr} at block {log['blockNumber']}")
121
+
122
+ # Invalidate cache for this contract
123
+ # We want to call clear_epoch_cache on the EXISTING cached instance if present
124
+ # ContractCache().get_contract(...) might return it or create new.
125
+ # We need a way to 'get if exists' or assume get_contract is cheap enough.
126
+ # Specifically we want to clear the EPOCH cache, not just destroy the instance
127
+ # (though destroying it works too, it's just less efficient for the next call).
128
+
129
+ # Option A: Just invalidate the instance
130
+ # self.contract_cache.invalidate(StakingContract, addr, self.chain_name)
131
+
132
+ # Option B: Get instance and clear specific cache (safe public access)
133
+ instance = self.contract_cache.get_if_cached(
134
+ StakingContract, addr, self.chain_name
135
+ )
136
+ if instance:
137
+ instance.clear_epoch_cache()
138
+ logger.debug(f"Cleared epoch cache for {addr}")
139
+
140
+ except Exception as e:
141
+ logger.warning(f"Failed to check logs in invalidator: {e}")
@@ -5,6 +5,7 @@ from typing import Dict, Optional
5
5
  from loguru import logger
6
6
 
7
7
  from iwa.core.chain import ChainInterfaces
8
+ from iwa.core.contracts.cache import ContractCache
8
9
  from iwa.core.models import Config
9
10
  from iwa.core.wallet import Wallet
10
11
  from iwa.plugins.olas.constants import OLAS_CONTRACTS
@@ -65,8 +66,12 @@ class ServiceManagerBase:
65
66
  if not registry_address or not manager_address:
66
67
  raise ValueError(f"OLAS contracts not found for chain: {chain_name}")
67
68
 
68
- self.registry = ServiceRegistryContract(registry_address, chain_name=chain_name)
69
- self.manager = ServiceManagerContract(manager_address, chain_name=chain_name)
69
+ self.registry = ContractCache().get_contract(
70
+ ServiceRegistryContract, registry_address, chain_name=chain_name
71
+ )
72
+ self.manager = ContractCache().get_contract(
73
+ ServiceManagerContract, manager_address, chain_name=chain_name
74
+ )
70
75
  logger.debug(f"[SM-INIT] ServiceManager initialized. Chain: {chain_name}")
71
76
  logger.debug(f"[SM-INIT] Registry Address: {self.registry.address}")
72
77
  logger.debug(f"[SM-INIT] Manager Address: {self.manager.address}")
@@ -65,6 +65,7 @@ from web3.types import Wei
65
65
 
66
66
  from iwa.core.chain import ChainInterfaces
67
67
  from iwa.core.constants import NATIVE_CURRENCY_ADDRESS, ZERO_ADDRESS
68
+ from iwa.core.contracts.cache import ContractCache
68
69
  from iwa.core.types import EthereumAddress
69
70
  from iwa.core.utils import get_tx_hash
70
71
  from iwa.plugins.olas.constants import (
@@ -485,7 +486,8 @@ class LifecycleManagerMixin:
485
486
  agent_id = agent_ids[0]
486
487
 
487
488
  # Use the ServiceRegistryTokenUtilityContract with official ABI
488
- token_utility = ServiceRegistryTokenUtilityContract(
489
+ token_utility = ContractCache().get_contract(
490
+ ServiceRegistryTokenUtilityContract,
489
491
  address=str(utility_address),
490
492
  chain_name=self.chain_name,
491
493
  )
@@ -759,7 +761,7 @@ class LifecycleManagerMixin:
759
761
  logger.info("[REGISTER] Success - service is now FINISHED_REGISTRATION")
760
762
  return True
761
763
 
762
- def deploy(self) -> Optional[str]: # noqa: C901
764
+ def deploy(self, fund_multisig: bool = False) -> Optional[str]: # noqa: C901
763
765
  """Deploy the service."""
764
766
  logger.info(f"[DEPLOY] Starting deployment for service {self.service.service_id}")
765
767
 
@@ -841,6 +843,25 @@ class LifecycleManagerMixin:
841
843
  except Exception as e:
842
844
  logger.warning(f"[DEPLOY] Failed to register multisig in wallet: {e}")
843
845
 
846
+ # Fund the multisig with 1 xDAI from master if requested (for staking)
847
+ if fund_multisig:
848
+ try:
849
+ funding_amount = Web3.to_wei(1, "ether")
850
+ logger.info(f"[DEPLOY] Funding multisig {multisig_address} with 1 xDAI from master")
851
+ tx_hash = self.wallet.send(
852
+ from_address_or_tag=self.wallet.master_account.address,
853
+ to_address_or_tag=multisig_address,
854
+ token_address_or_name="native",
855
+ amount_wei=funding_amount,
856
+ chain_name=self.chain_name,
857
+ )
858
+ if tx_hash:
859
+ logger.info(f"[DEPLOY] Funded multisig: {tx_hash}")
860
+ else:
861
+ logger.error("[DEPLOY] Failed to fund multisig")
862
+ except Exception as e:
863
+ logger.error(f"[DEPLOY] Failed to fund multisig: {e}")
864
+
844
865
  logger.info("[DEPLOY] Success - service is now DEPLOYED")
845
866
  return multisig_address
846
867
 
@@ -976,7 +997,10 @@ class LifecycleManagerMixin:
976
997
  previous_state = current_state
977
998
  logger.info(f"[SPIN-UP] Step {step}: Processing {current_state.name}...")
978
999
 
979
- if not self._process_spin_up_state(current_state, agent_address, bond_amount_wei):
1000
+ should_fund = staking_contract is not None
1001
+ if not self._process_spin_up_state(
1002
+ current_state, agent_address, bond_amount_wei, fund_multisig=should_fund
1003
+ ):
980
1004
  logger.error(f"[SPIN-UP] Step {step} FAILED at state {current_state.name}")
981
1005
  return False
982
1006
 
@@ -1012,6 +1036,7 @@ class LifecycleManagerMixin:
1012
1036
  current_state: ServiceState,
1013
1037
  agent_address: Optional[str],
1014
1038
  bond_amount_wei: Optional[Wei],
1039
+ fund_multisig: bool = False,
1015
1040
  ) -> bool:
1016
1041
  """Process a single state transition for spin up."""
1017
1042
  if current_state == ServiceState.PRE_REGISTRATION:
@@ -1025,8 +1050,8 @@ class LifecycleManagerMixin:
1025
1050
  ):
1026
1051
  return False
1027
1052
  elif current_state == ServiceState.FINISHED_REGISTRATION:
1028
- logger.info("[SPIN-UP] Action: deploy()")
1029
- if not self.deploy():
1053
+ logger.info(f"[SPIN-UP] Action: deploy(fund_multisig={fund_multisig})")
1054
+ if not self.deploy(fund_multisig=fund_multisig):
1030
1055
  return False
1031
1056
  else:
1032
1057
  logger.error(f"[SPIN-UP] Invalid state: {current_state.name}")
@@ -612,6 +612,15 @@ class MechManagerMixin:
612
612
 
613
613
  if event_found:
614
614
  logger.info(f"Event '{expected_event}' verified successfully")
615
+
616
+ # Log transfer events from receipt
617
+ from iwa.core.services.transaction import TransferLogger
618
+
619
+ transfer_logger = TransferLogger(
620
+ self.wallet.account_service, self.registry.chain_interface
621
+ )
622
+ transfer_logger.log_transfers(receipt)
623
+
615
624
  return tx_hash
616
625
  else:
617
626
  logger.error(f"Event '{expected_event}' NOT found in transaction logs")
@@ -50,6 +50,7 @@ from typing import Optional
50
50
  from loguru import logger
51
51
  from web3 import Web3
52
52
 
53
+ from iwa.core.contracts.cache import ContractCache
53
54
  from iwa.core.types import EthereumAddress
54
55
  from iwa.core.utils import get_tx_hash
55
56
  from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
@@ -96,7 +97,9 @@ class StakingManagerMixin:
96
97
 
97
98
  # Load the staking contract
98
99
  try:
99
- staking = StakingContract(str(staking_address), chain_name=self.chain_name)
100
+ staking = ContractCache().get_contract(
101
+ StakingContract, str(staking_address), chain_name=self.chain_name
102
+ )
100
103
  except Exception as e:
101
104
  logger.error(f"Failed to load staking contract: {e}")
102
105
  return StakingStatus(
@@ -613,7 +616,8 @@ class StakingManagerMixin:
613
616
  # Load staking contract if not provided
614
617
  if not staking_contract:
615
618
  try:
616
- staking_contract = StakingContract(
619
+ staking_contract = ContractCache().get_contract(
620
+ StakingContract,
617
621
  str(self.service.staking_contract_address),
618
622
  chain_name=self.service.chain_name,
619
623
  )
@@ -309,7 +309,10 @@ def test_importer_import_service_config_duplicate(mock_wallet):
309
309
 
310
310
  def test_sm_create_token_utility_missing(mock_wallet):
311
311
  """Cover create() with missing token utility (lines 204-206)."""
312
- with patch("iwa.core.models.Config"):
312
+ with patch("iwa.core.models.Config"), \
313
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
314
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
315
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
313
316
  sm = ServiceManager(mock_wallet)
314
317
 
315
318
  with patch.dict("iwa.plugins.olas.service_manager.base.OLAS_CONTRACTS", {"unknown": {}}):
@@ -320,7 +323,10 @@ def test_sm_create_token_utility_missing(mock_wallet):
320
323
 
321
324
  def test_sm_get_staking_status_staked_info_fail(mock_wallet):
322
325
  """Cover get_staking_status with STAKED but get_service_info fails (lines 843-854)."""
323
- with patch("iwa.core.models.Config"):
326
+ with patch("iwa.core.models.Config"), \
327
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
328
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
329
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
324
330
  sm = ServiceManager(mock_wallet)
325
331
  sm.service = Service(
326
332
  service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
@@ -341,7 +347,10 @@ def test_sm_get_staking_status_staked_info_fail(mock_wallet):
341
347
 
342
348
  def test_sm_call_checkpoint_prepare_fail(mock_wallet):
343
349
  """Cover call_checkpoint prepare failure (lines 1100-1102)."""
344
- with patch("iwa.core.models.Config"):
350
+ with patch("iwa.core.models.Config"), \
351
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
352
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
353
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
345
354
  sm = ServiceManager(mock_wallet)
346
355
  sm.service = Service(
347
356
  service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
@@ -360,7 +369,10 @@ def test_sm_call_checkpoint_prepare_fail(mock_wallet):
360
369
 
361
370
  def test_sm_spin_up_no_service(mock_wallet):
362
371
  """Cover spin_up with no service (lines 1167-1170)."""
363
- with patch("iwa.core.models.Config"):
372
+ with patch("iwa.core.models.Config"), \
373
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
374
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
375
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
364
376
  sm = ServiceManager(mock_wallet)
365
377
  sm.service = None
366
378
 
@@ -370,7 +382,10 @@ def test_sm_spin_up_no_service(mock_wallet):
370
382
 
371
383
  def test_sm_spin_up_activation_fail(mock_wallet):
372
384
  """Cover spin_up activation failure (lines 1181-1183)."""
373
- with patch("iwa.core.models.Config"):
385
+ with patch("iwa.core.models.Config"), \
386
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
387
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
388
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
374
389
  sm = ServiceManager(mock_wallet)
375
390
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
376
391
 
@@ -387,7 +402,10 @@ def test_sm_spin_up_activation_fail(mock_wallet):
387
402
 
388
403
  def test_sm_wind_down_no_service(mock_wallet):
389
404
  """Cover wind_down with no service (lines 1264-1266)."""
390
- with patch("iwa.core.models.Config"):
405
+ with patch("iwa.core.models.Config"), \
406
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
407
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
408
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
391
409
  sm = ServiceManager(mock_wallet)
392
410
  sm.service = None
393
411
 
@@ -397,7 +415,10 @@ def test_sm_wind_down_no_service(mock_wallet):
397
415
 
398
416
  def test_sm_wind_down_nonexistent(mock_wallet):
399
417
  """Cover wind_down with non-existent service (lines 1274-1276)."""
400
- with patch("iwa.core.models.Config"):
418
+ with patch("iwa.core.models.Config"), \
419
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
420
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
421
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
401
422
  sm = ServiceManager(mock_wallet)
402
423
  sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
403
424
 
@@ -411,7 +432,10 @@ def test_sm_wind_down_nonexistent(mock_wallet):
411
432
 
412
433
  def test_sm_mech_request_no_service(mock_wallet):
413
434
  """Cover _send_legacy_mech_request with no service (lines 1502-1504)."""
414
- with patch("iwa.core.models.Config"):
435
+ with patch("iwa.core.models.Config"), \
436
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
437
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
438
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
415
439
  sm = ServiceManager(mock_wallet)
416
440
  sm.service = None
417
441
 
@@ -421,7 +445,10 @@ def test_sm_mech_request_no_service(mock_wallet):
421
445
 
422
446
  def test_sm_mech_request_no_address(mock_wallet):
423
447
  """Cover _send_legacy_mech_request missing mech address (lines 1510-1512)."""
424
- with patch("iwa.core.models.Config"):
448
+ with patch("iwa.core.models.Config"), \
449
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
450
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
451
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
425
452
  sm = ServiceManager(mock_wallet)
426
453
  sm.service = Service(service_name="t", chain_name="unknown", service_id=1)
427
454
 
@@ -431,7 +458,8 @@ def test_sm_mech_request_no_address(mock_wallet):
431
458
 
432
459
  def test_sm_marketplace_mech_no_service(mock_wallet):
433
460
  """Cover _send_marketplace_mech_request with no service (lines 1549-1551)."""
434
- with patch("iwa.core.models.Config"):
461
+ with patch("iwa.core.models.Config"), \
462
+ patch("iwa.plugins.olas.service_manager.base.ContractCache"):
435
463
  sm = ServiceManager(mock_wallet)
436
464
  sm.service = None
437
465
 
@@ -114,11 +114,16 @@ def service_manager(
114
114
  mock_service,
115
115
  ):
116
116
  """ServiceManager fixture with mocked dependencies."""
117
- with patch("iwa.plugins.olas.service_manager.base.Config") as local_mock_config:
117
+ with patch("iwa.plugins.olas.service_manager.base.Config") as local_mock_config, \
118
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache:
118
119
  instance = local_mock_config.return_value
119
120
  instance.plugins = {"olas": mock_olas_config}
120
121
  instance.save_config = MagicMock()
121
122
 
123
+ # Mock ContractCache to return MagicMock contracts
124
+ mock_cache_instance = mock_cache.return_value
125
+ mock_cache_instance.get_contract.return_value = MagicMock()
126
+
122
127
  sm = ServiceManager(mock_wallet)
123
128
  # Ensure service is properly set
124
129
  sm.service = mock_service
@@ -127,6 +132,7 @@ def service_manager(
127
132
  yield sm
128
133
 
129
134
 
135
+
130
136
  def test_init(service_manager):
131
137
  """Test initialization."""
132
138
  assert service_manager.registry is not None
@@ -23,12 +23,19 @@ def mock_wallet():
23
23
  return wallet
24
24
 
25
25
 
26
- def setup_manager(mock_wallet):
26
+ @pytest.fixture
27
+ def mock_manager(mock_wallet):
27
28
  """Setup a ServiceManager with mocked dependencies."""
28
- with patch("iwa.plugins.olas.service_manager.base.Config") as mock_cfg_cls:
29
+ with patch("iwa.plugins.olas.service_manager.base.Config") as mock_cfg_cls, \
30
+ patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
31
+ patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
32
+
33
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
34
+
29
35
  mock_cfg = mock_cfg_cls.return_value
30
36
  mock_cfg.plugins = {"olas": MagicMock()}
31
37
  mock_cfg.plugins["olas"].get_service.return_value = None
38
+
32
39
  with patch(
33
40
  "iwa.plugins.olas.service_manager.OLAS_CONTRACTS",
34
41
  {
@@ -43,18 +50,22 @@ def setup_manager(mock_wallet):
43
50
  mock_if = mock_if_cls.return_value
44
51
  mock_if.get.return_value.chain.name.lower.return_value = "gnosis"
45
52
  mock_if.get.return_value.get_contract_address.return_value = VALID_ADDR
53
+
46
54
  manager = ServiceManager(mock_wallet)
47
55
  manager.registry = MagicMock()
48
56
  manager.manager_contract = MagicMock()
49
57
  manager.olas_config = mock_cfg.plugins["olas"]
50
58
  manager.chain_name = "gnosis"
59
+ # Fix recursive mock issue by setting explicit return value
60
+ manager.registry.chain_interface = MagicMock()
51
61
  manager.registry.chain_interface.get_contract_address.return_value = VALID_ADDR
52
- return manager
62
+
63
+ yield manager
53
64
 
54
65
 
55
- def test_service_manager_mech_requests_failures(mock_wallet):
66
+ def test_service_manager_mech_requests_failures(mock_manager):
56
67
  """Test failure paths in mech requests."""
57
- manager = setup_manager(mock_wallet)
68
+ manager = mock_manager
58
69
 
59
70
  # Service missing
60
71
  manager.service = None
@@ -99,9 +110,9 @@ def test_service_manager_mech_requests_failures(mock_wallet):
99
110
  assert manager.send_mech_request(b"data", use_marketplace=False) is None
100
111
 
101
112
 
102
- def test_service_manager_lifecycle_failures(mock_wallet):
113
+ def test_service_manager_lifecycle_failures(mock_manager, mock_wallet):
103
114
  """Test failure paths in lifecycle methods."""
104
- manager = setup_manager(mock_wallet)
115
+ manager = mock_manager
105
116
  manager.service = Service(service_name="t", chain_name="gnosis", service_id=1, agent_ids=[1])
106
117
 
107
118
  # register_agent failures
@@ -147,9 +158,9 @@ def test_service_manager_lifecycle_failures(mock_wallet):
147
158
  assert manager.wind_down() is True
148
159
 
149
160
 
150
- def test_service_manager_staking_status_failures(mock_wallet):
161
+ def test_service_manager_staking_status_failures(mock_manager):
151
162
  """Test failure paths in get_staking_status."""
152
- manager = setup_manager(mock_wallet)
163
+ manager = mock_manager
153
164
 
154
165
  # Service missing
155
166
  manager.service = None
@@ -178,9 +189,9 @@ def test_service_manager_staking_status_failures(mock_wallet):
178
189
  assert status.mech_requests_this_epoch == 0
179
190
 
180
191
 
181
- def test_service_manager_verify_event_exception(mock_wallet):
192
+ def test_service_manager_verify_event_exception(mock_manager):
182
193
  """Test exception path in _execute_mech_tx."""
183
- manager = setup_manager(mock_wallet)
194
+ manager = mock_manager
184
195
  manager.service = Service(
185
196
  service_name="t",
186
197
  chain_name="gnosis",
@@ -45,10 +45,12 @@ def mock_config():
45
45
  @patch("iwa.plugins.olas.service_manager.base.Config")
46
46
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
47
47
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
48
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
48
49
  def test_create_service_success(
49
- mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
50
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
50
51
  ):
51
52
  """Test successful service creation."""
53
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
52
54
  # Setup Config with new OlasConfig structure
53
55
  mock_config_inst = mock_config_cls.return_value
54
56
  mock_olas_config = MagicMock()
@@ -84,10 +86,12 @@ def test_create_service_success(
84
86
  @patch("iwa.plugins.olas.service_manager.base.Config")
85
87
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
86
88
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
89
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
87
90
  def test_create_service_failures(
88
- mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
91
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
89
92
  ):
90
93
  """Test service creation failure modes."""
94
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
91
95
  mock_config_inst = mock_config_cls.return_value
92
96
  mock_olas_config = MagicMock()
93
97
  mock_olas_config.get_service.return_value = None
@@ -139,10 +143,12 @@ def test_create_service_failures(
139
143
  @patch("iwa.plugins.olas.service_manager.base.Config")
140
144
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
141
145
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
146
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
142
147
  def test_create_service_with_approval(
143
- mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
148
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
144
149
  ):
145
150
  """Test service creation with token approval."""
151
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
146
152
  mock_config_inst = mock_config_cls.return_value
147
153
  mock_olas_config = MagicMock()
148
154
  mock_olas_config.get_service.return_value = None
@@ -170,10 +176,12 @@ def test_create_service_with_approval(
170
176
  @patch("iwa.plugins.olas.service_manager.base.Config")
171
177
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
172
178
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
179
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
173
180
  def test_activate_registration(
174
- mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
181
+ mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
175
182
  ):
176
183
  """Test service registration activation."""
184
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
177
185
  mock_config_inst = mock_config_cls.return_value
178
186
  mock_olas_config = MagicMock()
179
187
  mock_service = MagicMock()
@@ -228,8 +236,10 @@ def test_activate_registration(
228
236
  @patch("iwa.plugins.olas.service_manager.base.Config")
229
237
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
230
238
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
231
- def test_register_agent(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
239
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
240
+ def test_register_agent(mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
232
241
  """Test agent registration flow."""
242
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
233
243
  mock_config_inst = mock_config_cls.return_value
234
244
  mock_olas_config = MagicMock()
235
245
  mock_service = MagicMock()
@@ -277,8 +287,10 @@ def test_register_agent(mock_sm_contract, mock_registry_contract, mock_config_cl
277
287
  @patch("iwa.plugins.olas.service_manager.base.Config")
278
288
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
279
289
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
280
- def test_deploy(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
290
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
291
+ def test_deploy(mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
281
292
  """Test service deployment."""
293
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
282
294
  # Setup mock service
283
295
  mock_service = MagicMock()
284
296
  mock_service.service_id = 123
@@ -341,8 +353,10 @@ def test_deploy(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_
341
353
  @patch("iwa.plugins.olas.service_manager.base.Config")
342
354
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
343
355
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
344
- def test_terminate(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
356
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
357
+ def test_terminate(mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
345
358
  """Test service termination."""
359
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
346
360
  # Setup mock service
347
361
  mock_service = MagicMock()
348
362
  mock_service.service_id = 123
@@ -401,8 +415,10 @@ def test_terminate(mock_sm_contract, mock_registry_contract, mock_config_cls, mo
401
415
  @patch(
402
416
  "iwa.plugins.olas.service_manager.base.ServiceManagerContract"
403
417
  ) # MUST mock specifically here
404
- def test_stake(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
418
+ @patch("iwa.plugins.olas.service_manager.base.ContractCache")
419
+ def test_stake(mock_cache, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
405
420
  """Test service staking."""
421
+ mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
406
422
  # Setup mock service
407
423
  mock_service = MagicMock()
408
424
  mock_service.service_id = 123