iwa 0.0.1a6__py3-none-any.whl → 0.0.10__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 +30 -23
- iwa/core/chain/models.py +21 -0
- iwa/core/contracts/contract.py +8 -2
- iwa/core/pricing.py +30 -21
- iwa/core/services/safe.py +13 -8
- iwa/core/services/transaction.py +15 -4
- iwa/core/utils.py +22 -0
- iwa/plugins/gnosis/safe.py +4 -3
- iwa/plugins/gnosis/tests/test_safe.py +9 -7
- iwa/plugins/olas/contracts/service.py +4 -4
- iwa/plugins/olas/contracts/staking.py +2 -3
- iwa/plugins/olas/plugin.py +14 -7
- iwa/plugins/olas/service_manager/lifecycle.py +109 -48
- iwa/plugins/olas/service_manager/mech.py +1 -1
- iwa/plugins/olas/service_manager/staking.py +92 -34
- 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_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 +1 -2
- iwa/web/routers/olas/staking.py +19 -22
- 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.1a6.dist-info → iwa-0.0.10.dist-info}/METADATA +1 -1
- {iwa-0.0.1a6.dist-info → iwa-0.0.10.dist-info}/RECORD +50 -49
- 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.1a6.dist-info → iwa-0.0.10.dist-info}/WHEEL +0 -0
- {iwa-0.0.1a6.dist-info → iwa-0.0.10.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.1a6.dist-info → iwa-0.0.10.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.1a6.dist-info → iwa-0.0.10.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ from web3.types import Wei
|
|
|
9
9
|
from iwa.core.chain import ChainInterfaces
|
|
10
10
|
from iwa.core.constants import NATIVE_CURRENCY_ADDRESS, ZERO_ADDRESS
|
|
11
11
|
from iwa.core.types import EthereumAddress
|
|
12
|
+
from iwa.core.utils import get_tx_hash
|
|
12
13
|
from iwa.plugins.olas.constants import (
|
|
13
14
|
OLAS_CONTRACTS,
|
|
14
15
|
TRADER_CONFIG_HASH,
|
|
@@ -219,26 +220,36 @@ class LifecycleManagerMixin:
|
|
|
219
220
|
def activate_registration(self) -> bool:
|
|
220
221
|
"""Activate registration for the service."""
|
|
221
222
|
service_id = self.service.service_id
|
|
223
|
+
logger.info(f"[ACTIVATE] Starting activation for service {service_id}")
|
|
224
|
+
|
|
222
225
|
if not self._validate_pre_registration_state(service_id):
|
|
223
226
|
return False
|
|
224
227
|
|
|
225
228
|
token_address = self._get_service_token(service_id)
|
|
229
|
+
logger.debug(f"[ACTIVATE] Token address: {token_address}")
|
|
230
|
+
|
|
226
231
|
service_info = self.registry.get_service(service_id)
|
|
227
232
|
security_deposit = service_info["security_deposit"]
|
|
233
|
+
logger.info(f"[ACTIVATE] Security deposit required: {security_deposit} wei")
|
|
228
234
|
|
|
229
235
|
if not self._ensure_token_approval_for_activation(token_address, security_deposit):
|
|
236
|
+
logger.error("[ACTIVATE] Token approval failed")
|
|
230
237
|
return False
|
|
231
238
|
|
|
239
|
+
logger.info("[ACTIVATE] Sending activation transaction...")
|
|
232
240
|
return self._send_activation_transaction(service_id, security_deposit)
|
|
233
241
|
|
|
234
242
|
def _validate_pre_registration_state(self, service_id: int) -> bool:
|
|
235
243
|
"""Check if service is in PRE_REGISTRATION state."""
|
|
236
|
-
# Check that the service is created
|
|
237
244
|
service_info = self.registry.get_service(service_id)
|
|
238
245
|
service_state = service_info["state"]
|
|
246
|
+
logger.debug(f"[ACTIVATE] Current state: {service_state.name}")
|
|
239
247
|
if service_state != ServiceState.PRE_REGISTRATION:
|
|
240
|
-
logger.error(
|
|
248
|
+
logger.error(
|
|
249
|
+
f"[ACTIVATE] Service is in {service_state.name}, expected PRE_REGISTRATION"
|
|
250
|
+
)
|
|
241
251
|
return False
|
|
252
|
+
logger.debug("[ACTIVATE] State validated: PRE_REGISTRATION")
|
|
242
253
|
return True
|
|
243
254
|
|
|
244
255
|
def _get_service_token(self, service_id: int) -> str:
|
|
@@ -315,17 +326,14 @@ class LifecycleManagerMixin:
|
|
|
315
326
|
|
|
316
327
|
def _send_activation_transaction(self, service_id: int, security_deposit: Wei) -> bool:
|
|
317
328
|
"""Send the activation transaction."""
|
|
318
|
-
|
|
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.
|
|
329
|
+
logger.debug(f"[ACTIVATE] Preparing tx: service_id={service_id}, value={security_deposit}")
|
|
324
330
|
activate_tx = self.manager.prepare_activate_registration_tx(
|
|
325
331
|
from_address=self.wallet.master_account.address,
|
|
326
332
|
service_id=service_id,
|
|
327
333
|
value=security_deposit,
|
|
328
334
|
)
|
|
335
|
+
logger.debug(f"[ACTIVATE] TX prepared: to={activate_tx.get('to')}")
|
|
336
|
+
|
|
329
337
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
330
338
|
transaction=activate_tx,
|
|
331
339
|
signer_address_or_tag=self.wallet.master_account.address,
|
|
@@ -333,17 +341,21 @@ class LifecycleManagerMixin:
|
|
|
333
341
|
)
|
|
334
342
|
|
|
335
343
|
if not success:
|
|
336
|
-
logger.error("
|
|
344
|
+
logger.error("[ACTIVATE] Transaction failed")
|
|
337
345
|
return False
|
|
338
346
|
|
|
339
|
-
|
|
347
|
+
tx_hash = get_tx_hash(receipt)
|
|
348
|
+
logger.info(f"[ACTIVATE] TX sent: {tx_hash}")
|
|
340
349
|
|
|
341
350
|
events = self.registry.extract_events(receipt)
|
|
351
|
+
event_names = [e["name"] for e in events]
|
|
352
|
+
logger.debug(f"[ACTIVATE] Events: {event_names}")
|
|
342
353
|
|
|
343
|
-
if "ActivateRegistration" not in
|
|
344
|
-
logger.error("
|
|
354
|
+
if "ActivateRegistration" not in event_names:
|
|
355
|
+
logger.error("[ACTIVATE] ActivateRegistration event not found")
|
|
345
356
|
return False
|
|
346
357
|
|
|
358
|
+
logger.info("[ACTIVATE] Success - service is now ACTIVE_REGISTRATION")
|
|
347
359
|
return True
|
|
348
360
|
|
|
349
361
|
def register_agent(
|
|
@@ -360,24 +372,35 @@ class LifecycleManagerMixin:
|
|
|
360
372
|
True if registration succeeded, False otherwise.
|
|
361
373
|
|
|
362
374
|
"""
|
|
375
|
+
logger.info(f"[REGISTER] Starting agent registration for service {self.service.service_id}")
|
|
376
|
+
logger.debug(f"[REGISTER] agent_address={agent_address}, bond={bond_amount_wei}")
|
|
377
|
+
|
|
363
378
|
if not self._validate_active_registration_state():
|
|
364
379
|
return False
|
|
365
380
|
|
|
366
381
|
agent_account_address = self._get_or_create_agent_account(agent_address)
|
|
367
382
|
if not agent_account_address:
|
|
383
|
+
logger.error("[REGISTER] Failed to get/create agent account")
|
|
368
384
|
return False
|
|
385
|
+
logger.info(f"[REGISTER] Agent address: {agent_account_address}")
|
|
369
386
|
|
|
370
387
|
if not self._ensure_agent_token_approval(agent_account_address, bond_amount_wei):
|
|
388
|
+
logger.error("[REGISTER] Token approval failed")
|
|
371
389
|
return False
|
|
372
390
|
|
|
391
|
+
logger.info("[REGISTER] Sending register agent transaction...")
|
|
373
392
|
return self._send_register_agent_transaction(agent_account_address)
|
|
374
393
|
|
|
375
394
|
def _validate_active_registration_state(self) -> bool:
|
|
376
395
|
"""Check that the service is in active registration."""
|
|
377
396
|
service_state = self.registry.get_service(self.service.service_id)["state"]
|
|
397
|
+
logger.debug(f"[REGISTER] Current state: {service_state.name}")
|
|
378
398
|
if service_state != ServiceState.ACTIVE_REGISTRATION:
|
|
379
|
-
logger.error(
|
|
399
|
+
logger.error(
|
|
400
|
+
f"[REGISTER] Service is in {service_state.name}, expected ACTIVE_REGISTRATION"
|
|
401
|
+
)
|
|
380
402
|
return False
|
|
403
|
+
logger.debug("[REGISTER] State validated: ACTIVE_REGISTRATION")
|
|
381
404
|
return True
|
|
382
405
|
|
|
383
406
|
def _get_or_create_agent_account(self, agent_address: Optional[str]) -> Optional[str]:
|
|
@@ -457,14 +480,22 @@ class LifecycleManagerMixin:
|
|
|
457
480
|
service_id = self.service.service_id
|
|
458
481
|
service_info = self.registry.get_service(service_id)
|
|
459
482
|
security_deposit = service_info["security_deposit"]
|
|
483
|
+
total_value = security_deposit * len(self.service.agent_ids)
|
|
484
|
+
|
|
485
|
+
logger.debug(
|
|
486
|
+
f"[REGISTER] Preparing tx: agent={agent_account_address}, "
|
|
487
|
+
f"agent_ids={self.service.agent_ids}, value={total_value}"
|
|
488
|
+
)
|
|
460
489
|
|
|
461
490
|
register_tx = self.manager.prepare_register_agents_tx(
|
|
462
491
|
from_address=self.wallet.master_account.address,
|
|
463
492
|
service_id=service_id,
|
|
464
493
|
agent_instances=[agent_account_address],
|
|
465
494
|
agent_ids=self.service.agent_ids,
|
|
466
|
-
value=
|
|
495
|
+
value=total_value,
|
|
467
496
|
)
|
|
497
|
+
logger.debug(f"[REGISTER] TX prepared: to={register_tx.get('to')}")
|
|
498
|
+
|
|
468
499
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
469
500
|
transaction=register_tx,
|
|
470
501
|
signer_address_or_tag=self.wallet.master_account.address,
|
|
@@ -473,33 +504,50 @@ class LifecycleManagerMixin:
|
|
|
473
504
|
)
|
|
474
505
|
|
|
475
506
|
if not success:
|
|
476
|
-
logger.error("
|
|
507
|
+
logger.error("[REGISTER] Transaction failed")
|
|
477
508
|
return False
|
|
478
509
|
|
|
479
|
-
|
|
510
|
+
tx_hash = get_tx_hash(receipt)
|
|
511
|
+
logger.info(f"[REGISTER] TX sent: {tx_hash}")
|
|
480
512
|
|
|
481
513
|
events = self.registry.extract_events(receipt)
|
|
514
|
+
event_names = [e["name"] for e in events]
|
|
515
|
+
logger.debug(f"[REGISTER] Events: {event_names}")
|
|
482
516
|
|
|
483
|
-
if "RegisterInstance" not in
|
|
484
|
-
logger.error("
|
|
517
|
+
if "RegisterInstance" not in event_names:
|
|
518
|
+
logger.error("[REGISTER] RegisterInstance event not found")
|
|
485
519
|
return False
|
|
486
520
|
|
|
487
521
|
self.service.agent_address = EthereumAddress(agent_account_address)
|
|
488
522
|
self._update_and_save_service_state()
|
|
523
|
+
logger.info("[REGISTER] Success - service is now FINISHED_REGISTRATION")
|
|
489
524
|
return True
|
|
490
525
|
|
|
491
|
-
def deploy(self) -> Optional[str]:
|
|
526
|
+
def deploy(self) -> Optional[str]: # noqa: C901
|
|
492
527
|
"""Deploy the service."""
|
|
493
|
-
|
|
528
|
+
logger.info(f"[DEPLOY] Starting deployment for service {self.service.service_id}")
|
|
529
|
+
|
|
494
530
|
service_state = self.registry.get_service(self.service.service_id)["state"]
|
|
531
|
+
logger.debug(f"[DEPLOY] Current state: {service_state.name}")
|
|
532
|
+
|
|
495
533
|
if service_state != ServiceState.FINISHED_REGISTRATION:
|
|
496
|
-
logger.error(
|
|
534
|
+
logger.error(
|
|
535
|
+
f"[DEPLOY] Service is in {service_state.name}, expected FINISHED_REGISTRATION"
|
|
536
|
+
)
|
|
497
537
|
return False
|
|
498
538
|
|
|
539
|
+
logger.debug(f"[DEPLOY] Preparing deploy tx for owner {self.service.service_owner_address}")
|
|
499
540
|
deploy_tx = self.manager.prepare_deploy_tx(
|
|
500
541
|
from_address=self.service.service_owner_address,
|
|
501
542
|
service_id=self.service.service_id,
|
|
502
543
|
)
|
|
544
|
+
|
|
545
|
+
if not deploy_tx:
|
|
546
|
+
logger.error("[DEPLOY] Failed to prepare deploy transaction")
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
logger.debug(f"[DEPLOY] TX prepared: to={deploy_tx.get('to')}")
|
|
550
|
+
|
|
503
551
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
504
552
|
transaction=deploy_tx,
|
|
505
553
|
signer_address_or_tag=self.service.service_owner_address,
|
|
@@ -508,29 +556,31 @@ class LifecycleManagerMixin:
|
|
|
508
556
|
)
|
|
509
557
|
|
|
510
558
|
if not success:
|
|
511
|
-
logger.error("
|
|
559
|
+
logger.error("[DEPLOY] Transaction failed")
|
|
512
560
|
return None
|
|
513
561
|
|
|
514
|
-
|
|
562
|
+
tx_hash = get_tx_hash(receipt)
|
|
563
|
+
logger.info(f"[DEPLOY] TX sent: {tx_hash}")
|
|
515
564
|
|
|
516
565
|
events = self.registry.extract_events(receipt)
|
|
566
|
+
event_names = [e["name"] for e in events]
|
|
567
|
+
logger.debug(f"[DEPLOY] Events: {event_names}")
|
|
517
568
|
|
|
518
|
-
if "DeployService" not in
|
|
519
|
-
logger.error("
|
|
569
|
+
if "DeployService" not in event_names:
|
|
570
|
+
logger.error("[DEPLOY] DeployService event not found")
|
|
520
571
|
return None
|
|
521
572
|
|
|
522
573
|
multisig_address = None
|
|
523
|
-
|
|
524
574
|
for event in events:
|
|
525
575
|
if event["name"] == "CreateMultisigWithAgents":
|
|
526
576
|
multisig_address = event["args"]["multisig"]
|
|
527
|
-
logger.info(f"Service deployed with multisig address: {multisig_address}")
|
|
528
577
|
break
|
|
529
578
|
|
|
530
579
|
if multisig_address is None:
|
|
531
|
-
logger.error("Multisig address not found in
|
|
580
|
+
logger.error("[DEPLOY] Multisig address not found in events")
|
|
532
581
|
return None
|
|
533
582
|
|
|
583
|
+
logger.info(f"[DEPLOY] Multisig created: {multisig_address}")
|
|
534
584
|
self.service.multisig_address = EthereumAddress(multisig_address)
|
|
535
585
|
self._update_and_save_service_state()
|
|
536
586
|
|
|
@@ -551,11 +601,11 @@ class LifecycleManagerMixin:
|
|
|
551
601
|
)
|
|
552
602
|
self.wallet.key_storage.accounts[multisig_address] = safe_account
|
|
553
603
|
self.wallet.key_storage.save()
|
|
554
|
-
logger.
|
|
604
|
+
logger.debug("[DEPLOY] Registered multisig in wallet")
|
|
555
605
|
except Exception as e:
|
|
556
|
-
logger.warning(f"Failed to register multisig in wallet: {e}")
|
|
606
|
+
logger.warning(f"[DEPLOY] Failed to register multisig in wallet: {e}")
|
|
557
607
|
|
|
558
|
-
logger.info("
|
|
608
|
+
logger.info("[DEPLOY] Success - service is now DEPLOYED")
|
|
559
609
|
return multisig_address
|
|
560
610
|
|
|
561
611
|
def terminate(self) -> bool:
|
|
@@ -667,21 +717,31 @@ class LifecycleManagerMixin:
|
|
|
667
717
|
"""
|
|
668
718
|
if not service_id:
|
|
669
719
|
if not self.service:
|
|
670
|
-
logger.error("No active service and no service_id provided")
|
|
720
|
+
logger.error("[SPIN-UP] No active service and no service_id provided")
|
|
671
721
|
return False
|
|
672
722
|
service_id = self.service.service_id
|
|
673
|
-
|
|
723
|
+
|
|
724
|
+
logger.info("=" * 50)
|
|
725
|
+
logger.info(f"[SPIN-UP] Starting spin_up for service {service_id}")
|
|
726
|
+
logger.info(f"[SPIN-UP] Parameters: agent_address={agent_address}, bond={bond_amount_wei}")
|
|
727
|
+
logger.info(
|
|
728
|
+
f"[SPIN-UP] Staking contract: {staking_contract.address if staking_contract else 'None'}"
|
|
729
|
+
)
|
|
730
|
+
logger.info("=" * 50)
|
|
674
731
|
|
|
675
732
|
current_state = self._get_service_state_safe(service_id)
|
|
676
733
|
if not current_state:
|
|
677
734
|
return False
|
|
678
735
|
|
|
679
|
-
logger.info(f"
|
|
736
|
+
logger.info(f"[SPIN-UP] Initial state: {current_state.name}")
|
|
680
737
|
|
|
738
|
+
step = 1
|
|
681
739
|
while current_state != ServiceState.DEPLOYED:
|
|
682
740
|
previous_state = current_state
|
|
741
|
+
logger.info(f"[SPIN-UP] Step {step}: Processing {current_state.name}...")
|
|
683
742
|
|
|
684
743
|
if not self._process_spin_up_state(current_state, agent_address, bond_amount_wei):
|
|
744
|
+
logger.error(f"[SPIN-UP] Step {step} FAILED at state {current_state.name}")
|
|
685
745
|
return False
|
|
686
746
|
|
|
687
747
|
# Refresh state
|
|
@@ -690,21 +750,25 @@ class LifecycleManagerMixin:
|
|
|
690
750
|
return False
|
|
691
751
|
|
|
692
752
|
if current_state == previous_state:
|
|
693
|
-
logger.error(f"State stuck at {current_state.name} after action")
|
|
753
|
+
logger.error(f"[SPIN-UP] State stuck at {current_state.name} after action")
|
|
694
754
|
return False
|
|
695
755
|
|
|
696
|
-
|
|
756
|
+
logger.info(f"[SPIN-UP] Step {step} OK: {previous_state.name} -> {current_state.name}")
|
|
757
|
+
step += 1
|
|
758
|
+
|
|
759
|
+
logger.info(f"[SPIN-UP] Service {service_id} is now DEPLOYED")
|
|
697
760
|
|
|
698
761
|
# Stake if requested
|
|
699
762
|
if staking_contract:
|
|
700
|
-
logger.info("Staking service...")
|
|
763
|
+
logger.info(f"[SPIN-UP] Step {step}: Staking service...")
|
|
701
764
|
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.
|
|
765
|
+
logger.error("[SPIN-UP] Staking FAILED")
|
|
705
766
|
return False
|
|
706
|
-
logger.info("Service staked successfully")
|
|
767
|
+
logger.info(f"[SPIN-UP] Step {step} OK: Service staked successfully")
|
|
707
768
|
|
|
769
|
+
logger.info("=" * 50)
|
|
770
|
+
logger.info(f"[SPIN-UP] COMPLETE - Service {service_id} is deployed and ready")
|
|
771
|
+
logger.info("=" * 50)
|
|
708
772
|
return True
|
|
709
773
|
|
|
710
774
|
def _process_spin_up_state(
|
|
@@ -715,24 +779,21 @@ class LifecycleManagerMixin:
|
|
|
715
779
|
) -> bool:
|
|
716
780
|
"""Process a single state transition for spin up."""
|
|
717
781
|
if current_state == ServiceState.PRE_REGISTRATION:
|
|
718
|
-
logger.info("
|
|
782
|
+
logger.info("[SPIN-UP] Action: activate_registration()")
|
|
719
783
|
if not self.activate_registration():
|
|
720
|
-
logger.error("Failed to activate registration")
|
|
721
784
|
return False
|
|
722
785
|
elif current_state == ServiceState.ACTIVE_REGISTRATION:
|
|
723
|
-
logger.info("
|
|
786
|
+
logger.info("[SPIN-UP] Action: register_agent()")
|
|
724
787
|
if not self.register_agent(
|
|
725
788
|
agent_address=agent_address, bond_amount_wei=bond_amount_wei
|
|
726
789
|
):
|
|
727
|
-
logger.error("Failed to register agent")
|
|
728
790
|
return False
|
|
729
791
|
elif current_state == ServiceState.FINISHED_REGISTRATION:
|
|
730
|
-
logger.info("
|
|
792
|
+
logger.info("[SPIN-UP] Action: deploy()")
|
|
731
793
|
if not self.deploy():
|
|
732
|
-
logger.error("Failed to deploy service")
|
|
733
794
|
return False
|
|
734
795
|
else:
|
|
735
|
-
logger.error(f"
|
|
796
|
+
logger.error(f"[SPIN-UP] Invalid state: {current_state.name}")
|
|
736
797
|
return False
|
|
737
798
|
return True
|
|
738
799
|
|
|
@@ -296,7 +296,7 @@ class MechManagerMixin:
|
|
|
296
296
|
chain_name=self.chain_name,
|
|
297
297
|
tags=["olas_mech_request"],
|
|
298
298
|
)
|
|
299
|
-
tx_hash = receipt.get("transactionHash")
|
|
299
|
+
tx_hash = Web3.to_hex(receipt.get("transactionHash")) if success else None
|
|
300
300
|
|
|
301
301
|
if not tx_hash:
|
|
302
302
|
logger.error("Failed to send mech request transaction")
|
|
@@ -8,6 +8,7 @@ from web3 import Web3
|
|
|
8
8
|
|
|
9
9
|
from iwa.core.contracts.erc20 import ERC20Contract
|
|
10
10
|
from iwa.core.types import EthereumAddress
|
|
11
|
+
from iwa.core.utils import get_tx_hash
|
|
11
12
|
from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
|
|
12
13
|
from iwa.plugins.olas.models import StakingStatus
|
|
13
14
|
|
|
@@ -175,92 +176,141 @@ class StakingManagerMixin:
|
|
|
175
176
|
True if staking succeeded, False otherwise.
|
|
176
177
|
|
|
177
178
|
"""
|
|
179
|
+
logger.info("=" * 50)
|
|
180
|
+
logger.info(f"[STAKE] Starting staking for service {self.service.service_id}")
|
|
181
|
+
logger.info(f"[STAKE] Contract: {staking_contract.address}")
|
|
182
|
+
logger.info("=" * 50)
|
|
183
|
+
|
|
178
184
|
# 1. Validation
|
|
185
|
+
logger.info("[STAKE] Step 1: Checking requirements...")
|
|
179
186
|
requirements = self._check_stake_requirements(staking_contract)
|
|
180
187
|
if not requirements:
|
|
188
|
+
logger.error("[STAKE] Step 1 FAILED: Requirements not met")
|
|
181
189
|
return False
|
|
190
|
+
logger.info("[STAKE] Step 1 OK: All requirements met")
|
|
182
191
|
|
|
183
192
|
min_deposit = requirements["min_deposit"]
|
|
193
|
+
logger.info(
|
|
194
|
+
f"[STAKE] Min deposit required: {min_deposit} wei ({min_deposit / 1e18:.2f} OLAS)"
|
|
195
|
+
)
|
|
184
196
|
|
|
185
197
|
# 2. Approve Tokens
|
|
198
|
+
logger.info("[STAKE] Step 2: Approving tokens...")
|
|
186
199
|
if not self._approve_staking_tokens(staking_contract, min_deposit):
|
|
200
|
+
logger.error("[STAKE] Step 2 FAILED: Token approval failed")
|
|
187
201
|
return False
|
|
202
|
+
logger.info("[STAKE] Step 2 OK: Tokens approved")
|
|
188
203
|
|
|
189
204
|
# 3. Execute Stake Transaction
|
|
190
|
-
|
|
205
|
+
logger.info("[STAKE] Step 3: Executing stake transaction...")
|
|
206
|
+
result = self._execute_stake_transaction(staking_contract)
|
|
207
|
+
if result:
|
|
208
|
+
logger.info("[STAKE] Step 3 OK: Staking successful")
|
|
209
|
+
logger.info("=" * 50)
|
|
210
|
+
logger.info(f"[STAKE] COMPLETE - Service {self.service.service_id} is now staked")
|
|
211
|
+
logger.info("=" * 50)
|
|
212
|
+
else:
|
|
213
|
+
logger.error("[STAKE] Step 3 FAILED: Stake transaction failed")
|
|
214
|
+
return result
|
|
191
215
|
|
|
192
216
|
def _check_stake_requirements(self, staking_contract) -> Optional[dict]:
|
|
193
217
|
"""Validate all conditions required for staking."""
|
|
194
218
|
from iwa.plugins.olas.contracts.service import ServiceState
|
|
195
219
|
|
|
196
|
-
|
|
220
|
+
logger.debug("[STAKE] Fetching contract requirements...")
|
|
197
221
|
reqs = staking_contract.get_requirements()
|
|
198
222
|
min_deposit = reqs["min_staking_deposit"]
|
|
199
223
|
required_bond = reqs["required_agent_bond"]
|
|
200
224
|
staking_token = Web3.to_checksum_address(reqs["staking_token"])
|
|
201
225
|
staking_token_lower = staking_token.lower()
|
|
202
226
|
|
|
203
|
-
logger.info(
|
|
227
|
+
logger.info("[STAKE] Contract requirements:")
|
|
228
|
+
logger.info(f"[STAKE] - min_staking_deposit: {min_deposit} wei")
|
|
229
|
+
logger.info(f"[STAKE] - required_agent_bond: {required_bond} wei")
|
|
230
|
+
logger.info(f"[STAKE] - staking_token: {staking_token}")
|
|
204
231
|
|
|
205
232
|
# Check service state
|
|
233
|
+
logger.debug("[STAKE] Checking service state...")
|
|
206
234
|
service_info = self.registry.get_service(self.service.service_id)
|
|
207
235
|
service_state = service_info["state"]
|
|
208
|
-
logger.info(f"Service state: {service_state.name}")
|
|
236
|
+
logger.info(f"[STAKE] Service state: {service_state.name}")
|
|
209
237
|
|
|
210
238
|
if service_state != ServiceState.DEPLOYED:
|
|
211
|
-
logger.error("Service is
|
|
239
|
+
logger.error(f"[STAKE] FAIL: Service is {service_state.name}, expected DEPLOYED")
|
|
212
240
|
return None
|
|
241
|
+
logger.debug("[STAKE] OK: Service is DEPLOYED")
|
|
213
242
|
|
|
214
243
|
# Check token compatibility
|
|
215
244
|
service_token = (self.service.token_address or "").lower()
|
|
245
|
+
logger.debug(f"[STAKE] Service token: {service_token}")
|
|
216
246
|
if service_token != staking_token_lower:
|
|
217
247
|
logger.error(
|
|
218
|
-
f"
|
|
219
|
-
f"
|
|
248
|
+
f"[STAKE] FAIL: Token mismatch - service={service_token or 'native'}, "
|
|
249
|
+
f"contract requires={staking_token_lower}"
|
|
220
250
|
)
|
|
221
251
|
return None
|
|
252
|
+
logger.debug("[STAKE] OK: Token matches")
|
|
222
253
|
|
|
223
254
|
# Check agent bond
|
|
255
|
+
# NOTE: On-chain bond values often show 1 wei regardless of what was passed
|
|
256
|
+
# during service creation. This is a known issue with the OLAS contracts.
|
|
257
|
+
# We log a warning but don't block staking because of this discrepancy.
|
|
258
|
+
logger.debug("[STAKE] Checking agent bond...")
|
|
224
259
|
try:
|
|
225
260
|
agent_ids = service_info["agent_ids"]
|
|
226
261
|
if not agent_ids:
|
|
227
|
-
logger.error("No agent IDs found
|
|
262
|
+
logger.error("[STAKE] FAIL: No agent IDs found")
|
|
228
263
|
return None
|
|
229
264
|
|
|
230
|
-
|
|
231
|
-
agent_params =
|
|
265
|
+
params_list = self.registry.get_agent_params(self.service.service_id)
|
|
266
|
+
agent_params = params_list[0]
|
|
232
267
|
current_bond = agent_params["bond"]
|
|
268
|
+
logger.info(
|
|
269
|
+
f"[STAKE] Agent bond on-chain: {current_bond} wei (required: {required_bond} wei)"
|
|
270
|
+
)
|
|
233
271
|
|
|
234
272
|
if current_bond < required_bond:
|
|
235
|
-
logger.
|
|
236
|
-
f"
|
|
237
|
-
"
|
|
273
|
+
logger.warning(
|
|
274
|
+
f"[STAKE] WARN: On-chain bond ({current_bond}) < required ({required_bond}). "
|
|
275
|
+
"This is a known on-chain reporting issue. Proceeding anyway."
|
|
238
276
|
)
|
|
239
|
-
|
|
277
|
+
else:
|
|
278
|
+
logger.debug("[STAKE] OK: Agent bond sufficient")
|
|
240
279
|
except Exception as e:
|
|
241
|
-
logger.warning(f"Could not verify agent bond: {e}")
|
|
280
|
+
logger.warning(f"[STAKE] WARN: Could not verify agent bond: {e}")
|
|
242
281
|
|
|
243
282
|
# Check free slots
|
|
283
|
+
logger.debug("[STAKE] Checking available slots...")
|
|
244
284
|
staked_count = len(staking_contract.get_service_ids())
|
|
245
285
|
max_services = staking_contract.max_num_services
|
|
286
|
+
free_slots = max_services - staked_count
|
|
287
|
+
logger.info(f"[STAKE] Slots: {staked_count}/{max_services} used, {free_slots} free")
|
|
288
|
+
|
|
246
289
|
if staked_count >= max_services:
|
|
247
|
-
logger.error("
|
|
290
|
+
logger.error("[STAKE] FAIL: No free slots")
|
|
248
291
|
return None
|
|
292
|
+
logger.debug("[STAKE] OK: Slots available")
|
|
249
293
|
|
|
250
294
|
# Check OLAS balance
|
|
295
|
+
logger.debug("[STAKE] Checking master OLAS balance...")
|
|
251
296
|
erc20_contract = ERC20Contract(staking_token)
|
|
252
297
|
master_balance = erc20_contract.balance_of_wei(self.wallet.master_account.address)
|
|
298
|
+
logger.info(
|
|
299
|
+
f"[STAKE] Master OLAS balance: {master_balance} wei "
|
|
300
|
+
f"({master_balance / 1e18:.2f} OLAS, need {min_deposit / 1e18:.2f} OLAS)"
|
|
301
|
+
)
|
|
302
|
+
|
|
253
303
|
if master_balance < min_deposit:
|
|
254
|
-
logger.error(
|
|
255
|
-
f"Not enough tokens to stake service (have {master_balance}, need {min_deposit})"
|
|
256
|
-
)
|
|
304
|
+
logger.error(f"[STAKE] FAIL: Insufficient balance ({master_balance} < {min_deposit})")
|
|
257
305
|
return None
|
|
306
|
+
logger.debug("[STAKE] OK: Sufficient balance")
|
|
258
307
|
|
|
259
308
|
return {"min_deposit": min_deposit, "staking_token": staking_token}
|
|
260
309
|
|
|
261
310
|
def _approve_staking_tokens(self, staking_contract, min_deposit: int) -> bool:
|
|
262
311
|
"""Approve both the service NFT and OLAS tokens for staking."""
|
|
263
312
|
# Approve service NFT
|
|
313
|
+
logger.debug("[STAKE] Approving service NFT for staking contract...")
|
|
264
314
|
approve_tx = self.registry.prepare_approve_tx(
|
|
265
315
|
from_address=self.wallet.master_account.address,
|
|
266
316
|
spender=staking_contract.address,
|
|
@@ -275,14 +325,14 @@ class StakingManagerMixin:
|
|
|
275
325
|
)
|
|
276
326
|
|
|
277
327
|
if not success:
|
|
278
|
-
logger.error("
|
|
328
|
+
logger.error("[STAKE] FAIL: Service NFT approval failed")
|
|
279
329
|
return False
|
|
280
330
|
|
|
281
|
-
|
|
331
|
+
tx_hash = get_tx_hash(receipt)
|
|
332
|
+
logger.info(f"[STAKE] Service NFT approved: {tx_hash}")
|
|
282
333
|
|
|
283
334
|
# Approve OLAS tokens
|
|
284
|
-
|
|
285
|
-
# Retching for simplicity and safety
|
|
335
|
+
logger.debug(f"[STAKE] Approving OLAS tokens ({min_deposit} wei)...")
|
|
286
336
|
reqs = staking_contract.get_requirements()
|
|
287
337
|
staking_token = Web3.to_checksum_address(reqs["staking_token"])
|
|
288
338
|
erc20_contract = ERC20Contract(staking_token)
|
|
@@ -301,18 +351,21 @@ class StakingManagerMixin:
|
|
|
301
351
|
)
|
|
302
352
|
|
|
303
353
|
if not success:
|
|
304
|
-
logger.error("
|
|
354
|
+
logger.error("[STAKE] FAIL: OLAS token approval failed")
|
|
305
355
|
return False
|
|
306
356
|
|
|
307
|
-
|
|
357
|
+
tx_hash = get_tx_hash(receipt)
|
|
358
|
+
logger.info(f"[STAKE] OLAS tokens approved: {tx_hash}")
|
|
308
359
|
return True
|
|
309
360
|
|
|
310
361
|
def _execute_stake_transaction(self, staking_contract) -> bool:
|
|
311
362
|
"""Send the stake transaction and verify the result."""
|
|
363
|
+
logger.debug("[STAKE] Preparing stake transaction...")
|
|
312
364
|
stake_tx = staking_contract.prepare_stake_tx(
|
|
313
365
|
from_address=self.wallet.master_account.address,
|
|
314
366
|
service_id=self.service.service_id,
|
|
315
367
|
)
|
|
368
|
+
logger.debug(f"[STAKE] TX prepared: to={stake_tx.get('to')}")
|
|
316
369
|
|
|
317
370
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
318
371
|
transaction=stake_tx,
|
|
@@ -323,33 +376,38 @@ class StakingManagerMixin:
|
|
|
323
376
|
|
|
324
377
|
if not success:
|
|
325
378
|
if receipt and "status" in receipt and receipt["status"] == 0:
|
|
326
|
-
logger.error(f"
|
|
327
|
-
logger.error("
|
|
379
|
+
logger.error(f"[STAKE] TX reverted. Receipt: {receipt}")
|
|
380
|
+
logger.error("[STAKE] Stake transaction failed")
|
|
328
381
|
return False
|
|
329
382
|
|
|
330
|
-
|
|
383
|
+
tx_hash = get_tx_hash(receipt)
|
|
384
|
+
logger.info(f"[STAKE] TX sent: {tx_hash}")
|
|
331
385
|
|
|
332
386
|
events = staking_contract.extract_events(receipt)
|
|
333
387
|
event_names = [event["name"] for event in events]
|
|
388
|
+
logger.debug(f"[STAKE] Events: {event_names}")
|
|
334
389
|
|
|
335
390
|
if "ServiceStaked" not in event_names:
|
|
336
|
-
logger.error("
|
|
391
|
+
logger.error("[STAKE] ServiceStaked event not found")
|
|
337
392
|
return False
|
|
393
|
+
logger.debug("[STAKE] ServiceStaked event confirmed")
|
|
338
394
|
|
|
339
395
|
# Verify state
|
|
340
396
|
staking_state = staking_contract.get_staking_state(self.service.service_id)
|
|
397
|
+
logger.debug(f"[STAKE] Final staking state: {staking_state.name}")
|
|
398
|
+
|
|
341
399
|
if staking_state != StakingState.STAKED:
|
|
342
|
-
logger.error("Service
|
|
400
|
+
logger.error(f"[STAKE] FAIL: Service not staked (state={staking_state.name})")
|
|
343
401
|
return False
|
|
344
402
|
|
|
345
403
|
# Update local state
|
|
346
404
|
self.service.staking_contract_address = EthereumAddress(staking_contract.address)
|
|
347
405
|
self._update_and_save_service_state()
|
|
348
406
|
|
|
349
|
-
logger.info("Service
|
|
407
|
+
logger.info(f"[STAKE] Service {self.service.service_id} is now STAKED")
|
|
350
408
|
return True
|
|
351
409
|
|
|
352
|
-
def unstake(self, staking_contract) -> bool:
|
|
410
|
+
def unstake(self, staking_contract) -> bool: # noqa: C901
|
|
353
411
|
"""Unstake the service from the staking contract."""
|
|
354
412
|
if not self.service:
|
|
355
413
|
logger.error("No active service")
|
|
@@ -410,13 +468,13 @@ class StakingManagerMixin:
|
|
|
410
468
|
chain_name=self.chain_name,
|
|
411
469
|
tags=["olas_unstake_service"],
|
|
412
470
|
)
|
|
413
|
-
|
|
414
471
|
if not success:
|
|
415
472
|
logger.error(f"Failed to unstake service {self.service.service_id}: Transaction failed")
|
|
416
473
|
return False
|
|
417
474
|
|
|
475
|
+
tx_hash = get_tx_hash(receipt)
|
|
418
476
|
logger.info(
|
|
419
|
-
f"Unstake transaction sent: {
|
|
477
|
+
f"Unstake transaction sent: {tx_hash if receipt else 'No Receipt'}"
|
|
420
478
|
)
|
|
421
479
|
|
|
422
480
|
events = staking_contract.extract_events(receipt)
|
|
@@ -48,7 +48,12 @@ class TestOlasPlugin:
|
|
|
48
48
|
|
|
49
49
|
mock_wallet_class.assert_called_once()
|
|
50
50
|
mock_sm_class.assert_called_once_with(mock_wallet)
|
|
51
|
-
mock_manager.create.assert_called_once_with(
|
|
51
|
+
mock_manager.create.assert_called_once_with(
|
|
52
|
+
chain_name="gnosis",
|
|
53
|
+
service_owner_address_or_tag="0x1234",
|
|
54
|
+
token_address_or_tag="OLAS",
|
|
55
|
+
bond_amount_wei=100,
|
|
56
|
+
)
|
|
52
57
|
|
|
53
58
|
@patch("iwa.plugins.olas.plugin.Wallet")
|
|
54
59
|
@patch("iwa.plugins.olas.plugin.ServiceManager")
|