iwa 0.0.10__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.
@@ -213,3 +213,53 @@ class ServiceManagerContract(ContractInstance):
213
213
  tx_params={"from": from_address},
214
214
  )
215
215
  return tx
216
+
217
+
218
+ class ServiceRegistryTokenUtilityContract(ContractInstance):
219
+ """Class to interact with the service registry token utility contract.
220
+
221
+ This contract manages token-bonded services, tracking agent bonds and
222
+ security deposits for services that use ERC20 tokens (like OLAS) instead
223
+ of native currency.
224
+ """
225
+
226
+ name = "service_registry_token_utility"
227
+ abi_path = OLAS_ABI_PATH / "service_registry_token_utility.json"
228
+
229
+ def get_agent_bond(self, service_id: int, agent_id: int) -> int:
230
+ """Get the agent bond for a specific agent in a service.
231
+
232
+ Args:
233
+ service_id: The service ID.
234
+ agent_id: The agent ID within the service.
235
+
236
+ Returns:
237
+ The bond amount in wei.
238
+
239
+ """
240
+ return self.call("getAgentBond", service_id, agent_id)
241
+
242
+ def get_operator_balance(self, operator: str, service_id: int) -> int:
243
+ """Get the operator balance for a service.
244
+
245
+ Args:
246
+ operator: The operator address.
247
+ service_id: The service ID.
248
+
249
+ Returns:
250
+ The balance amount in wei.
251
+
252
+ """
253
+ return self.call("getOperatorBalance", operator, service_id)
254
+
255
+ def get_service_token_deposit(self, service_id: int) -> tuple:
256
+ """Get the token deposit info for a service.
257
+
258
+ Args:
259
+ service_id: The service ID.
260
+
261
+ Returns:
262
+ Tuple of (token_address, security_deposit).
263
+
264
+ """
265
+ return self.call("mapServiceIdTokenDeposit", service_id)
@@ -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
 
@@ -20,7 +77,26 @@ from iwa.plugins.olas.models import Service
20
77
 
21
78
 
22
79
  class LifecycleManagerMixin:
23
- """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
+ """
24
100
 
25
101
  def create(
26
102
  self,
@@ -192,7 +268,22 @@ class LifecycleManagerMixin:
192
268
  service_owner_account,
193
269
  bond_amount_wei: Wei,
194
270
  ) -> None:
195
- """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
+ """
196
287
  if not token_address:
197
288
  return
198
289
 
@@ -204,7 +295,7 @@ class LifecycleManagerMixin:
204
295
  logger.error(f"OLAS Service Registry Token Utility not found for chain: {chain_name}")
205
296
  return
206
297
 
207
- # 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)
208
299
  logger.info(f"Approving Token Utility {utility_address} for {2 * bond_amount_wei} tokens")
209
300
  approve_success = self.transfer_service.approve_erc20(
210
301
  owner_address_or_tag=service_owner_account.address,
@@ -218,7 +309,28 @@ class LifecycleManagerMixin:
218
309
  logger.error("Failed to approve Token Utility")
219
310
 
220
311
  def activate_registration(self) -> bool:
221
- """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
+ """
222
334
  service_id = self.service.service_id
223
335
  logger.info(f"[ACTIVATE] Starting activation for service {service_id}")
224
336
 
@@ -266,29 +378,50 @@ class LifecycleManagerMixin:
266
378
  def _ensure_token_approval_for_activation(
267
379
  self, token_address: str, security_deposit: Wei
268
380
  ) -> bool:
269
- """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
+ """
270
390
  is_native = str(token_address).lower() == str(ZERO_ADDRESS).lower()
271
391
  if is_native:
272
392
  return True
273
393
 
274
394
  try:
275
- # 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
276
406
  balance = self.wallet.balance_service.get_erc20_balance_wei(
277
407
  account_address_or_tag=self.service.service_owner_address,
278
408
  token_address_or_name=token_address,
279
409
  chain_name=self.chain_name,
280
410
  )
281
411
 
282
- if balance < security_deposit:
412
+ if balance < bond_amount:
283
413
  logger.error(
284
- f"[ACTIVATE] FAIL: Owner balance {balance} < required {security_deposit}"
414
+ f"[ACTIVATE] FAIL: Owner balance {balance} < required {bond_amount}"
285
415
  )
416
+ return False
286
417
 
287
418
  protocol_contracts = OLAS_CONTRACTS.get(self.chain_name.lower(), {})
288
419
  utility_address = protocol_contracts.get("OLAS_SERVICE_REGISTRY_TOKEN_UTILITY")
289
420
 
290
421
  if utility_address:
291
- 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
292
425
 
293
426
  # Check current allowance
294
427
  allowance = self.wallet.transfer_service.get_erc20_allowance(
@@ -298,9 +431,10 @@ class LifecycleManagerMixin:
298
431
  chain_name=self.chain_name,
299
432
  )
300
433
 
301
- if allowance < Web3.to_wei(10, "ether"): # Min threshold check
434
+ if allowance < required_approval:
302
435
  logger.info(
303
- f"Low allowance ({allowance}). Approving Token Utility {utility_address}"
436
+ f"[ACTIVATE] Allowance ({allowance}) < required ({required_approval}). "
437
+ f"Approving Token Utility {utility_address}"
304
438
  )
305
439
  success_approve = self.wallet.transfer_service.approve_erc20(
306
440
  owner_address_or_tag=self.service.service_owner_address,
@@ -310,27 +444,87 @@ class LifecycleManagerMixin:
310
444
  chain_name=self.chain_name,
311
445
  )
312
446
  if not success_approve:
313
- logger.warning("Token approval transaction returned failure.")
447
+ logger.error("[ACTIVATE] Token approval failed")
314
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
+ )
315
454
  return True
