iwa 0.0.0__py3-none-any.whl → 0.0.1a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,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
|