iwa 0.0.2__py3-none-any.whl → 0.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iwa/core/chain/interface.py +51 -30
- iwa/core/chain/models.py +9 -15
- iwa/core/contracts/contract.py +8 -2
- iwa/core/pricing.py +10 -8
- iwa/core/services/safe.py +13 -8
- iwa/core/services/transaction.py +211 -7
- iwa/core/utils.py +22 -0
- iwa/core/wallet.py +2 -1
- iwa/plugins/gnosis/safe.py +4 -3
- iwa/plugins/gnosis/tests/test_safe.py +9 -7
- iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +926 -0
- iwa/plugins/olas/contracts/service.py +54 -4
- iwa/plugins/olas/contracts/staking.py +2 -3
- iwa/plugins/olas/plugin.py +14 -7
- iwa/plugins/olas/service_manager/lifecycle.py +382 -85
- iwa/plugins/olas/service_manager/mech.py +1 -1
- iwa/plugins/olas/service_manager/staking.py +229 -82
- iwa/plugins/olas/tests/test_olas_contracts.py +6 -2
- iwa/plugins/olas/tests/test_plugin.py +6 -1
- iwa/plugins/olas/tests/test_plugin_full.py +12 -7
- iwa/plugins/olas/tests/test_service_lifecycle.py +1 -4
- iwa/plugins/olas/tests/test_service_manager.py +59 -89
- iwa/plugins/olas/tests/test_service_manager_errors.py +1 -2
- iwa/plugins/olas/tests/test_service_manager_flows.py +5 -15
- iwa/plugins/olas/tests/test_service_manager_validation.py +16 -15
- iwa/tools/list_contracts.py +2 -2
- iwa/web/dependencies.py +1 -3
- iwa/web/routers/accounts.py +1 -2
- iwa/web/routers/olas/admin.py +1 -3
- iwa/web/routers/olas/funding.py +1 -3
- iwa/web/routers/olas/general.py +1 -3
- iwa/web/routers/olas/services.py +53 -21
- iwa/web/routers/olas/staking.py +27 -24
- iwa/web/routers/swap.py +1 -2
- iwa/web/routers/transactions.py +0 -2
- iwa/web/server.py +8 -6
- iwa/web/static/app.js +22 -0
- iwa/web/tests/test_web_endpoints.py +1 -1
- iwa/web/tests/test_web_olas.py +1 -1
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/METADATA +1 -1
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/RECORD +58 -56
- tests/test_chain.py +12 -7
- tests/test_chain_interface_coverage.py +3 -2
- tests/test_contract.py +165 -0
- tests/test_keys.py +2 -1
- tests/test_legacy_wallet.py +11 -0
- tests/test_pricing.py +32 -15
- tests/test_safe_coverage.py +3 -3
- tests/test_safe_service.py +3 -6
- tests/test_service_transaction.py +8 -3
- tests/test_staking_router.py +6 -3
- tests/test_transaction_service.py +4 -0
- tools/create_and_stake_service.py +103 -0
- tools/verify_drain.py +1 -4
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/WHEEL +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,61 @@
|
|
|
1
|
-
"""Lifecycle manager mixin.
|
|
1
|
+
"""Lifecycle manager mixin for OLAS service lifecycle operations.
|
|
2
|
+
|
|
3
|
+
OLAS Service Lifecycle & Token Flow
|
|
4
|
+
====================================
|
|
5
|
+
|
|
6
|
+
This module handles the first 4 steps of the service lifecycle (staking is in staking.py).
|
|
7
|
+
Each step involves specific token movements as detailed below.
|
|
8
|
+
|
|
9
|
+
STEP 1: CREATE SERVICE
|
|
10
|
+
- What happens:
|
|
11
|
+
* Service is registered on-chain with metadata (config hash, agent IDs)
|
|
12
|
+
* Service ownership NFT (ERC-721) is minted to service owner
|
|
13
|
+
* Bond parameters are recorded but NO tokens move yet
|
|
14
|
+
- Token Movement: None
|
|
15
|
+
- Approval: Service Owner → Token Utility (for 2 × bond_amount OLAS)
|
|
16
|
+
- Next State: PRE_REGISTRATION
|
|
17
|
+
|
|
18
|
+
STEP 2: ACTIVATE REGISTRATION
|
|
19
|
+
- What happens:
|
|
20
|
+
* Service Owner signals readiness to accept agent registrations
|
|
21
|
+
* Token Utility pulls min_staking_deposit OLAS from owner via transferFrom()
|
|
22
|
+
- Token Movement:
|
|
23
|
+
* 5,000 OLAS: Service Owner → Token Utility (for 10k contract)
|
|
24
|
+
- Native value sent: 1 wei (not 5k OLAS!)
|
|
25
|
+
* This is MIN_AGENT_BOND, a placeholder for native-bonded services
|
|
26
|
+
* For OLAS-bonded services, tokens move via Token Utility, not via msg.value
|
|
27
|
+
- Next State: ACTIVE_REGISTRATION
|
|
28
|
+
|
|
29
|
+
STEP 3: REGISTER AGENT
|
|
30
|
+
- What happens:
|
|
31
|
+
* Agent instance address is registered to the service
|
|
32
|
+
* Token Utility pulls agent_bond OLAS from owner via transferFrom()
|
|
33
|
+
- Token Movement:
|
|
34
|
+
* 5,000 OLAS: Service Owner → Token Utility (for 10k contract)
|
|
35
|
+
- Native value sent: 1 wei per agent
|
|
36
|
+
* Same logic as activation - tokens move via Token Utility
|
|
37
|
+
- Next State: FINISHED_REGISTRATION
|
|
38
|
+
|
|
39
|
+
STEP 4: DEPLOY
|
|
40
|
+
- What happens:
|
|
41
|
+
* Safe multisig is created with agent instances as owners
|
|
42
|
+
* Service transitions to operational state
|
|
43
|
+
- Token Movement: None
|
|
44
|
+
- Next State: DEPLOYED
|
|
45
|
+
|
|
46
|
+
After DEPLOYED, see staking.py for STEP 5: STAKE
|
|
47
|
+
|
|
48
|
+
Token Utility Contract:
|
|
49
|
+
The Token Utility (0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8 on Gnosis) is the
|
|
50
|
+
intermediary that holds OLAS deposits. When you call activateRegistration() or
|
|
51
|
+
registerAgents() on the Service Manager, it internally calls Token Utility's
|
|
52
|
+
transferFrom() to move OLAS from the service owner.
|
|
53
|
+
|
|
54
|
+
This is why:
|
|
55
|
+
1. We approve Token Utility BEFORE activation/registration
|
|
56
|
+
2. We send 1 wei native value (not the OLAS amount) in TX
|
|
57
|
+
3. The actual OLAS moves via transferFrom(), not msg.value
|
|
58
|
+
"""
|
|
2
59
|
|
|
3
60
|
from typing import List, Optional, Union
|
|
4
61
|
|
|
@@ -9,6 +66,7 @@ from web3.types import Wei
|
|
|
9
66
|
from iwa.core.chain import ChainInterfaces
|
|
10
67
|
from iwa.core.constants import NATIVE_CURRENCY_ADDRESS, ZERO_ADDRESS
|
|
11
68
|
from iwa.core.types import EthereumAddress
|
|
69
|
+
from iwa.core.utils import get_tx_hash
|
|
12
70
|
from iwa.plugins.olas.constants import (
|
|
13
71
|
OLAS_CONTRACTS,
|
|
14
72
|
TRADER_CONFIG_HASH,
|
|
@@ -19,7 +77,26 @@ from iwa.plugins.olas.models import Service
|
|
|
19
77
|
|
|
20
78
|
|
|
21
79
|
class LifecycleManagerMixin:
|
|
22
|
-
"""Mixin for service lifecycle operations.
|
|
80
|
+
"""Mixin for OLAS service lifecycle operations.
|
|
81
|
+
|
|
82
|
+
Handles the CREATE → ACTIVATE → REGISTER → DEPLOY flow for OLAS services.
|
|
83
|
+
Each method transitions the service to the next state.
|
|
84
|
+
|
|
85
|
+
Token Movement Summary:
|
|
86
|
+
┌───────────────┬────────────────────────────────────────────────────┐
|
|
87
|
+
│ Step │ OLAS Movement │
|
|
88
|
+
├───────────────┼────────────────────────────────────────────────────┤
|
|
89
|
+
│ create() │ None (just approval to Token Utility) │
|
|
90
|
+
│ activate() │ 5k OLAS → Token Utility (via transferFrom) │
|
|
91
|
+
│ register() │ 5k OLAS → Token Utility (via transferFrom) │
|
|
92
|
+
│ deploy() │ None (just creates Safe multisig) │
|
|
93
|
+
└───────────────┴────────────────────────────────────────────────────┘
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
manager = ServiceManager(wallet)
|
|
97
|
+
service_id = manager.create(chain_name="gnosis", token_address_or_tag="OLAS")
|
|
98
|
+
manager.spin_up(bond_amount_wei=5000e18, staking_contract=staking)
|
|
99
|
+
"""
|
|
23
100
|
|
|
24
101
|
def create(
|
|
25
102
|
self,
|
|
@@ -191,7 +268,22 @@ class LifecycleManagerMixin:
|
|
|
191
268
|
service_owner_account,
|
|
192
269
|
bond_amount_wei: Wei,
|
|
193
270
|
) -> None:
|
|
194
|
-
"""Approve
|
|
271
|
+
"""Approve Token Utility to spend OLAS tokens (called during create).
|
|
272
|
+
|
|
273
|
+
Why 2× bond amount?
|
|
274
|
+
- Activation requires min_staking_deposit (= bond_amount)
|
|
275
|
+
- Registration requires agent_bond (= bond_amount)
|
|
276
|
+
- Total = 2 × bond_amount
|
|
277
|
+
|
|
278
|
+
Token Movement: None (this is just an approval, not a transfer)
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
token_address: OLAS token address (or None for native).
|
|
282
|
+
chain_name: Chain to operate on.
|
|
283
|
+
service_owner_account: Account that owns the OLAS tokens.
|
|
284
|
+
bond_amount_wei: Bond amount per agent in wei.
|
|
285
|
+
|
|
286
|
+
"""
|
|
195
287
|
if not token_address:
|
|
196
288
|
return
|
|
197
289
|
|
|
@@ -203,7 +295,7 @@ class LifecycleManagerMixin:
|
|
|
203
295
|
logger.error(f"OLAS Service Registry Token Utility not found for chain: {chain_name}")
|
|
204
296
|
return
|
|
205
297
|
|
|
206
|
-
# Approve the token utility to move tokens (2 * bond amount
|
|
298
|
+
# Approve the token utility to move tokens (2 * bond amount: activation + registration)
|
|
207
299
|
logger.info(f"Approving Token Utility {utility_address} for {2 * bond_amount_wei} tokens")
|
|
208
300
|
approve_success = self.transfer_service.approve_erc20(
|
|
209
301
|
owner_address_or_tag=service_owner_account.address,
|
|
@@ -217,28 +309,59 @@ class LifecycleManagerMixin:
|
|
|
217
309
|
logger.error("Failed to approve Token Utility")
|
|
218
310
|
|
|
219
311
|
def activate_registration(self) -> bool:
|
|
220
|
-
"""Activate registration for the service.
|
|
312
|
+
"""Activate registration for the service (Step 2 of lifecycle).
|
|
313
|
+
|
|
314
|
+
What This Does:
|
|
315
|
+
Transitions service from PRE_REGISTRATION → ACTIVE_REGISTRATION.
|
|
316
|
+
Signals that the service owner is ready to accept agent registrations.
|
|
317
|
+
|
|
318
|
+
Token Movement:
|
|
319
|
+
5,000 OLAS (for 10k contract): Service Owner → Token Utility
|
|
320
|
+
- Moved internally by Token Utility via transferFrom()
|
|
321
|
+
- NOT sent as msg.value (that's just 1 wei)
|
|
322
|
+
|
|
323
|
+
Native Value Sent:
|
|
324
|
+
1 wei (MIN_AGENT_BOND placeholder, not the actual deposit)
|
|
325
|
+
|
|
326
|
+
Prerequisites:
|
|
327
|
+
- Service must be in PRE_REGISTRATION state
|
|
328
|
+
- Token Utility must be approved to spend owner's OLAS
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
True if activation succeeded, False otherwise.
|
|
332
|
+
|
|
333
|
+
"""
|
|
221
334
|
service_id = self.service.service_id
|
|
335
|
+
logger.info(f"[ACTIVATE] Starting activation for service {service_id}")
|
|
336
|
+
|
|
222
337
|
if not self._validate_pre_registration_state(service_id):
|
|
223
338
|
return False
|
|
224
339
|
|
|
225
340
|
token_address = self._get_service_token(service_id)
|
|
341
|
+
logger.debug(f"[ACTIVATE] Token address: {token_address}")
|
|
342
|
+
|
|
226
343
|
service_info = self.registry.get_service(service_id)
|
|
227
344
|
security_deposit = service_info["security_deposit"]
|
|
345
|
+
logger.info(f"[ACTIVATE] Security deposit required: {security_deposit} wei")
|
|
228
346
|
|
|
229
347
|
if not self._ensure_token_approval_for_activation(token_address, security_deposit):
|
|
348
|
+
logger.error("[ACTIVATE] Token approval failed")
|
|
230
349
|
return False
|
|
231
350
|
|
|
351
|
+
logger.info("[ACTIVATE] Sending activation transaction...")
|
|
232
352
|
return self._send_activation_transaction(service_id, security_deposit)
|
|
233
353
|
|
|
234
354
|
def _validate_pre_registration_state(self, service_id: int) -> bool:
|
|
235
355
|
"""Check if service is in PRE_REGISTRATION state."""
|
|
236
|
-
# Check that the service is created
|
|
237
356
|
service_info = self.registry.get_service(service_id)
|
|
238
357
|
service_state = service_info["state"]
|
|
358
|
+
logger.debug(f"[ACTIVATE] Current state: {service_state.name}")
|
|
239
359
|
if service_state != ServiceState.PRE_REGISTRATION:
|
|
240
|
-
logger.error(
|
|
360
|
+
logger.error(
|
|
361
|
+
f"[ACTIVATE] Service is in {service_state.name}, expected PRE_REGISTRATION"
|
|
362
|
+
)
|
|
241
363
|
return False
|
|
364
|
+
logger.debug("[ACTIVATE] State validated: PRE_REGISTRATION")
|
|
242
365
|
return True
|
|
243
366
|
|
|
244
367
|
def _get_service_token(self, service_id: int) -> str:
|
|
@@ -255,29 +378,50 @@ class LifecycleManagerMixin:
|
|
|
255
378
|
def _ensure_token_approval_for_activation(
|
|
256
379
|
self, token_address: str, security_deposit: Wei
|
|
257
380
|
) -> bool:
|
|
258
|
-
"""Ensure token approval for activation if not native token.
|
|
381
|
+
"""Ensure token approval for activation if not native token.
|
|
382
|
+
|
|
383
|
+
For token-bonded services (e.g., OLAS), we need to approve the
|
|
384
|
+
ServiceRegistryTokenUtility contract to spend the security deposit
|
|
385
|
+
(agent bond) on our behalf.
|
|
386
|
+
|
|
387
|
+
IMPORTANT: We query the exact bond amount from the Token Utility contract
|
|
388
|
+
rather than approving a fixed amount, to match the official OLAS middleware.
|
|
389
|
+
"""
|
|
259
390
|
is_native = str(token_address).lower() == str(ZERO_ADDRESS).lower()
|
|
260
391
|
if is_native:
|
|
261
392
|
return True
|
|
262
393
|
|
|
263
394
|
try:
|
|
264
|
-
#
|
|
395
|
+
# Get the exact agent bond from Token Utility contract
|
|
396
|
+
bond_amount = self._get_agent_bond_from_token_utility()
|
|
397
|
+
if bond_amount is None or bond_amount == 0:
|
|
398
|
+
logger.warning(
|
|
399
|
+
"[ACTIVATE] Could not get agent bond from Token Utility, using security_deposit"
|
|
400
|
+
)
|
|
401
|
+
bond_amount = security_deposit
|
|
402
|
+
|
|
403
|
+
logger.info(f"[ACTIVATE] Agent bond from Token Utility: {bond_amount} wei")
|
|
404
|
+
|
|
405
|
+
# Check owner balance
|
|
265
406
|
balance = self.wallet.balance_service.get_erc20_balance_wei(
|
|
266
407
|
account_address_or_tag=self.service.service_owner_address,
|
|
267
408
|
token_address_or_name=token_address,
|
|
268
409
|
chain_name=self.chain_name,
|
|
269
410
|
)
|
|
270
411
|
|
|
271
|
-
if balance <
|
|
412
|
+
if balance < bond_amount:
|
|
272
413
|
logger.error(
|
|
273
|
-
f"[ACTIVATE] FAIL: Owner balance {balance} < required {
|
|
414
|
+
f"[ACTIVATE] FAIL: Owner balance {balance} < required {bond_amount}"
|
|
274
415
|
)
|
|
416
|
+
return False
|
|
275
417
|
|
|
276
418
|
protocol_contracts = OLAS_CONTRACTS.get(self.chain_name.lower(), {})
|
|
277
419
|
utility_address = protocol_contracts.get("OLAS_SERVICE_REGISTRY_TOKEN_UTILITY")
|
|
278
420
|
|
|
279
421
|
if utility_address:
|
|
280
|
-
|
|
422
|
+
# Approve exactly the bond amount (not 1000 OLAS fixed!)
|
|
423
|
+
# This matches the official OLAS middleware behavior
|
|
424
|
+
required_approval = bond_amount
|
|
281
425
|
|
|
282
426
|
# Check current allowance
|
|
283
427
|
allowance = self.wallet.transfer_service.get_erc20_allowance(
|
|
@@ -287,9 +431,10 @@ class LifecycleManagerMixin:
|
|
|
287
431
|
chain_name=self.chain_name,
|
|
288
432
|
)
|
|
289
433
|
|
|
290
|
-
if allowance <
|
|
434
|
+
if allowance < required_approval:
|
|
291
435
|
logger.info(
|
|
292
|
-
f"
|
|
436
|
+
f"[ACTIVATE] Allowance ({allowance}) < required ({required_approval}). "
|
|
437
|
+
f"Approving Token Utility {utility_address}"
|
|
293
438
|
)
|
|
294
439
|
success_approve = self.wallet.transfer_service.approve_erc20(
|
|
295
440
|
owner_address_or_tag=self.service.service_owner_address,
|
|
@@ -299,33 +444,90 @@ class LifecycleManagerMixin:
|
|
|
299
444
|
chain_name=self.chain_name,
|
|
300
445
|
)
|
|
301
446
|
if not success_approve:
|
|
302
|
-
logger.
|
|
447
|
+
logger.error("[ACTIVATE] Token approval failed")
|
|
303
448
|
return False
|
|
449
|
+
logger.info(f"[ACTIVATE] Approved {required_approval} wei to Token Utility")
|
|
450
|
+
else:
|
|
451
|
+
logger.debug(
|
|
452
|
+
f"[ACTIVATE] Sufficient allowance ({allowance} >= {required_approval})"
|
|
453
|
+
)
|
|
304
454
|
return True
|
|
305
455
|
except Exception as e:
|
|
306
|
-
logger.
|
|
307
|
-
return False
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
456
|
+
logger.error(f"[ACTIVATE] Failed to check/approve tokens: {e}")
|
|
457
|
+
return False
|
|
458
|
+
|
|
459
|
+
def _get_agent_bond_from_token_utility(self) -> Optional[int]:
|
|
460
|
+
"""Get the agent bond from the ServiceRegistryTokenUtility contract.
|
|
461
|
+
|
|
462
|
+
This queries the on-chain Token Utility contract to get the exact bond
|
|
463
|
+
amount required for the service, matching the official OLAS middleware.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
The agent bond in wei, or None if the query fails.
|
|
467
|
+
|
|
468
|
+
"""
|
|
469
|
+
from iwa.plugins.olas.contracts.service import ServiceRegistryTokenUtilityContract
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
protocol_contracts = OLAS_CONTRACTS.get(self.chain_name.lower(), {})
|
|
473
|
+
utility_address = protocol_contracts.get("OLAS_SERVICE_REGISTRY_TOKEN_UTILITY")
|
|
474
|
+
|
|
475
|
+
if not utility_address:
|
|
476
|
+
logger.warning("[ACTIVATE] Token Utility address not found for chain")
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
# Get agent ID (first agent in the service)
|
|
480
|
+
service_info = self.registry.get_service(self.service.service_id)
|
|
481
|
+
agent_ids = service_info.get("agent_ids", [])
|
|
482
|
+
if not agent_ids:
|
|
483
|
+
logger.warning("[ACTIVATE] No agent IDs found for service")
|
|
484
|
+
return None
|
|
485
|
+
agent_id = agent_ids[0]
|
|
486
|
+
|
|
487
|
+
# Use the ServiceRegistryTokenUtilityContract with official ABI
|
|
488
|
+
token_utility = ServiceRegistryTokenUtilityContract(
|
|
489
|
+
address=str(utility_address),
|
|
490
|
+
chain_name=self.chain_name,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
bond = token_utility.get_agent_bond(self.service.service_id, agent_id)
|
|
494
|
+
|
|
495
|
+
logger.debug(
|
|
496
|
+
f"[ACTIVATE] Token Utility getAgentBond({self.service.service_id}, {agent_id}) = {bond}"
|
|
497
|
+
)
|
|
498
|
+
return bond
|
|
499
|
+
|
|
500
|
+
except Exception as e:
|
|
501
|
+
logger.warning(f"[ACTIVATE] Failed to get agent bond from Token Utility: {e}")
|
|
502
|
+
return None
|
|
315
503
|
|
|
316
504
|
def _send_activation_transaction(self, service_id: int, security_deposit: Wei) -> bool:
|
|
317
|
-
"""Send the activation transaction.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
#
|
|
505
|
+
"""Send the activation transaction.
|
|
506
|
+
|
|
507
|
+
For token-bonded services (e.g., OLAS), we pass MIN_AGENT_BOND (1 wei) as native value.
|
|
508
|
+
The Token Utility handles OLAS transfers internally based on the service configuration.
|
|
509
|
+
For native currency services, we pass the full security_deposit.
|
|
510
|
+
"""
|
|
511
|
+
# Determine if this is a token-bonded service
|
|
512
|
+
token_address = self._get_service_token(service_id)
|
|
513
|
+
is_native = str(token_address).lower() == str(ZERO_ADDRESS).lower()
|
|
514
|
+
|
|
515
|
+
# For token services, use MIN_AGENT_BOND (1 wei) as native value
|
|
516
|
+
# The OLAS token approval was done in _ensure_token_approval_for_activation
|
|
517
|
+
activation_value = security_deposit if is_native else 1
|
|
518
|
+
logger.info(
|
|
519
|
+
f"[ACTIVATE] Token={token_address}, is_native={is_native}, "
|
|
520
|
+
f"activation_value={activation_value} wei"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
logger.debug(f"[ACTIVATE] Preparing tx: service_id={service_id}, value={activation_value}")
|
|
324
524
|
activate_tx = self.manager.prepare_activate_registration_tx(
|
|
325
525
|
from_address=self.wallet.master_account.address,
|
|
326
526
|
service_id=service_id,
|
|
327
|
-
value=
|
|
527
|
+
value=activation_value,
|
|
328
528
|
)
|
|
529
|
+
logger.debug(f"[ACTIVATE] TX prepared: to={activate_tx.get('to')}")
|
|
530
|
+
|
|
329
531
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
330
532
|
transaction=activate_tx,
|
|
331
533
|
signer_address_or_tag=self.wallet.master_account.address,
|
|
@@ -333,17 +535,21 @@ class LifecycleManagerMixin:
|
|
|
333
535
|
)
|
|
334
536
|
|
|
335
537
|
if not success:
|
|
336
|
-
logger.error("
|
|
538
|
+
logger.error("[ACTIVATE] Transaction failed")
|
|
337
539
|
return False
|
|
338
540
|
|
|
339
|
-
|
|
541
|
+
tx_hash = get_tx_hash(receipt)
|
|
542
|
+
logger.info(f"[ACTIVATE] TX sent: {tx_hash}")
|
|
340
543
|
|
|
341
544
|
events = self.registry.extract_events(receipt)
|
|
545
|
+
event_names = [e["name"] for e in events]
|
|
546
|
+
logger.debug(f"[ACTIVATE] Events: {event_names}")
|
|
342
547
|
|
|
343
|
-
if "ActivateRegistration" not in
|
|
344
|
-
logger.error("
|
|
548
|
+
if "ActivateRegistration" not in event_names:
|
|
549
|
+
logger.error("[ACTIVATE] ActivateRegistration event not found")
|
|
345
550
|
return False
|
|
346
551
|
|
|
552
|
+
logger.info("[ACTIVATE] Success - service is now ACTIVE_REGISTRATION")
|
|
347
553
|
return True
|
|
348
554
|
|
|
349
555
|
def register_agent(
|
|
@@ -360,24 +566,35 @@ class LifecycleManagerMixin:
|
|
|
360
566
|
True if registration succeeded, False otherwise.
|
|
361
567
|
|
|
362
568
|
"""
|
|
569
|
+
logger.info(f"[REGISTER] Starting agent registration for service {self.service.service_id}")
|
|
570
|
+
logger.debug(f"[REGISTER] agent_address={agent_address}, bond={bond_amount_wei}")
|
|
571
|
+
|
|
363
572
|
if not self._validate_active_registration_state():
|
|
364
573
|
return False
|
|
365
574
|
|
|
366
575
|
agent_account_address = self._get_or_create_agent_account(agent_address)
|
|
367
576
|
if not agent_account_address:
|
|
577
|
+
logger.error("[REGISTER] Failed to get/create agent account")
|
|
368
578
|
return False
|
|
579
|
+
logger.info(f"[REGISTER] Agent address: {agent_account_address}")
|
|
369
580
|
|
|
370
581
|
if not self._ensure_agent_token_approval(agent_account_address, bond_amount_wei):
|
|
582
|
+
logger.error("[REGISTER] Token approval failed")
|
|
371
583
|
return False
|
|
372
584
|
|
|
585
|
+
logger.info("[REGISTER] Sending register agent transaction...")
|
|
373
586
|
return self._send_register_agent_transaction(agent_account_address)
|
|
374
587
|
|
|
375
588
|
def _validate_active_registration_state(self) -> bool:
|
|
376
589
|
"""Check that the service is in active registration."""
|
|
377
590
|
service_state = self.registry.get_service(self.service.service_id)["state"]
|
|
591
|
+
logger.debug(f"[REGISTER] Current state: {service_state.name}")
|
|
378
592
|
if service_state != ServiceState.ACTIVE_REGISTRATION:
|
|
379
|
-
logger.error(
|
|
593
|
+
logger.error(
|
|
594
|
+
f"[REGISTER] Service is in {service_state.name}, expected ACTIVE_REGISTRATION"
|
|
595
|
+
)
|
|
380
596
|
return False
|
|
597
|
+
logger.debug("[REGISTER] State validated: ACTIVE_REGISTRATION")
|
|
381
598
|
return True
|
|
382
599
|
|
|
383
600
|
def _get_or_create_agent_account(self, agent_address: Optional[str]) -> Optional[str]:
|
|
@@ -418,7 +635,12 @@ class LifecycleManagerMixin:
|
|
|
418
635
|
def _ensure_agent_token_approval(
|
|
419
636
|
self, agent_account_address: str, bond_amount_wei: Optional[Wei]
|
|
420
637
|
) -> bool:
|
|
421
|
-
"""Ensure token approval for agent registration if needed.
|
|
638
|
+
"""Ensure token approval for agent registration if needed.
|
|
639
|
+
|
|
640
|
+
For token-bonded services, the service owner must approve the Token Utility
|
|
641
|
+
contract to transfer the agent bond. We query the exact bond from the
|
|
642
|
+
Token Utility contract to match the official OLAS middleware.
|
|
643
|
+
"""
|
|
422
644
|
service_id = self.service.service_id
|
|
423
645
|
token_address = self._get_service_token(service_id)
|
|
424
646
|
is_native = str(token_address) == str(ZERO_ADDRESS)
|
|
@@ -426,45 +648,90 @@ class LifecycleManagerMixin:
|
|
|
426
648
|
if is_native:
|
|
427
649
|
return True
|
|
428
650
|
|
|
651
|
+
# Get exact bond from Token Utility if not explicitly provided
|
|
429
652
|
if not bond_amount_wei:
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
653
|
+
bond_amount_wei = self._get_agent_bond_from_token_utility()
|
|
654
|
+
if not bond_amount_wei:
|
|
655
|
+
logger.warning(
|
|
656
|
+
"[REGISTER] Could not get bond from Token Utility, skipping approval"
|
|
657
|
+
)
|
|
658
|
+
return True
|
|
434
659
|
|
|
435
|
-
|
|
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")
|
|
660
|
+
logger.info(f"[REGISTER] Service Owner approving Token Utility for bond: {bond_amount_wei} wei")
|
|
438
661
|
|
|
439
662
|
utility_address = str(
|
|
440
663
|
OLAS_CONTRACTS[self.chain_name]["OLAS_SERVICE_REGISTRY_TOKEN_UTILITY"]
|
|
441
664
|
)
|
|
442
665
|
|
|
666
|
+
# Check current allowance first
|
|
667
|
+
allowance = self.wallet.transfer_service.get_erc20_allowance(
|
|
668
|
+
owner_address_or_tag=self.service.service_owner_address,
|
|
669
|
+
spender_address=utility_address,
|
|
670
|
+
token_address_or_name=token_address,
|
|
671
|
+
chain_name=self.chain_name,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if allowance >= bond_amount_wei:
|
|
675
|
+
logger.debug(
|
|
676
|
+
f"[REGISTER] Sufficient allowance ({allowance} >= {bond_amount_wei})"
|
|
677
|
+
)
|
|
678
|
+
return True
|
|
679
|
+
|
|
680
|
+
# Use service owner which holds the OLAS tokens (not necessarily master)
|
|
443
681
|
approve_success = self.wallet.transfer_service.approve_erc20(
|
|
444
682
|
token_address_or_name=token_address,
|
|
445
683
|
spender_address_or_tag=utility_address,
|
|
446
684
|
amount_wei=bond_amount_wei,
|
|
447
|
-
owner_address_or_tag=
|
|
685
|
+
owner_address_or_tag=self.service.service_owner_address,
|
|
448
686
|
chain_name=self.chain_name,
|
|
449
687
|
)
|
|
450
688
|
if not approve_success:
|
|
451
|
-
logger.error("Failed to approve token for agent registration")
|
|
689
|
+
logger.error("[REGISTER] Failed to approve token for agent registration")
|
|
452
690
|
return False
|
|
691
|
+
|
|
692
|
+
logger.info(f"[REGISTER] Approved {bond_amount_wei} wei to Token Utility")
|
|
453
693
|
return True
|
|
454
694
|
|
|
455
695
|
def _send_register_agent_transaction(self, agent_account_address: str) -> bool:
|
|
456
|
-
"""Send the register agent transaction.
|
|
696
|
+
"""Send the register agent transaction.
|
|
697
|
+
|
|
698
|
+
For token-bonded services (e.g., OLAS), we pass MIN_AGENT_BOND (1 wei) as native value.
|
|
699
|
+
The Token Utility handles OLAS transfers internally via transferFrom.
|
|
700
|
+
For native currency services, we pass the full security_deposit * num_agents.
|
|
701
|
+
"""
|
|
457
702
|
service_id = self.service.service_id
|
|
458
|
-
|
|
459
|
-
|
|
703
|
+
token_address = self._get_service_token(service_id)
|
|
704
|
+
is_native = str(token_address).lower() == str(ZERO_ADDRESS).lower()
|
|
705
|
+
|
|
706
|
+
# For token services, use MIN_AGENT_BOND (1 wei) per agent
|
|
707
|
+
# For native services, use security_deposit * num_agents
|
|
708
|
+
if is_native:
|
|
709
|
+
service_info = self.registry.get_service(service_id)
|
|
710
|
+
security_deposit = service_info["security_deposit"]
|
|
711
|
+
total_value = security_deposit * len(self.service.agent_ids)
|
|
712
|
+
else:
|
|
713
|
+
# MIN_AGENT_BOND = 1 wei per agent
|
|
714
|
+
total_value = 1 * len(self.service.agent_ids)
|
|
715
|
+
|
|
716
|
+
logger.info(
|
|
717
|
+
f"[REGISTER] Token={token_address}, is_native={is_native}, "
|
|
718
|
+
f"total_value={total_value} wei"
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
logger.debug(
|
|
722
|
+
f"[REGISTER] Preparing tx: agent={agent_account_address}, "
|
|
723
|
+
f"agent_ids={self.service.agent_ids}, value={total_value}"
|
|
724
|
+
)
|
|
460
725
|
|
|
461
726
|
register_tx = self.manager.prepare_register_agents_tx(
|
|
462
727
|
from_address=self.wallet.master_account.address,
|
|
463
728
|
service_id=service_id,
|
|
464
729
|
agent_instances=[agent_account_address],
|
|
465
730
|
agent_ids=self.service.agent_ids,
|
|
466
|
-
value=
|
|
731
|
+
value=total_value,
|
|
467
732
|
)
|
|
733
|
+
logger.debug(f"[REGISTER] TX prepared: to={register_tx.get('to')}")
|
|
734
|
+
|
|
468
735
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
469
736
|
transaction=register_tx,
|
|
470
737
|
signer_address_or_tag=self.wallet.master_account.address,
|
|
@@ -473,33 +740,50 @@ class LifecycleManagerMixin:
|
|
|
473
740
|
)
|
|
474
741
|
|
|
475
742
|
if not success:
|
|
476
|
-
logger.error("
|
|
743
|
+
logger.error("[REGISTER] Transaction failed")
|
|
477
744
|
return False
|
|
478
745
|
|
|
479
|
-
|
|
746
|
+
tx_hash = get_tx_hash(receipt)
|
|
747
|
+
logger.info(f"[REGISTER] TX sent: {tx_hash}")
|
|
480
748
|
|
|
481
749
|
events = self.registry.extract_events(receipt)
|
|
750
|
+
event_names = [e["name"] for e in events]
|
|
751
|
+
logger.debug(f"[REGISTER] Events: {event_names}")
|
|
482
752
|
|
|
483
|
-
if "RegisterInstance" not in
|
|
484
|
-
logger.error("
|
|
753
|
+
if "RegisterInstance" not in event_names:
|
|
754
|
+
logger.error("[REGISTER] RegisterInstance event not found")
|
|
485
755
|
return False
|
|
486
756
|
|
|
487
757
|
self.service.agent_address = EthereumAddress(agent_account_address)
|
|
488
758
|
self._update_and_save_service_state()
|
|
759
|
+
logger.info("[REGISTER] Success - service is now FINISHED_REGISTRATION")
|
|
489
760
|
return True
|
|
490
761
|
|
|
491
|
-
def deploy(self) -> Optional[str]:
|
|
762
|
+
def deploy(self) -> Optional[str]: # noqa: C901
|
|
492
763
|
"""Deploy the service."""
|
|
493
|
-
|
|
764
|
+
logger.info(f"[DEPLOY] Starting deployment for service {self.service.service_id}")
|
|
765
|
+
|
|
494
766
|
service_state = self.registry.get_service(self.service.service_id)["state"]
|
|
767
|
+
logger.debug(f"[DEPLOY] Current state: {service_state.name}")
|
|
768
|
+
|
|
495
769
|
if service_state != ServiceState.FINISHED_REGISTRATION:
|
|
496
|
-
logger.error(
|
|
770
|
+
logger.error(
|
|
771
|
+
f"[DEPLOY] Service is in {service_state.name}, expected FINISHED_REGISTRATION"
|
|
772
|
+
)
|
|
497
773
|
return False
|
|
498
774
|
|
|
775
|
+
logger.debug(f"[DEPLOY] Preparing deploy tx for owner {self.service.service_owner_address}")
|
|
499
776
|
deploy_tx = self.manager.prepare_deploy_tx(
|
|
500
777
|
from_address=self.service.service_owner_address,
|
|
501
778
|
service_id=self.service.service_id,
|
|
502
779
|
)
|
|
780
|
+
|
|
781
|
+
if not deploy_tx:
|
|
782
|
+
logger.error("[DEPLOY] Failed to prepare deploy transaction")
|
|
783
|
+
return None
|
|
784
|
+
|
|
785
|
+
logger.debug(f"[DEPLOY] TX prepared: to={deploy_tx.get('to')}")
|
|
786
|
+
|
|
503
787
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
504
788
|
transaction=deploy_tx,
|
|
505
789
|
signer_address_or_tag=self.service.service_owner_address,
|
|
@@ -508,29 +792,31 @@ class LifecycleManagerMixin:
|
|
|
508
792
|
)
|
|
509
793
|
|
|
510
794
|
if not success:
|
|
511
|
-
logger.error("
|
|
795
|
+
logger.error("[DEPLOY] Transaction failed")
|
|
512
796
|
return None
|
|
513
797
|
|
|
514
|
-
|
|
798
|
+
tx_hash = get_tx_hash(receipt)
|
|
799
|
+
logger.info(f"[DEPLOY] TX sent: {tx_hash}")
|
|
515
800
|
|
|
516
801
|
events = self.registry.extract_events(receipt)
|
|
802
|
+
event_names = [e["name"] for e in events]
|
|
803
|
+
logger.debug(f"[DEPLOY] Events: {event_names}")
|
|
517
804
|
|
|
518
|
-
if "DeployService" not in
|
|
519
|
-
logger.error("
|
|
805
|
+
if "DeployService" not in event_names:
|
|
806
|
+
logger.error("[DEPLOY] DeployService event not found")
|
|
520
807
|
return None
|
|
521
808
|
|
|
522
809
|
multisig_address = None
|
|
523
|
-
|
|
524
810
|
for event in events:
|
|
525
811
|
if event["name"] == "CreateMultisigWithAgents":
|
|
526
812
|
multisig_address = event["args"]["multisig"]
|
|
527
|
-
logger.info(f"Service deployed with multisig address: {multisig_address}")
|
|
528
813
|
break
|
|
529
814
|
|
|
530
815
|
if multisig_address is None:
|
|
531
|
-
logger.error("Multisig address not found in
|
|
816
|
+
logger.error("[DEPLOY] Multisig address not found in events")
|
|
532
817
|
return None
|
|
533
818
|
|
|
819
|
+
logger.info(f"[DEPLOY] Multisig created: {multisig_address}")
|
|
534
820
|
self.service.multisig_address = EthereumAddress(multisig_address)
|
|
535
821
|
self._update_and_save_service_state()
|
|
536
822
|
|
|
@@ -551,11 +837,11 @@ class LifecycleManagerMixin:
|
|
|
551
837
|
)
|
|
552
838
|
self.wallet.key_storage.accounts[multisig_address] = safe_account
|
|
553
839
|
self.wallet.key_storage.save()
|
|
554
|
-
logger.
|
|
840
|
+
logger.debug("[DEPLOY] Registered multisig in wallet")
|
|
555
841
|
except Exception as e:
|
|
556
|
-
logger.warning(f"Failed to register multisig in wallet: {e}")
|
|
842
|
+
logger.warning(f"[DEPLOY] Failed to register multisig in wallet: {e}")
|
|
557
843
|
|
|
558
|
-
logger.info("
|
|
844
|
+
logger.info("[DEPLOY] Success - service is now DEPLOYED")
|
|
559
845
|
return multisig_address
|
|
560
846
|
|
|
561
847
|
def terminate(self) -> bool:
|
|
@@ -667,21 +953,31 @@ class LifecycleManagerMixin:
|
|
|
667
953
|
"""
|
|
668
954
|
if not service_id:
|
|
669
955
|
if not self.service:
|
|
670
|
-
logger.error("No active service and no service_id provided")
|
|
956
|
+
logger.error("[SPIN-UP] No active service and no service_id provided")
|
|
671
957
|
return False
|
|
672
958
|
service_id = self.service.service_id
|
|
673
|
-
|
|
959
|
+
|
|
960
|
+
logger.info("=" * 50)
|
|
961
|
+
logger.info(f"[SPIN-UP] Starting spin_up for service {service_id}")
|
|
962
|
+
logger.info(f"[SPIN-UP] Parameters: agent_address={agent_address}, bond={bond_amount_wei}")
|
|
963
|
+
logger.info(
|
|
964
|
+
f"[SPIN-UP] Staking contract: {staking_contract.address if staking_contract else 'None'}"
|
|
965
|
+
)
|
|
966
|
+
logger.info("=" * 50)
|
|
674
967
|
|
|
675
968
|
current_state = self._get_service_state_safe(service_id)
|
|
676
969
|
if not current_state:
|
|
677
970
|
return False
|
|
678
971
|
|
|
679
|
-
logger.info(f"
|
|
972
|
+
logger.info(f"[SPIN-UP] Initial state: {current_state.name}")
|
|
680
973
|
|
|
974
|
+
step = 1
|
|
681
975
|
while current_state != ServiceState.DEPLOYED:
|
|
682
976
|
previous_state = current_state
|
|
977
|
+
logger.info(f"[SPIN-UP] Step {step}: Processing {current_state.name}...")
|
|
683
978
|
|
|
684
979
|
if not self._process_spin_up_state(current_state, agent_address, bond_amount_wei):
|
|
980
|
+
logger.error(f"[SPIN-UP] Step {step} FAILED at state {current_state.name}")
|
|
685
981
|
return False
|
|
686
982
|
|
|
687
983
|
# Refresh state
|
|
@@ -690,21 +986,25 @@ class LifecycleManagerMixin:
|
|
|
690
986
|
return False
|
|
691
987
|
|
|
692
988
|
if current_state == previous_state:
|
|
693
|
-
logger.error(f"State stuck at {current_state.name} after action")
|
|
989
|
+
logger.error(f"[SPIN-UP] State stuck at {current_state.name} after action")
|
|
694
990
|
return False
|
|
695
991
|
|
|
696
|
-
|
|
992
|
+
logger.info(f"[SPIN-UP] Step {step} OK: {previous_state.name} -> {current_state.name}")
|
|
993
|
+
step += 1
|
|
994
|
+
|
|
995
|
+
logger.info(f"[SPIN-UP] Service {service_id} is now DEPLOYED")
|
|
697
996
|
|
|
698
997
|
# Stake if requested
|
|
699
998
|
if staking_contract:
|
|
700
|
-
logger.info("Staking service...")
|
|
999
|
+
logger.info(f"[SPIN-UP] Step {step}: Staking service...")
|
|
701
1000
|
if not self.stake(staking_contract):
|
|
702
|
-
logger.error("
|
|
703
|
-
# Note: Service is DEPLOYED even if stake fails. Return False or True?
|
|
704
|
-
# Original logic returned False.
|
|
1001
|
+
logger.error("[SPIN-UP] Staking FAILED")
|
|
705
1002
|
return False
|
|
706
|
-
logger.info("Service staked successfully")
|
|
1003
|
+
logger.info(f"[SPIN-UP] Step {step} OK: Service staked successfully")
|
|
707
1004
|
|
|
1005
|
+
logger.info("=" * 50)
|
|
1006
|
+
logger.info(f"[SPIN-UP] COMPLETE - Service {service_id} is deployed and ready")
|
|
1007
|
+
logger.info("=" * 50)
|
|
708
1008
|
return True
|
|
709
1009
|
|
|
710
1010
|
def _process_spin_up_state(
|
|
@@ -715,24 +1015,21 @@ class LifecycleManagerMixin:
|
|
|
715
1015
|
) -> bool:
|
|
716
1016
|
"""Process a single state transition for spin up."""
|
|
717
1017
|
if current_state == ServiceState.PRE_REGISTRATION:
|
|
718
|
-
logger.info("
|
|
1018
|
+
logger.info("[SPIN-UP] Action: activate_registration()")
|
|
719
1019
|
if not self.activate_registration():
|
|
720
|
-
logger.error("Failed to activate registration")
|
|
721
1020
|
return False
|
|
722
1021
|
elif current_state == ServiceState.ACTIVE_REGISTRATION:
|
|
723
|
-
logger.info("
|
|
1022
|
+
logger.info("[SPIN-UP] Action: register_agent()")
|
|
724
1023
|
if not self.register_agent(
|
|
725
1024
|
agent_address=agent_address, bond_amount_wei=bond_amount_wei
|
|
726
1025
|
):
|
|
727
|
-
logger.error("Failed to register agent")
|
|
728
1026
|
return False
|
|
729
1027
|
elif current_state == ServiceState.FINISHED_REGISTRATION:
|
|
730
|
-
logger.info("
|
|
1028
|
+
logger.info("[SPIN-UP] Action: deploy()")
|
|
731
1029
|
if not self.deploy():
|
|
732
|
-
logger.error("Failed to deploy service")
|
|
733
1030
|
return False
|
|
734
1031
|
else:
|
|
735
|
-
logger.error(f"
|
|
1032
|
+
logger.error(f"[SPIN-UP] Invalid state: {current_state.name}")
|
|
736
1033
|
return False
|
|
737
1034
|
return True
|
|
738
1035
|
|