316
455
  except Exception as e:
317
- logger.warning(f"Failed to check/approve tokens: {e}")
318
- return False # Return False only if we are strict, or True if we want to try anyway?
319
- # Original code swallowed exception but continued.
320
- # If we want to return early, we should return False.
321
- # However, if we swallow, we return True. Let's stick to original behavior,
322
- # BUT original code didn't return False here, it just logged and continued.
323
- # To be safer with clean code, if token approval fails, we should probably stop.
324
- # Let's assume we return True to match original "swallow" behavior but log it.
325
- 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
326
503
 
327
504
  def _send_activation_transaction(self, service_id: int, security_deposit: Wei) -> bool:
328
- """Send the activation transaction."""
329
- logger.debug(f"[ACTIVATE] Preparing tx: service_id={service_id}, value={security_deposit}")
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}")
330
524
  activate_tx = self.manager.prepare_activate_registration_tx(
331
525
  from_address=self.wallet.master_account.address,
332
526
  service_id=service_id,
333
- value=security_deposit,
527
+ value=activation_value,
334
528
  )
335
529
  logger.debug(f"[ACTIVATE] TX prepared: to={activate_tx.get('to')}")
336
530
 
@@ -441,7 +635,12 @@ class LifecycleManagerMixin:
441
635
  def _ensure_agent_token_approval(
442
636
  self, agent_account_address: str, bond_amount_wei: Optional[Wei]
443
637
  ) -> bool:
444
- """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
+ """
445
644
  service_id = self.service.service_id
446
645
  token_address = self._get_service_token(service_id)
447
646
  is_native = str(token_address) == str(ZERO_ADDRESS)
@@ -449,38 +648,75 @@ class LifecycleManagerMixin:
449
648
  if is_native:
450
649
  return True
451
650
 
651
+ # Get exact bond from Token Utility if not explicitly provided
452
652
  if not bond_amount_wei:
453
- logger.warning("No bond amount provided for token bonding. Agent might fail to bond.")
454
- # We don't return False here, similar to original logic, just warn.
455
- # But approval will fail if we try to approve None.
456
- 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
457
659
 
458
- # 1. Service Owner Approves Token Utility (for Bond)
459
- # The service owner (operator) pays the bond, not the agent.
460
- 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")
461
661
 
462
662
  utility_address = str(
463
663
  OLAS_CONTRACTS[self.chain_name]["OLAS_SERVICE_REGISTRY_TOKEN_UTILITY"]
464
664
  )
465
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)
466
681
  approve_success = self.wallet.transfer_service.approve_erc20(
467
682
  token_address_or_name=token_address,
468
683
  spender_address_or_tag=utility_address,
469
684
  amount_wei=bond_amount_wei,
470
- owner_address_or_tag=agent_account_address,
685
+ owner_address_or_tag=self.service.service_owner_address,
471
686
  chain_name=self.chain_name,
472
687
  )
473
688
  if not approve_success:
474
- logger.error("Failed to approve token for agent registration")
689
+ logger.error("[REGISTER] Failed to approve token for agent registration")
475
690
  return False
691
+
692
+ logger.info(f"[REGISTER] Approved {bond_amount_wei} wei to Token Utility")
476
693
  return True
477
694
 
478
695
  def _send_register_agent_transaction(self, agent_account_address: str) -> bool:
479
- """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
+ """
480
702
  service_id = self.service.service_id
481
- service_info = self.registry.get_service(service_id)
482
- security_deposit = service_info["security_deposit"]
483
- total_value = security_deposit * len(self.service.agent_ids)
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
+ )
484
720
 
485
721
  logger.debug(
486
722
  f"[REGISTER] Preparing tx: agent={agent_account_address}, "