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.
- iwa/core/chainlist.py +116 -0
- iwa/core/constants.py +1 -0
- iwa/core/contracts/cache.py +131 -0
- iwa/core/contracts/contract.py +7 -0
- iwa/core/rpc_monitor.py +60 -0
- iwa/plugins/olas/contracts/activity_checker.py +63 -25
- iwa/plugins/olas/contracts/staking.py +115 -19
- iwa/plugins/olas/events.py +141 -0
- iwa/plugins/olas/service_manager/base.py +7 -2
- iwa/plugins/olas/service_manager/lifecycle.py +30 -5
- iwa/plugins/olas/service_manager/mech.py +9 -0
- iwa/plugins/olas/service_manager/staking.py +6 -2
- iwa/plugins/olas/tests/test_olas_integration.py +38 -10
- iwa/plugins/olas/tests/test_service_manager.py +7 -1
- iwa/plugins/olas/tests/test_service_manager_errors.py +22 -11
- iwa/plugins/olas/tests/test_service_manager_flows.py +24 -8
- iwa/plugins/olas/tests/test_service_staking.py +59 -15
- iwa/plugins/olas/tests/test_staking_validation.py +8 -14
- iwa/tools/test_chainlist.py +38 -0
- iwa/web/routers/olas/staking.py +9 -4
- {iwa-0.0.19.dist-info → iwa-0.0.20.dist-info}/METADATA +1 -1
- {iwa-0.0.19.dist-info → iwa-0.0.20.dist-info}/RECORD +27 -21
- tests/test_rpc_efficiency.py +103 -0
- {iwa-0.0.19.dist-info → iwa-0.0.20.dist-info}/WHEEL +0 -0
- {iwa-0.0.19.dist-info → iwa-0.0.20.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.19.dist-info → iwa-0.0.20.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.19.dist-info → iwa-0.0.20.dist-info}/top_level.txt +0 -0
|
@@ -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 =
|
|
69
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
|
|
63
|
+
yield manager
|
|
53
64
|
|
|
54
65
|
|
|
55
|
-
def test_service_manager_mech_requests_failures(
|
|
66
|
+
def test_service_manager_mech_requests_failures(mock_manager):
|
|
56
67
|
"""Test failure paths in mech requests."""
|
|
57
|
-
manager =
|
|
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 =
|
|
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(
|
|
161
|
+
def test_service_manager_staking_status_failures(mock_manager):
|
|
151
162
|
"""Test failure paths in get_staking_status."""
|
|
152
|
-
manager =
|
|
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(
|
|
192
|
+
def test_service_manager_verify_event_exception(mock_manager):
|
|
182
193
|
"""Test exception path in _execute_mech_tx."""
|
|
183
|
-
manager =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|