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.
Files changed (191) hide show
  1. conftest.py +22 -0
  2. iwa/__init__.py +1 -0
  3. iwa/__main__.py +6 -0
  4. iwa/core/__init__.py +1 -0
  5. iwa/core/chain/__init__.py +68 -0
  6. iwa/core/chain/errors.py +47 -0
  7. iwa/core/chain/interface.py +514 -0
  8. iwa/core/chain/manager.py +38 -0
  9. iwa/core/chain/models.py +128 -0
  10. iwa/core/chain/rate_limiter.py +193 -0
  11. iwa/core/cli.py +210 -0
  12. iwa/core/constants.py +28 -0
  13. iwa/core/contracts/__init__.py +1 -0
  14. iwa/core/contracts/contract.py +297 -0
  15. iwa/core/contracts/erc20.py +79 -0
  16. iwa/core/contracts/multisend.py +71 -0
  17. iwa/core/db.py +317 -0
  18. iwa/core/keys.py +361 -0
  19. iwa/core/mnemonic.py +385 -0
  20. iwa/core/models.py +344 -0
  21. iwa/core/monitor.py +209 -0
  22. iwa/core/plugins.py +45 -0
  23. iwa/core/pricing.py +91 -0
  24. iwa/core/services/__init__.py +17 -0
  25. iwa/core/services/account.py +57 -0
  26. iwa/core/services/balance.py +113 -0
  27. iwa/core/services/plugin.py +88 -0
  28. iwa/core/services/safe.py +392 -0
  29. iwa/core/services/transaction.py +172 -0
  30. iwa/core/services/transfer/__init__.py +166 -0
  31. iwa/core/services/transfer/base.py +260 -0
  32. iwa/core/services/transfer/erc20.py +247 -0
  33. iwa/core/services/transfer/multisend.py +386 -0
  34. iwa/core/services/transfer/native.py +262 -0
  35. iwa/core/services/transfer/swap.py +326 -0
  36. iwa/core/settings.py +95 -0
  37. iwa/core/tables.py +60 -0
  38. iwa/core/test.py +27 -0
  39. iwa/core/tests/test_wallet.py +255 -0
  40. iwa/core/types.py +59 -0
  41. iwa/core/ui.py +99 -0
  42. iwa/core/utils.py +59 -0
  43. iwa/core/wallet.py +380 -0
  44. iwa/plugins/__init__.py +1 -0
  45. iwa/plugins/gnosis/__init__.py +5 -0
  46. iwa/plugins/gnosis/cow/__init__.py +6 -0
  47. iwa/plugins/gnosis/cow/quotes.py +148 -0
  48. iwa/plugins/gnosis/cow/swap.py +403 -0
  49. iwa/plugins/gnosis/cow/types.py +20 -0
  50. iwa/plugins/gnosis/cow_utils.py +44 -0
  51. iwa/plugins/gnosis/plugin.py +68 -0
  52. iwa/plugins/gnosis/safe.py +157 -0
  53. iwa/plugins/gnosis/tests/test_cow.py +227 -0
  54. iwa/plugins/gnosis/tests/test_safe.py +100 -0
  55. iwa/plugins/olas/__init__.py +5 -0
  56. iwa/plugins/olas/constants.py +106 -0
  57. iwa/plugins/olas/contracts/activity_checker.py +93 -0
  58. iwa/plugins/olas/contracts/base.py +10 -0
  59. iwa/plugins/olas/contracts/mech.py +49 -0
  60. iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
  61. iwa/plugins/olas/contracts/service.py +215 -0
  62. iwa/plugins/olas/contracts/staking.py +403 -0
  63. iwa/plugins/olas/importer.py +736 -0
  64. iwa/plugins/olas/mech_reference.py +135 -0
  65. iwa/plugins/olas/models.py +110 -0
  66. iwa/plugins/olas/plugin.py +243 -0
  67. iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
  68. iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
  69. iwa/plugins/olas/service_manager/__init__.py +60 -0
  70. iwa/plugins/olas/service_manager/base.py +113 -0
  71. iwa/plugins/olas/service_manager/drain.py +336 -0
  72. iwa/plugins/olas/service_manager/lifecycle.py +839 -0
  73. iwa/plugins/olas/service_manager/mech.py +322 -0
  74. iwa/plugins/olas/service_manager/staking.py +530 -0
  75. iwa/plugins/olas/tests/conftest.py +30 -0
  76. iwa/plugins/olas/tests/test_importer.py +128 -0
  77. iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
  78. iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
  79. iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
  80. iwa/plugins/olas/tests/test_olas_integration.py +561 -0
  81. iwa/plugins/olas/tests/test_olas_models.py +144 -0
  82. iwa/plugins/olas/tests/test_olas_view.py +258 -0
  83. iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
  84. iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
  85. iwa/plugins/olas/tests/test_plugin.py +70 -0
  86. iwa/plugins/olas/tests/test_plugin_full.py +212 -0
  87. iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
  88. iwa/plugins/olas/tests/test_service_manager.py +1065 -0
  89. iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
  90. iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
  91. iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
  92. iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
  93. iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
  94. iwa/plugins/olas/tests/test_service_staking.py +342 -0
  95. iwa/plugins/olas/tests/test_staking_integration.py +269 -0
  96. iwa/plugins/olas/tests/test_staking_validation.py +109 -0
  97. iwa/plugins/olas/tui/__init__.py +1 -0
  98. iwa/plugins/olas/tui/olas_view.py +952 -0
  99. iwa/tools/check_profile.py +67 -0
  100. iwa/tools/release.py +111 -0
  101. iwa/tools/reset_env.py +111 -0
  102. iwa/tools/reset_tenderly.py +362 -0
  103. iwa/tools/restore_backup.py +82 -0
  104. iwa/tui/__init__.py +1 -0
  105. iwa/tui/app.py +174 -0
  106. iwa/tui/modals/__init__.py +5 -0
  107. iwa/tui/modals/base.py +406 -0
  108. iwa/tui/rpc.py +63 -0
  109. iwa/tui/screens/__init__.py +1 -0
  110. iwa/tui/screens/wallets.py +749 -0
  111. iwa/tui/tests/test_app.py +125 -0
  112. iwa/tui/tests/test_rpc.py +139 -0
  113. iwa/tui/tests/test_wallets_refactor.py +30 -0
  114. iwa/tui/tests/test_widgets.py +123 -0
  115. iwa/tui/widgets/__init__.py +5 -0
  116. iwa/tui/widgets/base.py +100 -0
  117. iwa/tui/workers.py +42 -0
  118. iwa/web/dependencies.py +76 -0
  119. iwa/web/models.py +76 -0
  120. iwa/web/routers/accounts.py +115 -0
  121. iwa/web/routers/olas/__init__.py +24 -0
  122. iwa/web/routers/olas/admin.py +169 -0
  123. iwa/web/routers/olas/funding.py +135 -0
  124. iwa/web/routers/olas/general.py +29 -0
  125. iwa/web/routers/olas/services.py +378 -0
  126. iwa/web/routers/olas/staking.py +341 -0
  127. iwa/web/routers/state.py +65 -0
  128. iwa/web/routers/swap.py +617 -0
  129. iwa/web/routers/transactions.py +153 -0
  130. iwa/web/server.py +155 -0
  131. iwa/web/tests/test_web_endpoints.py +713 -0
  132. iwa/web/tests/test_web_olas.py +430 -0
  133. iwa/web/tests/test_web_swap.py +103 -0
  134. iwa-0.0.1a2.dist-info/METADATA +234 -0
  135. iwa-0.0.1a2.dist-info/RECORD +186 -0
  136. iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
  137. iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
  138. iwa-0.0.1a2.dist-info/top_level.txt +4 -0
  139. tests/legacy_cow.py +248 -0
  140. tests/legacy_safe.py +93 -0
  141. tests/legacy_transaction_retry_logic.py +51 -0
  142. tests/legacy_tui.py +440 -0
  143. tests/legacy_wallets_screen.py +554 -0
  144. tests/legacy_web.py +243 -0
  145. tests/test_account_service.py +120 -0
  146. tests/test_balance_service.py +186 -0
  147. tests/test_chain.py +490 -0
  148. tests/test_chain_interface.py +210 -0
  149. tests/test_cli.py +139 -0
  150. tests/test_contract.py +195 -0
  151. tests/test_db.py +180 -0
  152. tests/test_drain_coverage.py +174 -0
  153. tests/test_erc20.py +95 -0
  154. tests/test_gnosis_plugin.py +111 -0
  155. tests/test_keys.py +449 -0
  156. tests/test_legacy_wallet.py +1285 -0
  157. tests/test_main.py +13 -0
  158. tests/test_mnemonic.py +217 -0
  159. tests/test_modals.py +109 -0
  160. tests/test_models.py +213 -0
  161. tests/test_monitor.py +202 -0
  162. tests/test_multisend.py +84 -0
  163. tests/test_plugin_service.py +119 -0
  164. tests/test_pricing.py +143 -0
  165. tests/test_rate_limiter.py +199 -0
  166. tests/test_reset_tenderly.py +202 -0
  167. tests/test_rpc_view.py +73 -0
  168. tests/test_safe_coverage.py +139 -0
  169. tests/test_safe_service.py +168 -0
  170. tests/test_service_manager_integration.py +61 -0
  171. tests/test_service_manager_structure.py +31 -0
  172. tests/test_service_transaction.py +176 -0
  173. tests/test_staking_router.py +71 -0
  174. tests/test_staking_simple.py +31 -0
  175. tests/test_tables.py +76 -0
  176. tests/test_transaction_service.py +161 -0
  177. tests/test_transfer_multisend.py +179 -0
  178. tests/test_transfer_native.py +220 -0
  179. tests/test_transfer_security.py +93 -0
  180. tests/test_transfer_structure.py +37 -0
  181. tests/test_transfer_swap_unit.py +155 -0
  182. tests/test_ui_coverage.py +66 -0
  183. tests/test_utils.py +53 -0
  184. tests/test_workers.py +91 -0
  185. tools/verify_drain.py +183 -0
  186. __init__.py +0 -2
  187. hello.py +0 -6
  188. iwa-0.0.0.dist-info/METADATA +0 -10
  189. iwa-0.0.0.dist-info/RECORD +0 -6
  190. iwa-0.0.0.dist-info/top_level.txt +0 -2
  191. {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()