iwa 0.0.0__py3-none-any.whl → 0.0.1a2__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.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
"""Tests for ServiceManager."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.models import StoredAccount
|
|
8
|
+
from iwa.core.wallet import Wallet
|
|
9
|
+
from iwa.plugins.olas.contracts.service import ServiceState
|
|
10
|
+
from iwa.plugins.olas.contracts.staking import StakingState
|
|
11
|
+
from iwa.plugins.olas.models import OlasConfig, Service
|
|
12
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
13
|
+
|
|
14
|
+
# Valid test addresses (checksummed)
|
|
15
|
+
TEST_MULTISIG_ADDR = "0x5555555555555555555555555555555555555555"
|
|
16
|
+
TEST_STAKING_ADDR = "0x6666666666666666666666666666666666666666"
|
|
17
|
+
TEST_AGENT_ADDR = "0x7777777777777777777777777777777777777777"
|
|
18
|
+
TEST_EXISTING_AGENT_ADDR = "0x8888888888888888888888888888888888888888"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_service():
|
|
23
|
+
"""Create a mock Service object."""
|
|
24
|
+
service = MagicMock(spec=Service)
|
|
25
|
+
service.service_name = "test_service"
|
|
26
|
+
service.chain_name = "gnosis"
|
|
27
|
+
service.service_id = 1
|
|
28
|
+
service.agent_ids = [25] # Default TRADER agent
|
|
29
|
+
service.service_owner_address = "0x1234567890123456789012345678901234567890"
|
|
30
|
+
service.agent_address = None
|
|
31
|
+
service.multisig_address = None
|
|
32
|
+
service.staking_contract_address = None
|
|
33
|
+
service.token_address = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" # OLAS token
|
|
34
|
+
service.security_deposit = 50000000000000000000 # 50 OLAS
|
|
35
|
+
service.key = "gnosis:1"
|
|
36
|
+
return service
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def mock_olas_config(mock_service):
|
|
41
|
+
"""Create a mock OlasConfig object."""
|
|
42
|
+
olas_config = MagicMock(spec=OlasConfig)
|
|
43
|
+
olas_config.services = {"gnosis:1": mock_service}
|
|
44
|
+
olas_config.get_service.return_value = mock_service
|
|
45
|
+
return olas_config
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def mock_config(mock_olas_config):
|
|
50
|
+
"""Mock configuration fixture."""
|
|
51
|
+
with patch(
|
|
52
|
+
"iwa.plugins.olas.service_manager.base.Config"
|
|
53
|
+
) as mock: # Patch the class used in service_manager
|
|
54
|
+
instance = mock.return_value
|
|
55
|
+
instance.plugins = {"olas": mock_olas_config}
|
|
56
|
+
instance.save_config = MagicMock()
|
|
57
|
+
yield instance
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def mock_wallet():
|
|
62
|
+
"""Mock wallet fixture."""
|
|
63
|
+
wallet = MagicMock(spec=Wallet)
|
|
64
|
+
wallet.master_account = MagicMock(spec=StoredAccount)
|
|
65
|
+
wallet.master_account.address = "0x1234567890123456789012345678901234567890"
|
|
66
|
+
wallet.key_storage = MagicMock()
|
|
67
|
+
wallet.key_storage.get_account.return_value = None # Default
|
|
68
|
+
# Mock create_account which returns a StoredAccount or similar
|
|
69
|
+
new_acc = MagicMock()
|
|
70
|
+
new_acc.address = "0x0987654321098765432109876543210987654321"
|
|
71
|
+
wallet.key_storage.create_account.return_value = new_acc
|
|
72
|
+
# Mock transfer_service
|
|
73
|
+
wallet.transfer_service = MagicMock()
|
|
74
|
+
wallet.transfer_service.approve_erc20.return_value = True
|
|
75
|
+
return wallet
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def mock_registry():
|
|
80
|
+
"""Mock service registry fixture."""
|
|
81
|
+
with patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract") as mock:
|
|
82
|
+
yield mock
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.fixture
|
|
86
|
+
def mock_manager_contract():
|
|
87
|
+
"""Mock service manager contract fixture."""
|
|
88
|
+
with patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract") as mock:
|
|
89
|
+
yield mock
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.fixture
|
|
93
|
+
def mock_chain_interfaces():
|
|
94
|
+
"""Mock chain interfaces fixture."""
|
|
95
|
+
with patch("iwa.plugins.olas.service_manager.base.ChainInterfaces") as mock:
|
|
96
|
+
chain = MagicMock()
|
|
97
|
+
# Use valid token address
|
|
98
|
+
chain.chain.get_token_address.return_value = "0x1111111111111111111111111111111111111111"
|
|
99
|
+
mock.return_value.get.return_value = chain
|
|
100
|
+
yield mock
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.fixture
|
|
104
|
+
def mock_erc20_contract():
|
|
105
|
+
"""Mock ERC20 contract fixture."""
|
|
106
|
+
with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock:
|
|
107
|
+
yield mock
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.fixture
|
|
111
|
+
def service_manager(
|
|
112
|
+
mock_config,
|
|
113
|
+
mock_wallet,
|
|
114
|
+
mock_registry,
|
|
115
|
+
mock_manager_contract,
|
|
116
|
+
mock_chain_interfaces,
|
|
117
|
+
mock_erc20_contract,
|
|
118
|
+
mock_olas_config,
|
|
119
|
+
mock_service,
|
|
120
|
+
):
|
|
121
|
+
"""ServiceManager fixture with mocked dependencies."""
|
|
122
|
+
with patch("iwa.plugins.olas.service_manager.base.Config") as local_mock_config:
|
|
123
|
+
instance = local_mock_config.return_value
|
|
124
|
+
instance.plugins = {"olas": mock_olas_config}
|
|
125
|
+
instance.save_config = MagicMock()
|
|
126
|
+
|
|
127
|
+
sm = ServiceManager(mock_wallet)
|
|
128
|
+
# Ensure service is properly set
|
|
129
|
+
sm.service = mock_service
|
|
130
|
+
sm.olas_config = mock_olas_config
|
|
131
|
+
sm.global_config = instance
|
|
132
|
+
yield sm
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_init(service_manager):
|
|
136
|
+
"""Test initialization."""
|
|
137
|
+
assert service_manager.registry is not None
|
|
138
|
+
assert service_manager.manager is not None
|
|
139
|
+
assert service_manager.service is not None
|
|
140
|
+
assert service_manager.olas_config is not None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_get(service_manager):
|
|
144
|
+
"""Test get service."""
|
|
145
|
+
service_manager.get()
|
|
146
|
+
service_manager.registry.get_service.assert_called_with(1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_create_success(service_manager, mock_wallet):
|
|
150
|
+
"""Test successful service creation."""
|
|
151
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {"raw": "receipt"})
|
|
152
|
+
service_manager.registry.extract_events.return_value = [
|
|
153
|
+
{"name": "CreateService", "args": {"serviceId": 123}}
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
service_id = service_manager.create(
|
|
157
|
+
token_address_or_tag="0x1111111111111111111111111111111111111111"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert service_id == 123
|
|
161
|
+
service_manager.manager.prepare_create_tx.assert_called()
|
|
162
|
+
mock_wallet.sign_and_send_transaction.assert_called()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_create_fail_tx(service_manager, mock_wallet):
|
|
166
|
+
"""Test failure when transaction fails."""
|
|
167
|
+
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
168
|
+
res = service_manager.create()
|
|
169
|
+
assert res is None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_create_no_event(service_manager, mock_wallet):
|
|
173
|
+
"""Test failure when no event is emitted."""
|
|
174
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
175
|
+
service_manager.registry.extract_events.return_value = []
|
|
176
|
+
|
|
177
|
+
with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract"): # Mock ERC20
|
|
178
|
+
res = service_manager.create(
|
|
179
|
+
token_address_or_tag="0x1111111111111111111111111111111111111111"
|
|
180
|
+
)
|
|
181
|
+
# create() finds no ID, logs error, returns None for service_id.
|
|
182
|
+
assert res is None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_activate_registration_success(service_manager, mock_wallet):
|
|
186
|
+
"""Test successful activation."""
|
|
187
|
+
service_manager.registry.get_service.return_value = {
|
|
188
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
189
|
+
"security_deposit": 50000000000000000000,
|
|
190
|
+
}
|
|
191
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
192
|
+
service_manager.registry.extract_events.return_value = [{"name": "ActivateRegistration"}]
|
|
193
|
+
|
|
194
|
+
# Mock balance/allowance for the new check
|
|
195
|
+
mock_wallet.balance_service = MagicMock()
|
|
196
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
197
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
198
|
+
|
|
199
|
+
assert service_manager.activate_registration() is True
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_activate_registration_wrong_state(service_manager):
|
|
203
|
+
"""Test activation fails in wrong state."""
|
|
204
|
+
service_manager.registry.get_service.return_value = {
|
|
205
|
+
"state": ServiceState.DEPLOYED,
|
|
206
|
+
"security_deposit": 50000000000000000000,
|
|
207
|
+
}
|
|
208
|
+
assert service_manager.activate_registration() is False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_register_agent_success(service_manager, mock_wallet):
|
|
212
|
+
"""Test successful agent registration."""
|
|
213
|
+
service_manager.registry.get_service.return_value = {
|
|
214
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
215
|
+
"security_deposit": 50000000000000000000,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# create_account is already mocked
|
|
219
|
+
mock_wallet.send.return_value = "0xMockTxHash" # wallet.send returns tx_hash
|
|
220
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
221
|
+
service_manager.registry.extract_events.return_value = [{"name": "RegisterInstance"}]
|
|
222
|
+
|
|
223
|
+
assert service_manager.register_agent() is True
|
|
224
|
+
assert service_manager.service.agent_address == "0x0987654321098765432109876543210987654321"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_deploy_success(service_manager, mock_wallet):
|
|
228
|
+
"""Test successful deployment."""
|
|
229
|
+
service_manager.registry.get_service.return_value = {
|
|
230
|
+
"state": ServiceState.FINISHED_REGISTRATION
|
|
231
|
+
}
|
|
232
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
233
|
+
service_manager.registry.extract_events.return_value = [
|
|
234
|
+
{"name": "DeployService"},
|
|
235
|
+
{"name": "CreateMultisigWithAgents", "args": {"multisig": TEST_MULTISIG_ADDR}},
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
assert service_manager.deploy() == TEST_MULTISIG_ADDR
|
|
239
|
+
assert service_manager.service.multisig_address == TEST_MULTISIG_ADDR
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_terminate_success(service_manager, mock_wallet):
|
|
243
|
+
"""Test successful termination."""
|
|
244
|
+
service_manager.registry.get_service.return_value = {
|
|
245
|
+
"state": ServiceState.DEPLOYED,
|
|
246
|
+
"security_deposit": 50000000000000000000,
|
|
247
|
+
}
|
|
248
|
+
# Not staked
|
|
249
|
+
service_manager.service.staking_contract_address = None
|
|
250
|
+
|
|
251
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
252
|
+
service_manager.registry.extract_events.return_value = [{"name": "TerminateService"}]
|
|
253
|
+
|
|
254
|
+
assert service_manager.terminate() is True
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_unbond_success(service_manager, mock_wallet):
|
|
258
|
+
"""Test successful unbonding."""
|
|
259
|
+
service_manager.registry.get_service.return_value = {
|
|
260
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
261
|
+
"security_deposit": 50000000000000000000,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
265
|
+
service_manager.registry.extract_events.return_value = [{"name": "OperatorUnbond"}]
|
|
266
|
+
|
|
267
|
+
assert service_manager.unbond() is True
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_stake_success(service_manager, mock_wallet):
|
|
271
|
+
"""Test successful staking."""
|
|
272
|
+
staking_contract = MagicMock()
|
|
273
|
+
staking_contract.staking_token_address = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
|
|
274
|
+
staking_contract.get_service_ids.return_value = []
|
|
275
|
+
staking_contract.max_num_services = 10
|
|
276
|
+
staking_contract.min_staking_deposit = 100
|
|
277
|
+
staking_contract.address = TEST_STAKING_ADDR
|
|
278
|
+
staking_contract.get_requirements.return_value = {
|
|
279
|
+
"staking_token": "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", # OLAS token
|
|
280
|
+
"min_staking_deposit": 50000000000000000000,
|
|
281
|
+
"num_agent_instances": 1,
|
|
282
|
+
"required_agent_bond": 50000000000000000000, # 50 OLAS
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
service_manager.registry.get_service.return_value = {
|
|
286
|
+
"state": ServiceState.DEPLOYED,
|
|
287
|
+
"security_deposit": 50000000000000000000,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock_erc20:
|
|
291
|
+
mock_erc20.return_value.balance_of_wei.return_value = 100000000000000000000 # 100 OLAS
|
|
292
|
+
|
|
293
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
294
|
+
staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
|
|
295
|
+
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
296
|
+
|
|
297
|
+
# We need to make sure prepare_approve_tx is mocked ON THE REGISTRY INSTANCE
|
|
298
|
+
service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
|
|
299
|
+
|
|
300
|
+
assert service_manager.stake(staking_contract) is True
|
|
301
|
+
assert service_manager.service.staking_contract_address == TEST_STAKING_ADDR
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_unstake_success(service_manager, mock_wallet):
|
|
305
|
+
"""Test successful unstaking."""
|
|
306
|
+
staking_contract = MagicMock()
|
|
307
|
+
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
308
|
+
|
|
309
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
310
|
+
staking_contract.extract_events.return_value = [{"name": "ServiceUnstaked"}]
|
|
311
|
+
|
|
312
|
+
assert service_manager.unstake(staking_contract) is True
|
|
313
|
+
assert service_manager.service.staking_contract_address is None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# --- Tests for register_agent with existing address ---
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_register_agent_with_existing_address(service_manager, mock_wallet):
|
|
320
|
+
"""Test registering an existing agent address (no new account creation)."""
|
|
321
|
+
service_manager.registry.get_service.return_value = {
|
|
322
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
323
|
+
"security_deposit": 50000000000000000000,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
327
|
+
service_manager.registry.extract_events.return_value = [{"name": "RegisterInstance"}]
|
|
328
|
+
|
|
329
|
+
existing_agent = TEST_EXISTING_AGENT_ADDR
|
|
330
|
+
assert service_manager.register_agent(agent_address=existing_agent) is True
|
|
331
|
+
assert service_manager.service.agent_address == TEST_EXISTING_AGENT_ADDR
|
|
332
|
+
# Should NOT create a new account
|
|
333
|
+
mock_wallet.key_storage.create_account.assert_not_called()
|
|
334
|
+
# Should NOT fund the agent (only for new accounts)
|
|
335
|
+
mock_wallet.send.assert_not_called()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_register_agent_creates_new_if_none(service_manager, mock_wallet):
|
|
339
|
+
"""Test that register_agent creates and funds a new agent when no address provided."""
|
|
340
|
+
service_manager.registry.get_service.return_value = {
|
|
341
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
342
|
+
"security_deposit": 50000000000000000000,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
mock_wallet.send.return_value = "0xMockTxHash" # wallet.send returns tx_hash
|
|
346
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
347
|
+
service_manager.registry.extract_events.return_value = [{"name": "RegisterInstance"}]
|
|
348
|
+
|
|
349
|
+
assert service_manager.register_agent() is True
|
|
350
|
+
# Should create a new account
|
|
351
|
+
mock_wallet.key_storage.create_account.assert_called()
|
|
352
|
+
# Should fund the new agent
|
|
353
|
+
mock_wallet.send.assert_called()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_register_agent_fund_fails(service_manager, mock_wallet):
|
|
357
|
+
"""Test that register_agent fails when funding new agent fails."""
|
|
358
|
+
service_manager.registry.get_service.return_value = {
|
|
359
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
360
|
+
"security_deposit": 50000000000000000000,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
mock_wallet.send.return_value = None # Funding fails (wallet.send returns None on failure)
|
|
364
|
+
|
|
365
|
+
assert service_manager.register_agent() is False
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# --- Tests for spin_up ---
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_spin_up_from_pre_registration_success(service_manager, mock_wallet):
|
|
372
|
+
"""Test full spin_up path from PRE_REGISTRATION to DEPLOYED."""
|
|
373
|
+
# Mock state transitions - need to match actual calls in spin_up
|
|
374
|
+
# The state after activate_registration should be ACTIVE_REGISTRATION
|
|
375
|
+
state_sequence = [
|
|
376
|
+
{
|
|
377
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
378
|
+
"security_deposit": 50000000000000000000,
|
|
379
|
+
}, # spin_up initial
|
|
380
|
+
{
|
|
381
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
382
|
+
"security_deposit": 50000000000000000000,
|
|
383
|
+
}, # activate_registration check
|
|
384
|
+
{
|
|
385
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
386
|
+
"security_deposit": 50000000000000000000,
|
|
387
|
+
}, # activate_registration internal (get security deposit)
|
|
388
|
+
{
|
|
389
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
390
|
+
"security_deposit": 50000000000000000000,
|
|
391
|
+
}, # spin_up verify after activate
|
|
392
|
+
{
|
|
393
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
394
|
+
"security_deposit": 50000000000000000000,
|
|
395
|
+
}, # register_agent check
|
|
396
|
+
{
|
|
397
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
398
|
+
"security_deposit": 50000000000000000000,
|
|
399
|
+
}, # register_agent internal
|
|
400
|
+
{
|
|
401
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
402
|
+
"security_deposit": 50000000000000000000,
|
|
403
|
+
}, # spin_up verify after register
|
|
404
|
+
{
|
|
405
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
406
|
+
"security_deposit": 50000000000000000000,
|
|
407
|
+
}, # deploy check
|
|
408
|
+
{
|
|
409
|
+
"state": ServiceState.DEPLOYED,
|
|
410
|
+
"security_deposit": 50000000000000000000,
|
|
411
|
+
}, # spin_up verify after deploy
|
|
412
|
+
{
|
|
413
|
+
"state": ServiceState.DEPLOYED,
|
|
414
|
+
"security_deposit": 50000000000000000000,
|
|
415
|
+
}, # final verification
|
|
416
|
+
]
|
|
417
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
418
|
+
|
|
419
|
+
mock_wallet.send.return_value = "0xMockTxHash" # wallet.send returns tx_hash
|
|
420
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
421
|
+
|
|
422
|
+
# Mock balance/allowance for activate_registration internal call
|
|
423
|
+
mock_wallet.balance_service = MagicMock()
|
|
424
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
425
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
426
|
+
service_manager.registry.extract_events.side_effect = [
|
|
427
|
+
[{"name": "ActivateRegistration"}],
|
|
428
|
+
[{"name": "RegisterInstance"}],
|
|
429
|
+
[
|
|
430
|
+
{"name": "DeployService"},
|
|
431
|
+
{"name": "CreateMultisigWithAgents", "args": {"multisig": TEST_MULTISIG_ADDR}},
|
|
432
|
+
],
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
assert service_manager.spin_up() is True
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_spin_up_from_active_registration(service_manager, mock_wallet):
|
|
439
|
+
"""Test spin_up resume from ACTIVE_REGISTRATION state."""
|
|
440
|
+
# Need extra states because register_agent makes additional get_service calls
|
|
441
|
+
state_sequence = [
|
|
442
|
+
{
|
|
443
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
444
|
+
"security_deposit": 50000000000000000000,
|
|
445
|
+
}, # spin_up initial
|
|
446
|
+
{
|
|
447
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
448
|
+
"security_deposit": 50000000000000000000,
|
|
449
|
+
}, # register_agent check
|
|
450
|
+
{
|
|
451
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
452
|
+
"security_deposit": 50000000000000000000,
|
|
453
|
+
}, # register_agent internal
|
|
454
|
+
{
|
|
455
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
456
|
+
"security_deposit": 50000000000000000000,
|
|
457
|
+
}, # spin_up verify after register
|
|
458
|
+
{
|
|
459
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
460
|
+
"security_deposit": 50000000000000000000,
|
|
461
|
+
}, # deploy check
|
|
462
|
+
{
|
|
463
|
+
"state": ServiceState.DEPLOYED,
|
|
464
|
+
"security_deposit": 50000000000000000000,
|
|
465
|
+
}, # spin_up verify after deploy
|
|
466
|
+
{
|
|
467
|
+
"state": ServiceState.DEPLOYED,
|
|
468
|
+
"security_deposit": 50000000000000000000,
|
|
469
|
+
}, # final verification
|
|
470
|
+
]
|
|
471
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
472
|
+
|
|
473
|
+
mock_wallet.send.return_value = "0xMockTxHash" # wallet.send returns tx_hash
|
|
474
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
475
|
+
service_manager.registry.extract_events.side_effect = [
|
|
476
|
+
[{"name": "RegisterInstance"}],
|
|
477
|
+
[
|
|
478
|
+
{"name": "DeployService"},
|
|
479
|
+
{"name": "CreateMultisigWithAgents", "args": {"multisig": TEST_MULTISIG_ADDR}},
|
|
480
|
+
],
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
assert service_manager.spin_up() is True
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_spin_up_from_finished_registration(service_manager, mock_wallet):
|
|
487
|
+
"""Test spin_up resume from FINISHED_REGISTRATION state."""
|
|
488
|
+
state_sequence = [
|
|
489
|
+
{
|
|
490
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
491
|
+
"security_deposit": 50000000000000000000,
|
|
492
|
+
}, # spin_up initial
|
|
493
|
+
{
|
|
494
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
495
|
+
"security_deposit": 50000000000000000000,
|
|
496
|
+
}, # deploy check
|
|
497
|
+
{
|
|
498
|
+
"state": ServiceState.DEPLOYED,
|
|
499
|
+
"security_deposit": 50000000000000000000,
|
|
500
|
+
}, # spin_up verify after deploy
|
|
501
|
+
{
|
|
502
|
+
"state": ServiceState.DEPLOYED,
|
|
503
|
+
"security_deposit": 50000000000000000000,
|
|
504
|
+
}, # final verification
|
|
505
|
+
]
|
|
506
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
507
|
+
|
|
508
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
509
|
+
service_manager.registry.extract_events.return_value = [
|
|
510
|
+
{"name": "DeployService"},
|
|
511
|
+
{"name": "CreateMultisigWithAgents", "args": {"multisig": TEST_MULTISIG_ADDR}},
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
assert service_manager.spin_up() is True
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def test_spin_up_already_deployed(service_manager, mock_wallet):
|
|
518
|
+
"""Test spin_up when already DEPLOYED (idempotent)."""
|
|
519
|
+
service_manager.registry.get_service.return_value = {
|
|
520
|
+
"state": ServiceState.DEPLOYED,
|
|
521
|
+
"security_deposit": 50000000000000000000,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# Should succeed without any transactions
|
|
525
|
+
assert service_manager.spin_up() is True
|
|
526
|
+
mock_wallet.sign_and_send_transaction.assert_not_called()
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def test_spin_up_with_staking(service_manager, mock_wallet):
|
|
530
|
+
"""Test spin_up with staking after deployment."""
|
|
531
|
+
state_sequence = [
|
|
532
|
+
{
|
|
533
|
+
"state": ServiceState.DEPLOYED,
|
|
534
|
+
"security_deposit": 50000000000000000000,
|
|
535
|
+
}, # spin_up initial
|
|
536
|
+
{
|
|
537
|
+
"state": ServiceState.DEPLOYED,
|
|
538
|
+
"security_deposit": 50000000000000000000,
|
|
539
|
+
}, # stake internal check
|
|
540
|
+
{
|
|
541
|
+
"state": ServiceState.DEPLOYED,
|
|
542
|
+
"security_deposit": 50000000000000000000,
|
|
543
|
+
}, # final verification
|
|
544
|
+
]
|
|
545
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
546
|
+
|
|
547
|
+
staking_contract = MagicMock()
|
|
548
|
+
staking_contract.staking_token_address = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
|
|
549
|
+
staking_contract.get_service_ids.return_value = []
|
|
550
|
+
staking_contract.max_num_services = 10
|
|
551
|
+
staking_contract.min_staking_deposit = 100
|
|
552
|
+
staking_contract.address = TEST_STAKING_ADDR
|
|
553
|
+
staking_contract.get_requirements.return_value = {
|
|
554
|
+
"staking_token": "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f",
|
|
555
|
+
"min_staking_deposit": 50000000000000000000,
|
|
556
|
+
"num_agent_instances": 1,
|
|
557
|
+
"required_agent_bond": 50000000000000000000, # 50 OLAS
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock_erc20:
|
|
561
|
+
mock_erc20.return_value.balance_of_wei.return_value = 100000000000000000000 # 100 OLAS
|
|
562
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
563
|
+
staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
|
|
564
|
+
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
565
|
+
service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
|
|
566
|
+
|
|
567
|
+
assert service_manager.spin_up(staking_contract=staking_contract) is True
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def test_spin_up_activate_fails(service_manager, mock_wallet):
|
|
571
|
+
"""Test spin_up fails when activate_registration fails."""
|
|
572
|
+
state_sequence = [
|
|
573
|
+
{
|
|
574
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
575
|
+
"security_deposit": 50000000000000000000,
|
|
576
|
+
}, # spin_up initial
|
|
577
|
+
{
|
|
578
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
579
|
+
"security_deposit": 50000000000000000000,
|
|
580
|
+
}, # activate_registration check
|
|
581
|
+
{
|
|
582
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
583
|
+
"security_deposit": 50000000000000000000,
|
|
584
|
+
}, # activate_registration internal (get security deposit)
|
|
585
|
+
]
|
|
586
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
587
|
+
|
|
588
|
+
# Mock balance/allowance for activate_registration behavior
|
|
589
|
+
mock_wallet.balance_service = MagicMock()
|
|
590
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
591
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
592
|
+
|
|
593
|
+
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
594
|
+
|
|
595
|
+
assert service_manager.spin_up() is False
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def test_spin_up_register_fails(service_manager, mock_wallet):
|
|
599
|
+
"""Test spin_up fails when register_agent fails."""
|
|
600
|
+
state_sequence = [
|
|
601
|
+
{
|
|
602
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
603
|
+
"security_deposit": 50000000000000000000,
|
|
604
|
+
}, # spin_up initial
|
|
605
|
+
{
|
|
606
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
607
|
+
"security_deposit": 50000000000000000000,
|
|
608
|
+
}, # register_agent check
|
|
609
|
+
]
|
|
610
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
611
|
+
|
|
612
|
+
# Funding fails
|
|
613
|
+
mock_wallet.send.return_value = None # wallet.send returns None on failure
|
|
614
|
+
|
|
615
|
+
assert service_manager.spin_up() is False
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def test_spin_up_deploy_fails(service_manager, mock_wallet):
|
|
619
|
+
"""Test spin_up fails when deploy fails."""
|
|
620
|
+
state_sequence = [
|
|
621
|
+
{
|
|
622
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
623
|
+
"security_deposit": 50000000000000000000,
|
|
624
|
+
}, # spin_up initial
|
|
625
|
+
{
|
|
626
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
627
|
+
"security_deposit": 50000000000000000000,
|
|
628
|
+
}, # deploy check
|
|
629
|
+
]
|
|
630
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
631
|
+
|
|
632
|
+
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
633
|
+
|
|
634
|
+
assert service_manager.spin_up() is False
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def test_spin_up_with_existing_agent(service_manager, mock_wallet):
|
|
638
|
+
"""Test spin_up uses provided agent address."""
|
|
639
|
+
# Need extra states for internal get_service calls
|
|
640
|
+
state_sequence = [
|
|
641
|
+
{
|
|
642
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
643
|
+
"security_deposit": 50000000000000000000,
|
|
644
|
+
}, # spin_up initial
|
|
645
|
+
{
|
|
646
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
647
|
+
"security_deposit": 50000000000000000000,
|
|
648
|
+
}, # register_agent check
|
|
649
|
+
{
|
|
650
|
+
"state": ServiceState.ACTIVE_REGISTRATION,
|
|
651
|
+
"security_deposit": 50000000000000000000,
|
|
652
|
+
}, # register_agent internal
|
|
653
|
+
{
|
|
654
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
655
|
+
"security_deposit": 50000000000000000000,
|
|
656
|
+
}, # spin_up verify after register
|
|
657
|
+
{
|
|
658
|
+
"state": ServiceState.FINISHED_REGISTRATION,
|
|
659
|
+
"security_deposit": 50000000000000000000,
|
|
660
|
+
}, # deploy check
|
|
661
|
+
{
|
|
662
|
+
"state": ServiceState.DEPLOYED,
|
|
663
|
+
"security_deposit": 50000000000000000000,
|
|
664
|
+
}, # spin_up verify after deploy
|
|
665
|
+
{
|
|
666
|
+
"state": ServiceState.DEPLOYED,
|
|
667
|
+
"security_deposit": 50000000000000000000,
|
|
668
|
+
}, # final verification
|
|
669
|
+
]
|
|
670
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
671
|
+
|
|
672
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
673
|
+
service_manager.registry.extract_events.side_effect = [
|
|
674
|
+
[{"name": "RegisterInstance"}],
|
|
675
|
+
[
|
|
676
|
+
{"name": "DeployService"},
|
|
677
|
+
{"name": "CreateMultisigWithAgents", "args": {"multisig": TEST_MULTISIG_ADDR}},
|
|
678
|
+
],
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
existing_agent = TEST_EXISTING_AGENT_ADDR
|
|
682
|
+
assert service_manager.spin_up(agent_address=existing_agent) is True
|
|
683
|
+
# Verify agent address was not newly created
|
|
684
|
+
mock_wallet.key_storage.create_account.assert_not_called()
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# --- Tests for wind_down ---
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def test_wind_down_from_deployed_success(service_manager, mock_wallet):
|
|
691
|
+
"""Test full wind_down path from DEPLOYED to PRE_REGISTRATION."""
|
|
692
|
+
# Mock state transitions - need to account for all get_service calls:
|
|
693
|
+
# 1. wind_down initial check
|
|
694
|
+
# 2. wind_down refresh after unstake check
|
|
695
|
+
# 3. terminate internal check
|
|
696
|
+
# 4. wind_down verify after terminate
|
|
697
|
+
# 5. unbond internal check
|
|
698
|
+
# 6. wind_down verify after unbond
|
|
699
|
+
# 7. final verification
|
|
700
|
+
state_sequence = [
|
|
701
|
+
{
|
|
702
|
+
"state": ServiceState.DEPLOYED,
|
|
703
|
+
"security_deposit": 50000000000000000000,
|
|
704
|
+
}, # wind_down initial
|
|
705
|
+
{
|
|
706
|
+
"state": ServiceState.DEPLOYED,
|
|
707
|
+
"security_deposit": 50000000000000000000,
|
|
708
|
+
}, # terminate internal check
|
|
709
|
+
{
|
|
710
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
711
|
+
"security_deposit": 50000000000000000000,
|
|
712
|
+
}, # wind_down verify after terminate
|
|
713
|
+
{
|
|
714
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
715
|
+
"security_deposit": 50000000000000000000,
|
|
716
|
+
}, # unbond internal check
|
|
717
|
+
{
|
|
718
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
719
|
+
"security_deposit": 50000000000000000000,
|
|
720
|
+
}, # wind_down verify after unbond
|
|
721
|
+
{
|
|
722
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
723
|
+
"security_deposit": 50000000000000000000,
|
|
724
|
+
}, # final verification
|
|
725
|
+
]
|
|
726
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
727
|
+
service_manager.service.staking_contract_address = None
|
|
728
|
+
|
|
729
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
730
|
+
service_manager.registry.extract_events.side_effect = [
|
|
731
|
+
[{"name": "TerminateService"}],
|
|
732
|
+
[{"name": "OperatorUnbond"}],
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
assert service_manager.wind_down() is True
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def test_wind_down_from_staked(service_manager, mock_wallet):
|
|
739
|
+
"""Test wind_down handles unstaking first."""
|
|
740
|
+
state_sequence = [
|
|
741
|
+
{
|
|
742
|
+
"state": ServiceState.DEPLOYED,
|
|
743
|
+
"security_deposit": 50000000000000000000,
|
|
744
|
+
}, # wind_down initial
|
|
745
|
+
{
|
|
746
|
+
"state": ServiceState.DEPLOYED,
|
|
747
|
+
"security_deposit": 50000000000000000000,
|
|
748
|
+
}, # terminate internal check
|
|
749
|
+
{
|
|
750
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
751
|
+
"security_deposit": 50000000000000000000,
|
|
752
|
+
}, # wind_down verify after terminate
|
|
753
|
+
{
|
|
754
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
755
|
+
"security_deposit": 50000000000000000000,
|
|
756
|
+
}, # unbond internal check
|
|
757
|
+
{
|
|
758
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
759
|
+
"security_deposit": 50000000000000000000,
|
|
760
|
+
}, # wind_down verify after unbond
|
|
761
|
+
{
|
|
762
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
763
|
+
"security_deposit": 50000000000000000000,
|
|
764
|
+
}, # final verification
|
|
765
|
+
]
|
|
766
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
767
|
+
service_manager.service.staking_contract_address = TEST_STAKING_ADDR
|
|
768
|
+
|
|
769
|
+
staking_contract = MagicMock()
|
|
770
|
+
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
771
|
+
|
|
772
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
773
|
+
staking_contract.extract_events.return_value = [{"name": "ServiceUnstaked"}]
|
|
774
|
+
service_manager.registry.extract_events.side_effect = [
|
|
775
|
+
[{"name": "TerminateService"}],
|
|
776
|
+
[{"name": "OperatorUnbond"}],
|
|
777
|
+
]
|
|
778
|
+
|
|
779
|
+
assert service_manager.wind_down(staking_contract=staking_contract) is True
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_wind_down_from_terminated(service_manager, mock_wallet):
|
|
783
|
+
"""Test wind_down resume from TERMINATED_BONDED state."""
|
|
784
|
+
# When starting from TERMINATED_BONDED:
|
|
785
|
+
# 1. wind_down initial check (line 586)
|
|
786
|
+
# 2. wind_down refresh after unstake block (line 607) - always called
|
|
787
|
+
# 3. unbond internal check (line 323)
|
|
788
|
+
# 4. wind_down verify after unbond (line 633)
|
|
789
|
+
# 5. final verification (line 642)
|
|
790
|
+
state_sequence = [
|
|
791
|
+
{
|
|
792
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
793
|
+
"security_deposit": 50000000000000000000,
|
|
794
|
+
}, # wind_down initial
|
|
795
|
+
{
|
|
796
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
797
|
+
"security_deposit": 50000000000000000000,
|
|
798
|
+
}, # unbond internal check
|
|
799
|
+
{
|
|
800
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
801
|
+
"security_deposit": 50000000000000000000,
|
|
802
|
+
}, # wind_down verify after unbond
|
|
803
|
+
{
|
|
804
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
805
|
+
"security_deposit": 50000000000000000000,
|
|
806
|
+
}, # final verification
|
|
807
|
+
]
|
|
808
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
809
|
+
|
|
810
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
811
|
+
service_manager.registry.extract_events.return_value = [{"name": "OperatorUnbond"}]
|
|
812
|
+
|
|
813
|
+
assert service_manager.wind_down() is True
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def test_wind_down_already_pre_registration(service_manager, mock_wallet):
|
|
817
|
+
"""Test wind_down when already PRE_REGISTRATION (idempotent)."""
|
|
818
|
+
service_manager.registry.get_service.return_value = {
|
|
819
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
820
|
+
"security_deposit": 50000000000000000000,
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
# Should succeed without any transactions
|
|
824
|
+
assert service_manager.wind_down() is True
|
|
825
|
+
mock_wallet.sign_and_send_transaction.assert_not_called()
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def test_wind_down_staked_no_contract_provided(service_manager, mock_wallet):
|
|
829
|
+
"""Test wind_down fails when staked but no staking contract provided."""
|
|
830
|
+
service_manager.registry.get_service.return_value = {
|
|
831
|
+
"state": ServiceState.DEPLOYED,
|
|
832
|
+
"security_deposit": 50000000000000000000,
|
|
833
|
+
}
|
|
834
|
+
service_manager.service.staking_contract_address = TEST_STAKING_ADDR
|
|
835
|
+
|
|
836
|
+
# No staking_contract provided
|
|
837
|
+
assert service_manager.wind_down() is False
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def test_wind_down_unstake_fails(service_manager, mock_wallet):
|
|
841
|
+
"""Test wind_down fails when unstake fails."""
|
|
842
|
+
service_manager.registry.get_service.return_value = {
|
|
843
|
+
"state": ServiceState.DEPLOYED,
|
|
844
|
+
"security_deposit": 50000000000000000000,
|
|
845
|
+
}
|
|
846
|
+
service_manager.service.staking_contract_address = TEST_STAKING_ADDR
|
|
847
|
+
|
|
848
|
+
staking_contract = MagicMock()
|
|
849
|
+
staking_contract.get_staking_state.return_value = StakingState.STAKED
|
|
850
|
+
|
|
851
|
+
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
852
|
+
|
|
853
|
+
assert service_manager.wind_down(staking_contract=staking_contract) is False
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def test_wind_down_terminate_fails(service_manager, mock_wallet):
|
|
857
|
+
"""Test wind_down fails when terminate fails."""
|
|
858
|
+
state_sequence = [
|
|
859
|
+
{
|
|
860
|
+
"state": ServiceState.DEPLOYED,
|
|
861
|
+
"security_deposit": 50000000000000000000,
|
|
862
|
+
}, # wind_down initial
|
|
863
|
+
{
|
|
864
|
+
"state": ServiceState.DEPLOYED,
|
|
865
|
+
"security_deposit": 50000000000000000000,
|
|
866
|
+
}, # wind_down refresh after unstake check
|
|
867
|
+
{
|
|
868
|
+
"state": ServiceState.DEPLOYED,
|
|
869
|
+
"security_deposit": 50000000000000000000,
|
|
870
|
+
}, # terminate internal check
|
|
871
|
+
]
|
|
872
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
873
|
+
service_manager.service.staking_contract_address = None
|
|
874
|
+
|
|
875
|
+
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
876
|
+
|
|
877
|
+
assert service_manager.wind_down() is False
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def test_wind_down_unbond_fails(service_manager, mock_wallet):
|
|
881
|
+
"""Test wind_down fails when unbond fails."""
|
|
882
|
+
# When starting from TERMINATED_BONDED and unbond fails:
|
|
883
|
+
# 1. wind_down initial check (line 586)
|
|
884
|
+
# 2. wind_down refresh after unstake block (line 607) - always called
|
|
885
|
+
# 3. unbond internal check (line 323)
|
|
886
|
+
state_sequence = [
|
|
887
|
+
{
|
|
888
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
889
|
+
"security_deposit": 50000000000000000000,
|
|
890
|
+
}, # wind_down initial
|
|
891
|
+
{
|
|
892
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
893
|
+
"security_deposit": 50000000000000000000,
|
|
894
|
+
}, # wind_down refresh
|
|
895
|
+
{
|
|
896
|
+
"state": ServiceState.TERMINATED_BONDED,
|
|
897
|
+
"security_deposit": 50000000000000000000,
|
|
898
|
+
}, # unbond internal check
|
|
899
|
+
]
|
|
900
|
+
service_manager.registry.get_service.side_effect = state_sequence
|
|
901
|
+
|
|
902
|
+
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
903
|
+
|
|
904
|
+
assert service_manager.wind_down() is False
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
# --- REGRESSION TESTS for activate_registration (Dec 2025 bug fix) ---
|
|
908
|
+
# These tests ensure the value parameter is ALWAYS set to security_deposit
|
|
909
|
+
# and that the master account is used as signer, preventing the regression
|
|
910
|
+
# where value=0 was incorrectly sent for token-based services.
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def test_activate_registration_token_service_sends_security_deposit_as_value(
|
|
914
|
+
service_manager, mock_wallet
|
|
915
|
+
):
|
|
916
|
+
"""REGRESSION TEST: Token services MUST send security_deposit as msg.value.
|
|
917
|
+
|
|
918
|
+
Bug context: A previous change incorrectly set value=0 for token-based services,
|
|
919
|
+
but the ServiceManager contract REQUIRES msg.value == security_deposit even for
|
|
920
|
+
token services (where security_deposit is typically 1 wei).
|
|
921
|
+
"""
|
|
922
|
+
security_deposit = 1 # 1 wei for token services
|
|
923
|
+
service_manager.service.token_address = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" # OLAS
|
|
924
|
+
|
|
925
|
+
service_manager.registry.get_service.return_value = {
|
|
926
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
927
|
+
"security_deposit": security_deposit,
|
|
928
|
+
}
|
|
929
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
930
|
+
service_manager.registry.extract_events.return_value = [{"name": "ActivateRegistration"}]
|
|
931
|
+
|
|
932
|
+
# Mock balance check to pass
|
|
933
|
+
mock_wallet.balance_service = MagicMock()
|
|
934
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18 # Plenty of balance
|
|
935
|
+
|
|
936
|
+
# Mock allowance to pass check (return an int)
|
|
937
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20 # Plenty of allowance
|
|
938
|
+
|
|
939
|
+
service_manager.activate_registration()
|
|
940
|
+
|
|
941
|
+
# Verify the CRITICAL parameter - value MUST equal security_deposit
|
|
942
|
+
call_args = service_manager.manager.prepare_activate_registration_tx.call_args
|
|
943
|
+
assert call_args is not None, "prepare_activate_registration_tx was not called"
|
|
944
|
+
assert call_args.kwargs.get("value") == security_deposit, (
|
|
945
|
+
f"REGRESSION: value should be {security_deposit} (security_deposit), "
|
|
946
|
+
f"got {call_args.kwargs.get('value')}"
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def test_activate_registration_native_service_sends_security_deposit_as_value(
|
|
951
|
+
service_manager, mock_wallet
|
|
952
|
+
):
|
|
953
|
+
"""REGRESSION TEST: Native services MUST send security_deposit as msg.value."""
|
|
954
|
+
security_deposit = 50000000000000000000 # 50 xDAI for native services
|
|
955
|
+
service_manager.service.token_address = None # Native service
|
|
956
|
+
|
|
957
|
+
service_manager.registry.get_service.return_value = {
|
|
958
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
959
|
+
"security_deposit": security_deposit,
|
|
960
|
+
}
|
|
961
|
+
service_manager.registry.get_token.return_value = "0x0000000000000000000000000000000000000000"
|
|
962
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
963
|
+
service_manager.registry.extract_events.return_value = [{"name": "ActivateRegistration"}]
|
|
964
|
+
|
|
965
|
+
# Mock balance/allowance
|
|
966
|
+
mock_wallet.balance_service = MagicMock()
|
|
967
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
968
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
969
|
+
|
|
970
|
+
service_manager.activate_registration()
|
|
971
|
+
|
|
972
|
+
# Verify the CRITICAL parameter - value MUST equal security_deposit
|
|
973
|
+
call_args = service_manager.manager.prepare_activate_registration_tx.call_args
|
|
974
|
+
assert call_args is not None, "prepare_activate_registration_tx was not called"
|
|
975
|
+
assert call_args.kwargs.get("value") == security_deposit, (
|
|
976
|
+
f"REGRESSION: value should be {security_deposit} (security_deposit), "
|
|
977
|
+
f"got {call_args.kwargs.get('value')}"
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def test_activate_registration_uses_master_account_as_from_address(service_manager, mock_wallet):
|
|
982
|
+
"""REGRESSION TEST: activate_registration MUST use master_account.address as from_address.
|
|
983
|
+
|
|
984
|
+
Bug context: A previous change used service_owner_address instead of master_account,
|
|
985
|
+
which could fail if they differ or if master_account is the only funded account.
|
|
986
|
+
"""
|
|
987
|
+
master_address = mock_wallet.master_account.address
|
|
988
|
+
|
|
989
|
+
service_manager.registry.get_service.return_value = {
|
|
990
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
991
|
+
"security_deposit": 1,
|
|
992
|
+
}
|
|
993
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
994
|
+
service_manager.registry.extract_events.return_value = [{"name": "ActivateRegistration"}]
|
|
995
|
+
|
|
996
|
+
# Mock balance/allowance
|
|
997
|
+
mock_wallet.balance_service = MagicMock()
|
|
998
|
+
mock_wallet.transfer_service = MagicMock()
|
|
999
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
1000
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
1001
|
+
|
|
1002
|
+
service_manager.activate_registration()
|
|
1003
|
+
|
|
1004
|
+
# Verify master_account is used as from_address
|
|
1005
|
+
call_args = service_manager.manager.prepare_activate_registration_tx.call_args
|
|
1006
|
+
assert call_args is not None, "prepare_activate_registration_tx was not called"
|
|
1007
|
+
assert call_args.kwargs.get("from_address") == master_address, (
|
|
1008
|
+
f"REGRESSION: from_address should be master_account.address ({master_address}), "
|
|
1009
|
+
f"got {call_args.kwargs.get('from_address')}"
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def test_activate_registration_uses_master_account_as_signer(service_manager, mock_wallet):
|
|
1014
|
+
"""REGRESSION TEST: activate_registration MUST use master_account.address as signer.
|
|
1015
|
+
|
|
1016
|
+
Bug context: A previous change used service_owner_address as signer,
|
|
1017
|
+
which could fail transaction signing.
|
|
1018
|
+
"""
|
|
1019
|
+
master_address = mock_wallet.master_account.address
|
|
1020
|
+
|
|
1021
|
+
service_manager.registry.get_service.return_value = {
|
|
1022
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
1023
|
+
"security_deposit": 1,
|
|
1024
|
+
}
|
|
1025
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
1026
|
+
service_manager.registry.extract_events.return_value = [{"name": "ActivateRegistration"}]
|
|
1027
|
+
|
|
1028
|
+
# Mock balance/allowance
|
|
1029
|
+
mock_wallet.balance_service = MagicMock()
|
|
1030
|
+
mock_wallet.transfer_service = MagicMock()
|
|
1031
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
1032
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
|
|
1033
|
+
|
|
1034
|
+
service_manager.activate_registration()
|
|
1035
|
+
|
|
1036
|
+
# Verify master_account is used as signer
|
|
1037
|
+
call_args = mock_wallet.sign_and_send_transaction.call_args
|
|
1038
|
+
assert call_args is not None, "sign_and_send_transaction was not called"
|
|
1039
|
+
assert call_args.kwargs.get("signer_address_or_tag") == master_address, (
|
|
1040
|
+
f"REGRESSION: signer should be master_account.address ({master_address}), "
|
|
1041
|
+
f"got {call_args.kwargs.get('signer_address_or_tag')}"
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def test_activate_registration_token_service_approves_token_utility(service_manager, mock_wallet):
|
|
1046
|
+
"""TEST: Token services should trigger TokenUtility approval when allowance is low."""
|
|
1047
|
+
service_manager.service.token_address = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" # OLAS
|
|
1048
|
+
|
|
1049
|
+
service_manager.registry.get_service.return_value = {
|
|
1050
|
+
"state": ServiceState.PRE_REGISTRATION,
|
|
1051
|
+
"security_deposit": 1,
|
|
1052
|
+
}
|
|
1053
|
+
mock_wallet.sign_and_send_transaction.return_value = (True, {})
|
|
1054
|
+
service_manager.registry.extract_events.return_value = [{"name": "ActivateRegistration"}]
|
|
1055
|
+
|
|
1056
|
+
# Mock low allowance to trigger approval
|
|
1057
|
+
mock_wallet.balance_service = MagicMock()
|
|
1058
|
+
mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
|
|
1059
|
+
mock_wallet.transfer_service.get_erc20_allowance.return_value = 0 # Low allowance
|
|
1060
|
+
mock_wallet.transfer_service.approve_erc20.return_value = True
|
|
1061
|
+
|
|
1062
|
+
service_manager.activate_registration()
|
|
1063
|
+
|
|
1064
|
+
# Verify approval was called
|
|
1065
|
+
mock_wallet.transfer_service.approve_erc20.assert_called()
|