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.
Files changed (58) hide show
  1. iwa/core/chain/interface.py +51 -30
  2. iwa/core/chain/models.py +9 -15
  3. iwa/core/contracts/contract.py +8 -2
  4. iwa/core/pricing.py +10 -8
  5. iwa/core/services/safe.py +13 -8
  6. iwa/core/services/transaction.py +211 -7
  7. iwa/core/utils.py +22 -0
  8. iwa/core/wallet.py +2 -1
  9. iwa/plugins/gnosis/safe.py +4 -3
  10. iwa/plugins/gnosis/tests/test_safe.py +9 -7
  11. iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +926 -0
  12. iwa/plugins/olas/contracts/service.py +54 -4
  13. iwa/plugins/olas/contracts/staking.py +2 -3
  14. iwa/plugins/olas/plugin.py +14 -7
  15. iwa/plugins/olas/service_manager/lifecycle.py +382 -85
  16. iwa/plugins/olas/service_manager/mech.py +1 -1
  17. iwa/plugins/olas/service_manager/staking.py +229 -82
  18. iwa/plugins/olas/tests/test_olas_contracts.py +6 -2
  19. iwa/plugins/olas/tests/test_plugin.py +6 -1
  20. iwa/plugins/olas/tests/test_plugin_full.py +12 -7
  21. iwa/plugins/olas/tests/test_service_lifecycle.py +1 -4
  22. iwa/plugins/olas/tests/test_service_manager.py +59 -89
  23. iwa/plugins/olas/tests/test_service_manager_errors.py +1 -2
  24. iwa/plugins/olas/tests/test_service_manager_flows.py +5 -15
  25. iwa/plugins/olas/tests/test_service_manager_validation.py +16 -15
  26. iwa/tools/list_contracts.py +2 -2
  27. iwa/web/dependencies.py +1 -3
  28. iwa/web/routers/accounts.py +1 -2
  29. iwa/web/routers/olas/admin.py +1 -3
  30. iwa/web/routers/olas/funding.py +1 -3
  31. iwa/web/routers/olas/general.py +1 -3
  32. iwa/web/routers/olas/services.py +53 -21
  33. iwa/web/routers/olas/staking.py +27 -24
  34. iwa/web/routers/swap.py +1 -2
  35. iwa/web/routers/transactions.py +0 -2
  36. iwa/web/server.py +8 -6
  37. iwa/web/static/app.js +22 -0
  38. iwa/web/tests/test_web_endpoints.py +1 -1
  39. iwa/web/tests/test_web_olas.py +1 -1
  40. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/METADATA +1 -1
  41. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/RECORD +58 -56
  42. tests/test_chain.py +12 -7
  43. tests/test_chain_interface_coverage.py +3 -2
  44. tests/test_contract.py +165 -0
  45. tests/test_keys.py +2 -1
  46. tests/test_legacy_wallet.py +11 -0
  47. tests/test_pricing.py +32 -15
  48. tests/test_safe_coverage.py +3 -3
  49. tests/test_safe_service.py +3 -6
  50. tests/test_service_transaction.py +8 -3
  51. tests/test_staking_router.py +6 -3
  52. tests/test_transaction_service.py +4 -0
  53. tools/create_and_stake_service.py +103 -0
  54. tools/verify_drain.py +1 -4
  55. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/WHEEL +0 -0
  56. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/entry_points.txt +0 -0
  57. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/licenses/LICENSE +0 -0
  58. {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 token utility if a token address is provided."""
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 as per Triton reference)
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("Service is not created, cannot activate registration")
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
- # Check Master Balance first
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 < security_deposit:
412
+ if balance < bond_amount:
272
413
  logger.error(
273
- f"[ACTIVATE] FAIL: Owner balance {balance} < required {security_deposit}"
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
- required_approval = Web3.to_wei(1000, "ether") # Approve generous amount to be safe
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 < Web3.to_wei(10, "ether"): # Min threshold check
434
+ if allowance < required_approval:
291
435
  logger.info(
292
- f"Low allowance ({allowance}). Approving Token Utility {utility_address}"
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.warning("Token approval transaction returned failure.")
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.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
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
- # 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.
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=security_deposit,
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("Failed to activate registration")
538
+ logger.error("[ACTIVATE] Transaction failed")
337
539
  return False
338
540
 
339
- logger.info("Registration activation transaction sent successfully")
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 [event["name"] for event in events]:
344
- logger.error("Activation event not found")
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("Service is not in active registration, cannot register agent")
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
- 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
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
- # 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")
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=agent_account_address,
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
- service_info = self.registry.get_service(service_id)
459
- security_deposit = service_info["security_deposit"]
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=(security_deposit * len(self.service.agent_ids)),
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("Failed to register agent")
743
+ logger.error("[REGISTER] Transaction failed")
477
744
  return False
478
745
 
479
- logger.info("Agent registration transaction sent successfully")
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 [event["name"] for event in events]:
484
- logger.error("Agent registration event not found")
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
- # Check that the service has finished registration
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("Service registration is not finished, cannot deploy")
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("Failed to deploy service")
795
+ logger.error("[DEPLOY] Transaction failed")
512
796
  return None
513
797
 
514
- logger.info("Service deployment transaction sent successfully")
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 [event["name"] for event in events]:
519
- logger.error("Deploy service event not found")
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 deployment events")
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.info(f"Registered multisig {multisig_address} in wallet")
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("Service deployed successfully")
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
- logger.info(f"Spinning up service {service_id}")
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"Service {service_id} initial state: {current_state.name}")
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
- logger.info(f"Service deployed successfully (State: {current_state.name})")
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("Failed to stake service")
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("Activating registration...")
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("Registering agent...")
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("Deploying service...")
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"Unknown or invalid state for spin up: {current_state.name}")
1032
+ logger.error(f"[SPIN-UP] Invalid state: {current_state.name}")
736
1033
  return False
737
1034
  return True
738
1035