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,839 @@
1
+ """Lifecycle manager mixin."""
2
+
3
+ from typing import List, Optional, Union
4
+
5
+ from loguru import logger
6
+ from web3 import Web3
7
+ from web3.types import Wei
8
+
9
+ from iwa.core.chain import ChainInterfaces
10
+ from iwa.core.constants import NATIVE_CURRENCY_ADDRESS, ZERO_ADDRESS
11
+ from iwa.core.types import EthereumAddress
12
+ from iwa.plugins.olas.constants import (
13
+ OLAS_CONTRACTS,
14
+ TRADER_CONFIG_HASH,
15
+ AgentType,
16
+ )
17
+ from iwa.plugins.olas.contracts.service import ServiceState
18
+ from iwa.plugins.olas.models import Service
19
+
20
+
21
+ class LifecycleManagerMixin:
22
+ """Mixin for service lifecycle operations."""
23
+
24
+ def create(
25
+ self,
26
+ chain_name: str = "gnosis",
27
+ service_name: Optional[str] = None,
28
+ agent_ids: Optional[List[Union[AgentType, int]]] = None,
29
+ service_owner_address_or_tag: Optional[str] = None,
30
+ token_address_or_tag: Optional[str] = None,
31
+ bond_amount_wei: Wei = 1, # type: ignore
32
+ ) -> Optional[int]:
33
+ """Create a new service.
34
+
35
+ Args:
36
+ chain_name: The blockchain to create the service on.
37
+ service_name: Human-readable name for the service (auto-generated if not provided).
38
+ agent_ids: List of agent type IDs or AgentType enum values.
39
+ Defaults to [AgentType.TRADER] if not provided.
40
+ service_owner_address_or_tag: The owner address or tag.
41
+ token_address_or_tag: Token address for staking (optional).
42
+ bond_amount_wei: Bond amount in tokens.
43
+
44
+ Returns:
45
+ The service_id if successful, None otherwise.
46
+
47
+ """
48
+ # Default to TRADER if no agents specified
49
+ if agent_ids is None:
50
+ agent_ids = [AgentType.TRADER]
51
+
52
+ # Convert AgentType enums to ints
53
+ agent_id_values = [int(a) for a in agent_ids]
54
+
55
+ service_owner_account = (
56
+ self.wallet.key_storage.get_account(service_owner_address_or_tag)
57
+ if service_owner_address_or_tag
58
+ else self.wallet.master_account
59
+ )
60
+ chain = ChainInterfaces().get(chain_name).chain
61
+ token_address = chain.get_token_address(token_address_or_tag)
62
+
63
+ agent_params = self._prepare_agent_params(agent_id_values, bond_amount_wei)
64
+
65
+ logger.info(
66
+ f"Preparing create tx: owner={service_owner_account.address}, "
67
+ f"token={token_address}, agent_ids={agent_id_values}, agent_params={agent_params}"
68
+ )
69
+
70
+ receipt = self._send_create_transaction(
71
+ service_owner_account=service_owner_account,
72
+ token_address=token_address,
73
+ agent_id_values=agent_id_values,
74
+ agent_params=agent_params,
75
+ chain_name=chain_name,
76
+ )
77
+
78
+ if receipt is None:
79
+ return None
80
+
81
+ service_id = self._extract_service_id_from_receipt(receipt)
82
+ if not service_id:
83
+ return None
84
+
85
+ self._save_new_service(
86
+ service_id=service_id,
87
+ service_name=service_name,
88
+ chain_name=chain_name,
89
+ agent_id_values=agent_id_values,
90
+ service_owner_address=service_owner_account.address,
91
+ token_address=token_address,
92
+ )
93
+
94
+ self._approve_token_if_needed(
95
+ token_address=token_address,
96
+ chain_name=chain_name,
97
+ service_owner_account=service_owner_account,
98
+ bond_amount_wei=bond_amount_wei,
99
+ )
100
+
101
+ return service_id
102
+
103
+ def _prepare_agent_params(self, agent_id_values: List[int], bond_amount_wei: Wei) -> List[dict]:
104
+ """Prepare agent parameters for service creation."""
105
+ # Create agent_params: [[instances_per_agent, bond_amount_wei], ...]
106
+ # Use dictionary for explicit struct encoding
107
+ return [{"slots": 1, "bond": bond_amount_wei} for _ in agent_id_values]
108
+
109
+ def _send_create_transaction(
110
+ self,
111
+ service_owner_account,
112
+ token_address,
113
+ agent_id_values: List[int],
114
+ agent_params: List[dict],
115
+ chain_name: str,
116
+ ) -> Optional[dict]:
117
+ """Prepare and send the create service transaction."""
118
+ try:
119
+ create_tx = self.manager.prepare_create_tx(
120
+ from_address=self.wallet.master_account.address,
121
+ service_owner=service_owner_account.address,
122
+ token_address=token_address if token_address else NATIVE_CURRENCY_ADDRESS,
123
+ config_hash=bytes.fromhex(TRADER_CONFIG_HASH),
124
+ agent_ids=agent_id_values,
125
+ agent_params=agent_params,
126
+ threshold=1,
127
+ )
128
+ except Exception as e:
129
+ logger.error(f"prepare_create_tx failed: {e}")
130
+ return None
131
+
132
+ if not create_tx:
133
+ logger.error("prepare_create_tx returned None (preparation failed)")
134
+ return None
135
+
136
+ logger.info(f"Prepared create_tx: to={create_tx.get('to')}, value={create_tx.get('value')}")
137
+ success, receipt = self.wallet.sign_and_send_transaction(
138
+ transaction=create_tx,
139
+ signer_address_or_tag=self.wallet.master_account.address,
140
+ chain_name=chain_name,
141
+ tags=["olas_create_service"],
142
+ )
143
+
144
+ if not success:
145
+ logger.error(
146
+ f"Failed to create service - sign_and_send returned False. Receipt: {receipt}"
147
+ )
148
+ return None
149
+
150
+ logger.info("Service creation transaction sent successfully")
151
+ return receipt
152
+
153
+ def _extract_service_id_from_receipt(self, receipt: dict) -> Optional[int]:
154
+ """Extract service ID from transaction receipt events."""
155
+ events = self.registry.extract_events(receipt)
156
+ for event in events:
157
+ if event["name"] == "CreateService":
158
+ service_id = event["args"]["serviceId"]
159
+ logger.info(f"Service created with ID: {service_id}")
160
+ return service_id
161
+ logger.error("Service creation event not found or service ID not in event")
162
+ return None
163
+
164
+ def _save_new_service(
165
+ self,
166
+ service_id: int,
167
+ service_name: Optional[str],
168
+ chain_name: str,
169
+ agent_id_values: List[int],
170
+ service_owner_address: str,
171
+ token_address: Optional[str],
172
+ ) -> None:
173
+ """Create and save the new Service model."""
174
+ new_service = Service(
175
+ service_name=service_name or f"service_{service_id}",
176
+ chain_name=chain_name,
177
+ service_id=service_id,
178
+ agent_ids=agent_id_values,
179
+ service_owner_address=service_owner_address,
180
+ token_address=token_address,
181
+ )
182
+
183
+ self.olas_config.add_service(new_service)
184
+ self.service = new_service
185
+ self._save_config()
186
+
187
+ def _approve_token_if_needed(
188
+ self,
189
+ token_address: Optional[str],
190
+ chain_name: str,
191
+ service_owner_account,
192
+ bond_amount_wei: Wei,
193
+ ) -> None:
194
+ """Approve token utility if a token address is provided."""
195
+ if not token_address:
196
+ return
197
+
198
+ # Approve the service registry token utility contract
199
+ protocol_contracts = OLAS_CONTRACTS.get(chain_name.lower(), {})
200
+ utility_address = protocol_contracts.get("OLAS_SERVICE_REGISTRY_TOKEN_UTILITY")
201
+
202
+ if not utility_address:
203
+ logger.error(f"OLAS Service Registry Token Utility not found for chain: {chain_name}")
204
+ return
205
+
206
+ # Approve the token utility to move tokens (2 * bond amount as per Triton reference)
207
+ logger.info(f"Approving Token Utility {utility_address} for {2 * bond_amount_wei} tokens")
208
+ approve_success = self.transfer_service.approve_erc20(
209
+ owner_address_or_tag=service_owner_account.address,
210
+ spender_address_or_tag=utility_address,
211
+ token_address_or_name=token_address,
212
+ amount_wei=2 * bond_amount_wei,
213
+ chain_name=chain_name,
214
+ )
215
+
216
+ if not approve_success:
217
+ logger.error("Failed to approve Token Utility")
218
+
219
+ def activate_registration(self) -> bool:
220
+ """Activate registration for the service."""
221
+ service_id = self.service.service_id
222
+ if not self._validate_pre_registration_state(service_id):
223
+ return False
224
+
225
+ token_address = self._get_service_token(service_id)
226
+ service_info = self.registry.get_service(service_id)
227
+ security_deposit = service_info["security_deposit"]
228
+
229
+ if not self._ensure_token_approval_for_activation(token_address, security_deposit):
230
+ return False
231
+
232
+ return self._send_activation_transaction(service_id, security_deposit)
233
+
234
+ def _validate_pre_registration_state(self, service_id: int) -> bool:
235
+ """Check if service is in PRE_REGISTRATION state."""
236
+ # Check that the service is created
237
+ service_info = self.registry.get_service(service_id)
238
+ service_state = service_info["state"]
239
+ if service_state != ServiceState.PRE_REGISTRATION:
240
+ logger.error("Service is not created, cannot activate registration")
241
+ return False
242
+ return True
243
+
244
+ def _get_service_token(self, service_id: int) -> str:
245
+ """Get the token address for the service, defaulting to native if not found."""
246
+ token_address = self.service.token_address
247
+ if not token_address:
248
+ try:
249
+ token_address = self.registry.get_token(service_id)
250
+ except Exception:
251
+ # Default to native if query fails
252
+ token_address = ZERO_ADDRESS
253
+ return token_address
254
+
255
+ def _ensure_token_approval_for_activation(
256
+ self, token_address: str, security_deposit: Wei
257
+ ) -> bool:
258
+ """Ensure token approval for activation if not native token."""
259
+ is_native = str(token_address).lower() == str(ZERO_ADDRESS).lower()
260
+ if is_native:
261
+ return True
262
+
263
+ try:
264
+ # Check Master Balance first
265
+ balance = self.wallet.balance_service.get_erc20_balance_wei(
266
+ account_address_or_tag=self.service.service_owner_address,
267
+ token_address_or_name=token_address,
268
+ chain_name=self.chain_name,
269
+ )
270
+
271
+ if balance < security_deposit:
272
+ logger.error(
273
+ f"[ACTIVATE] FAIL: Owner balance {balance} < required {security_deposit}"
274
+ )
275
+
276
+ protocol_contracts = OLAS_CONTRACTS.get(self.chain_name.lower(), {})
277
+ utility_address = protocol_contracts.get("OLAS_SERVICE_REGISTRY_TOKEN_UTILITY")
278
+
279
+ if utility_address:
280
+ required_approval = Web3.to_wei(1000, "ether") # Approve generous amount to be safe
281
+
282
+ # Check current allowance
283
+ allowance = self.wallet.transfer_service.get_erc20_allowance(
284
+ owner_address_or_tag=self.service.service_owner_address,
285
+ spender_address=utility_address,
286
+ token_address_or_name=token_address,
287
+ chain_name=self.chain_name,
288
+ )
289
+
290
+ if allowance < Web3.to_wei(10, "ether"): # Min threshold check
291
+ logger.info(
292
+ f"Low allowance ({allowance}). Approving Token Utility {utility_address}"
293
+ )
294
+ success_approve = self.wallet.transfer_service.approve_erc20(
295
+ owner_address_or_tag=self.service.service_owner_address,
296
+ spender_address_or_tag=utility_address,
297
+ token_address_or_name=token_address,
298
+ amount_wei=required_approval,
299
+ chain_name=self.chain_name,
300
+ )
301
+ if not success_approve:
302
+ logger.warning("Token approval transaction returned failure.")
303
+ return False
304
+ return True
305
+ except Exception as e:
306
+ logger.warning(f"Failed to check/approve tokens: {e}")
307
+ return False # Return False only if we are strict, or True if we want to try anyway?
308
+ # Original code swallowed exception but continued.
309
+ # If we want to return early, we should return False.
310
+ # However, if we swallow, we return True. Let's stick to original behavior,
311
+ # BUT original code didn't return False here, it just logged and continued.
312
+ # To be safer with clean code, if token approval fails, we should probably stop.
313
+ # Let's assume we return True to match original "swallow" behavior but log it.
314
+ return True
315
+
316
+ def _send_activation_transaction(self, service_id: int, security_deposit: Wei) -> bool:
317
+ """Send the activation transaction."""
318
+ # Prepare activation transaction
319
+ # NOTE: For token-based services, the security deposit is handled by the TokenUtility via transferFrom.
320
+ # However, the ServiceManager (and Registry) REQUIRES that msg.value == security_deposit
321
+ # even for token-based services (where security_deposit is typically 1 wei).
322
+ # This native value (1 wei) acts as a protocol validation or fee and MUST be sent.
323
+ # The 'value' parameter here corresponds to msg.value in the transaction.
324
+ activate_tx = self.manager.prepare_activate_registration_tx(
325
+ from_address=self.wallet.master_account.address,
326
+ service_id=service_id,
327
+ value=security_deposit,
328
+ )
329
+ success, receipt = self.wallet.sign_and_send_transaction(
330
+ transaction=activate_tx,
331
+ signer_address_or_tag=self.wallet.master_account.address,
332
+ chain_name=self.chain_name,
333
+ )
334
+
335
+ if not success:
336
+ logger.error("Failed to activate registration")
337
+ return False
338
+
339
+ logger.info("Registration activation transaction sent successfully")
340
+
341
+ events = self.registry.extract_events(receipt)
342
+
343
+ if "ActivateRegistration" not in [event["name"] for event in events]:
344
+ logger.error("Activation event not found")
345
+ return False
346
+
347
+ return True
348
+
349
+ def register_agent(
350
+ self, agent_address: Optional[str] = None, bond_amount_wei: Optional[Wei] = None
351
+ ) -> bool:
352
+ """Register an agent for the service.
353
+
354
+ Args:
355
+ agent_address: Optional existing agent address to use.
356
+ If not provided, a new agent account will be created and funded.
357
+ bond_amount_wei: The amount of tokens to bond for the agent. Required for token-bonded services.
358
+
359
+ Returns:
360
+ True if registration succeeded, False otherwise.
361
+
362
+ """
363
+ if not self._validate_active_registration_state():
364
+ return False
365
+
366
+ agent_account_address = self._get_or_create_agent_account(agent_address)
367
+ if not agent_account_address:
368
+ return False
369
+
370
+ if not self._ensure_agent_token_approval(agent_account_address, bond_amount_wei):
371
+ return False
372
+
373
+ return self._send_register_agent_transaction(agent_account_address)
374
+
375
+ def _validate_active_registration_state(self) -> bool:
376
+ """Check that the service is in active registration."""
377
+ service_state = self.registry.get_service(self.service.service_id)["state"]
378
+ if service_state != ServiceState.ACTIVE_REGISTRATION:
379
+ logger.error("Service is not in active registration, cannot register agent")
380
+ return False
381
+ return True
382
+
383
+ def _get_or_create_agent_account(self, agent_address: Optional[str]) -> Optional[str]:
384
+ """Get existing agent address or create and fund a new one."""
385
+ if agent_address:
386
+ logger.info(f"Using existing agent address: {agent_address}")
387
+ return agent_address
388
+
389
+ # Create a new account for the service (or use existing if found)
390
+ # Use service_name for consistency with Safe naming
391
+ agent_tag = f"{self.service.service_name}_agent"
392
+ try:
393
+ agent_account = self.wallet.key_storage.create_account(agent_tag)
394
+ agent_account_address = agent_account.address
395
+ logger.info(f"Created new agent account: {agent_account_address}")
396
+
397
+ # Fund the agent account with some native currency for gas
398
+ # This is needed for the agent to approve the token utility
399
+ logger.info(f"Funding agent account {agent_account_address} with 0.1 xDAI")
400
+ tx_hash = self.wallet.send(
401
+ from_address_or_tag=self.wallet.master_account.address,
402
+ to_address_or_tag=agent_account_address,
403
+ token_address_or_name="native",
404
+ amount_wei=Web3.to_wei(0.1, "ether"), # 0.1 xDAI
405
+ )
406
+ if not tx_hash:
407
+ logger.error("Failed to fund agent account")
408
+ return None
409
+ logger.info(f"Funded agent account: {tx_hash}")
410
+ return agent_account_address
411
+ except ValueError:
412
+ # Handle case where account already exists
413
+ agent_account = self.wallet.key_storage.get_account(agent_tag)
414
+ agent_account_address = agent_account.address
415
+ logger.info(f"Using existing agent account: {agent_account_address}")
416
+ return agent_account_address
417
+
418
+ def _ensure_agent_token_approval(
419
+ self, agent_account_address: str, bond_amount_wei: Optional[Wei]
420
+ ) -> bool:
421
+ """Ensure token approval for agent registration if needed."""
422
+ service_id = self.service.service_id
423
+ token_address = self._get_service_token(service_id)
424
+ is_native = str(token_address) == str(ZERO_ADDRESS)
425
+
426
+ if is_native:
427
+ return True
428
+
429
+ if not bond_amount_wei:
430
+ logger.warning("No bond amount provided for token bonding. Agent might fail to bond.")
431
+ # We don't return False here, similar to original logic, just warn.
432
+ # But approval will fail if we try to approve None.
433
+ return True
434
+
435
+ # 1. Service Owner Approves Token Utility (for Bond)
436
+ # The service owner (operator) pays the bond, not the agent.
437
+ logger.info(f"Service Owner approving Token Utility for bond: {bond_amount_wei} wei")
438
+
439
+ utility_address = str(
440
+ OLAS_CONTRACTS[self.chain_name]["OLAS_SERVICE_REGISTRY_TOKEN_UTILITY"]
441
+ )
442
+
443
+ approve_success = self.wallet.transfer_service.approve_erc20(
444
+ token_address_or_name=token_address,
445
+ spender_address_or_tag=utility_address,
446
+ amount_wei=bond_amount_wei,
447
+ owner_address_or_tag=agent_account_address,
448
+ chain_name=self.chain_name,
449
+ )
450
+ if not approve_success:
451
+ logger.error("Failed to approve token for agent registration")
452
+ return False
453
+ return True
454
+
455
+ def _send_register_agent_transaction(self, agent_account_address: str) -> bool:
456
+ """Send the register agent transaction."""
457
+ service_id = self.service.service_id
458
+ service_info = self.registry.get_service(service_id)
459
+ security_deposit = service_info["security_deposit"]
460
+
461
+ register_tx = self.manager.prepare_register_agents_tx(
462
+ from_address=self.wallet.master_account.address,
463
+ service_id=service_id,
464
+ agent_instances=[agent_account_address],
465
+ agent_ids=self.service.agent_ids,
466
+ value=(security_deposit * len(self.service.agent_ids)),
467
+ )
468
+ success, receipt = self.wallet.sign_and_send_transaction(
469
+ transaction=register_tx,
470
+ signer_address_or_tag=self.wallet.master_account.address,
471
+ chain_name=self.chain_name,
472
+ tags=["olas_register_agent"],
473
+ )
474
+
475
+ if not success:
476
+ logger.error("Failed to register agent")
477
+ return False
478
+
479
+ logger.info("Agent registration transaction sent successfully")
480
+
481
+ events = self.registry.extract_events(receipt)
482
+
483
+ if "RegisterInstance" not in [event["name"] for event in events]:
484
+ logger.error("Agent registration event not found")
485
+ return False
486
+
487
+ self.service.agent_address = EthereumAddress(agent_account_address)
488
+ self._update_and_save_service_state()
489
+ return True
490
+
491
+ def deploy(self) -> Optional[str]:
492
+ """Deploy the service."""
493
+ # Check that the service has finished registration
494
+ service_state = self.registry.get_service(self.service.service_id)["state"]
495
+ if service_state != ServiceState.FINISHED_REGISTRATION:
496
+ logger.error("Service registration is not finished, cannot deploy")
497
+ return False
498
+
499
+ deploy_tx = self.manager.prepare_deploy_tx(
500
+ from_address=self.service.service_owner_address,
501
+ service_id=self.service.service_id,
502
+ )
503
+ success, receipt = self.wallet.sign_and_send_transaction(
504
+ transaction=deploy_tx,
505
+ signer_address_or_tag=self.service.service_owner_address,
506
+ chain_name=self.chain_name,
507
+ tags=["olas_deploy_service"],
508
+ )
509
+
510
+ if not success:
511
+ logger.error("Failed to deploy service")
512
+ return None
513
+
514
+ logger.info("Service deployment transaction sent successfully")
515
+
516
+ events = self.registry.extract_events(receipt)
517
+
518
+ if "DeployService" not in [event["name"] for event in events]:
519
+ logger.error("Deploy service event not found")
520
+ return None
521
+
522
+ multisig_address = None
523
+
524
+ for event in events:
525
+ if event["name"] == "CreateMultisigWithAgents":
526
+ multisig_address = event["args"]["multisig"]
527
+ logger.info(f"Service deployed with multisig address: {multisig_address}")
528
+ break
529
+
530
+ if multisig_address is None:
531
+ logger.error("Multisig address not found in deployment events")
532
+ return None
533
+
534
+ self.service.multisig_address = EthereumAddress(multisig_address)
535
+ self._update_and_save_service_state()
536
+
537
+ # Register multisig in wallet KeyStorage
538
+ try:
539
+ from iwa.core.models import StoredSafeAccount
540
+
541
+ _, agent_instances = self.registry.call("getAgentInstances", self.service.service_id)
542
+ service_info = self.registry.get_service(self.service.service_id)
543
+ threshold = service_info["threshold"]
544
+
545
+ safe_account = StoredSafeAccount(
546
+ tag=f"{self.service.service_name}_multisig",
547
+ address=multisig_address,
548
+ chains=[self.chain_name],
549
+ threshold=threshold,
550
+ signers=agent_instances,
551
+ )
552
+ self.wallet.key_storage.accounts[multisig_address] = safe_account
553
+ self.wallet.key_storage.save()
554
+ logger.info(f"Registered multisig {multisig_address} in wallet")
555
+ except Exception as e:
556
+ logger.warning(f"Failed to register multisig in wallet: {e}")
557
+
558
+ logger.info("Service deployed successfully")
559
+ return multisig_address
560
+
561
+ def terminate(self) -> bool:
562
+ """Terminate the service."""
563
+ # Check that the service is deployed
564
+ service_state = self.registry.get_service(self.service.service_id)["state"]
565
+ if service_state != ServiceState.DEPLOYED:
566
+ logger.error("Service is not deployed, cannot terminate")
567
+ return False
568
+
569
+ # Check that the service is not staked
570
+ if self.service.staking_contract_address:
571
+ logger.error("Service is staked, cannot terminate")
572
+ return False
573
+
574
+ logger.info(f"[SM-TERM] Preparing Terminate TX. Service ID: {self.service.service_id}")
575
+ logger.info(f"[SM-TERM] Manager Contract Address: {self.manager.address}")
576
+
577
+ terminate_tx = self.manager.prepare_terminate_tx(
578
+ from_address=self.service.service_owner_address,
579
+ service_id=self.service.service_id,
580
+ )
581
+ logger.info(f"[SM-TERM] Terminate TX Prepared. To: {terminate_tx.get('to')}")
582
+
583
+ success, receipt = self.wallet.sign_and_send_transaction(
584
+ transaction=terminate_tx,
585
+ signer_address_or_tag=self.service.service_owner_address,
586
+ chain_name=self.chain_name,
587
+ tags=["olas_terminate_service"],
588
+ )
589
+
590
+ if not success:
591
+ logger.error("Failed to terminate service")
592
+ return False
593
+
594
+ logger.info("Service terminate transaction sent successfully")
595
+
596
+ events = self.registry.extract_events(receipt)
597
+
598
+ if "TerminateService" not in [event["name"] for event in events]:
599
+ logger.error("Terminate service event not found")
600
+ return False
601
+
602
+ logger.info("Service terminated successfully")
603
+ return True
604
+
605
+ def unbond(self) -> bool:
606
+ """Unbond the service."""
607
+ # Check that the service is terminated
608
+ service_state = self.registry.get_service(self.service.service_id)["state"]
609
+ if service_state != ServiceState.TERMINATED_BONDED:
610
+ logger.error("Service is not terminated, cannot unbond")
611
+ return False
612
+
613
+ unbond_tx = self.manager.prepare_unbond_tx(
614
+ from_address=self.service.service_owner_address,
615
+ service_id=self.service.service_id,
616
+ )
617
+
618
+ success, receipt = self.wallet.sign_and_send_transaction(
619
+ transaction=unbond_tx,
620
+ signer_address_or_tag=self.service.service_owner_address,
621
+ chain_name=self.chain_name,
622
+ tags=["olas_unbond_service"],
623
+ )
624
+
625
+ if not success:
626
+ logger.error("Failed to unbond service")
627
+ return False
628
+
629
+ logger.info("Service unbond transaction sent successfully")
630
+
631
+ events = self.registry.extract_events(receipt)
632
+
633
+ if "OperatorUnbond" not in [event["name"] for event in events]:
634
+ logger.error("Unbond service event not found")
635
+ return False
636
+
637
+ logger.info("Service unbonded successfully")
638
+ return True
639
+
640
+ def spin_up(
641
+ self,
642
+ service_id: Optional[int] = None,
643
+ agent_address: Optional[str] = None,
644
+ staking_contract=None,
645
+ bond_amount_wei: Optional[Wei] = None,
646
+ ) -> bool:
647
+ """Spin up a service from PRE_REGISTRATION to DEPLOYED state.
648
+
649
+ Performs sequential state transitions with event verification:
650
+ 1. activate_registration() - if in PRE_REGISTRATION
651
+ 2. register_agent() - if in ACTIVE_REGISTRATION
652
+ 3. deploy() - if in FINISHED_REGISTRATION
653
+ 4. stake() - if staking_contract provided and service is DEPLOYED
654
+
655
+ Each step verifies the state transition succeeded before proceeding.
656
+ The method is idempotent - if already in a later state, it skips completed steps.
657
+
658
+ Args:
659
+ service_id: Optional service ID to spin up. If None, uses active service.
660
+ agent_address: Optional pre-existing agent address to use for registration.
661
+ staking_contract: Optional staking contract to stake after deployment.
662
+ bond_amount_wei: Optional bond amount for agent registration.
663
+
664
+ Returns:
665
+ True if service reached DEPLOYED (and staked if requested), False otherwise.
666
+
667
+ """
668
+ if not service_id:
669
+ if not self.service:
670
+ logger.error("No active service and no service_id provided")
671
+ return False
672
+ service_id = self.service.service_id
673
+ logger.info(f"Spinning up service {service_id}")
674
+
675
+ current_state = self._get_service_state_safe(service_id)
676
+ if not current_state:
677
+ return False
678
+
679
+ logger.info(f"Service {service_id} initial state: {current_state.name}")
680
+
681
+ while current_state != ServiceState.DEPLOYED:
682
+ previous_state = current_state
683
+
684
+ if not self._process_spin_up_state(current_state, agent_address, bond_amount_wei):
685
+ return False
686
+
687
+ # Refresh state
688
+ current_state = self._get_service_state_safe(service_id)
689
+ if not current_state:
690
+ return False
691
+
692
+ if current_state == previous_state:
693
+ logger.error(f"State stuck at {current_state.name} after action")
694
+ return False
695
+
696
+ logger.info(f"Service deployed successfully (State: {current_state.name})")
697
+
698
+ # Stake if requested
699
+ if staking_contract:
700
+ logger.info("Staking service...")
701
+ if not self.stake(staking_contract):
702
+ logger.error("Failed to stake service")
703
+ # Note: Service is DEPLOYED even if stake fails. Return False or True?
704
+ # Original logic returned False.
705
+ return False
706
+ logger.info("Service staked successfully")
707
+
708
+ return True
709
+
710
+ def _process_spin_up_state(
711
+ self,
712
+ current_state: ServiceState,
713
+ agent_address: Optional[str],
714
+ bond_amount_wei: Optional[Wei],
715
+ ) -> bool:
716
+ """Process a single state transition for spin up."""
717
+ if current_state == ServiceState.PRE_REGISTRATION:
718
+ logger.info("Activating registration...")
719
+ if not self.activate_registration():
720
+ logger.error("Failed to activate registration")
721
+ return False
722
+ elif current_state == ServiceState.ACTIVE_REGISTRATION:
723
+ logger.info("Registering agent...")
724
+ if not self.register_agent(
725
+ agent_address=agent_address, bond_amount_wei=bond_amount_wei
726
+ ):
727
+ logger.error("Failed to register agent")
728
+ return False
729
+ elif current_state == ServiceState.FINISHED_REGISTRATION:
730
+ logger.info("Deploying service...")
731
+ if not self.deploy():
732
+ logger.error("Failed to deploy service")
733
+ return False
734
+ else:
735
+ logger.error(f"Unknown or invalid state for spin up: {current_state.name}")
736
+ return False
737
+ return True
738
+
739
+ def _get_service_state_safe(self, service_id: int):
740
+ """Get service state safely, logging errors."""
741
+ try:
742
+ return self.registry.get_service(service_id)["state"]
743
+ except Exception as e:
744
+ logger.error(f"Could not get service info for {service_id}: {e}")
745
+ return None
746
+
747
+ def wind_down(self, staking_contract=None) -> bool:
748
+ """Wind down a service to PRE_REGISTRATION state.
749
+
750
+ Performs sequential state transitions with event verification:
751
+ 1. unstake() - if service is staked (requires staking_contract)
752
+ 2. terminate() - if service is DEPLOYED
753
+ 3. unbond() - if service is TERMINATED_BONDED
754
+
755
+ Each step verifies the state transition succeeded before proceeding.
756
+ The method is idempotent - if already in PRE_REGISTRATION, returns True.
757
+
758
+ Args:
759
+ staking_contract: Staking contract instance (required if service is staked).
760
+
761
+ Returns:
762
+ True if service reached PRE_REGISTRATION, False otherwise.
763
+
764
+ """
765
+ if not self.service:
766
+ logger.error("No active service")
767
+ return False
768
+ service_id = self.service.service_id
769
+ logger.info(f"Winding down service {service_id}")
770
+
771
+ current_state = self._get_service_state_safe(service_id)
772
+ if not current_state:
773
+ return False
774
+
775
+ logger.info(f"Current service state: {current_state.name}")
776
+
777
+ if current_state == ServiceState.NON_EXISTENT:
778
+ logger.error(f"Service {service_id} does not exist, cannot wind down")
779
+ return False
780
+
781
+ # Step 1: Unstake if staked (Special case as it doesn't change the main service state)
782
+ if not self._ensure_unstaked(service_id, current_state, staking_contract):
783
+ return False
784
+
785
+ # Step 2 & 3: Terminate and Unbond loop
786
+ while current_state != ServiceState.PRE_REGISTRATION:
787
+ previous_state = current_state
788
+
789
+ if not self._process_wind_down_state(current_state):
790
+ return False
791
+
792
+ # Refresh state
793
+ current_state = self._get_service_state_safe(service_id)
794
+ if not current_state:
795
+ return False
796
+
797
+ if current_state == previous_state:
798
+ logger.error(f"State stuck at {current_state.name} after action")
799
+ return False
800
+
801
+ logger.info(f"Service {service_id} wind down complete. State: {current_state.name}")
802
+ return True
803
+
804
+ def _process_wind_down_state(self, current_state: ServiceState) -> bool:
805
+ """Process a single state transition for wind down."""
806
+ if current_state == ServiceState.DEPLOYED:
807
+ logger.info("Terminating service...")
808
+ if not self.terminate():
809
+ logger.error("Failed to terminate service")
810
+ return False
811
+ elif current_state == ServiceState.TERMINATED_BONDED:
812
+ logger.info("Unbonding service...")
813
+ if not self.unbond():
814
+ logger.error("Failed to unbond service")
815
+ return False
816
+ else:
817
+ # Should not happen if logic is correct map of transitions
818
+ logger.error(
819
+ f"State {current_state.name} is not a valid start for wind_down (expected DEPLOYED or TERMINATED_BONDED)"
820
+ )
821
+ return False
822
+ return True
823
+
824
+ def _ensure_unstaked(
825
+ self, service_id: int, current_state: ServiceState, staking_contract=None
826
+ ) -> bool:
827
+ """Ensure the service is unstaked if it was staked."""
828
+ if current_state == ServiceState.DEPLOYED and self.service.staking_contract_address:
829
+ if not staking_contract:
830
+ logger.error("Service is staked but no staking contract provided for unstaking")
831
+ return False
832
+
833
+ logger.info("Unstaking service...")
834
+ if not self.unstake(staking_contract):
835
+ logger.error("Failed to unstake service")
836
+ # Return strict False if unstake fails
837
+ return False
838
+ logger.info("Service unstaked successfully")
839
+ return True