iwa 0.0.33__py3-none-any.whl → 0.0.59__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 +130 -11
- iwa/core/chain/models.py +15 -3
- iwa/core/chain/rate_limiter.py +48 -12
- iwa/core/chainlist.py +15 -10
- iwa/core/cli.py +4 -1
- iwa/core/contracts/cache.py +1 -1
- iwa/core/contracts/contract.py +1 -0
- iwa/core/contracts/decoder.py +10 -4
- iwa/core/http.py +31 -0
- iwa/core/ipfs.py +21 -7
- iwa/core/keys.py +65 -15
- iwa/core/models.py +58 -13
- iwa/core/pricing.py +10 -6
- iwa/core/rpc_monitor.py +1 -0
- iwa/core/secrets.py +27 -0
- iwa/core/services/account.py +1 -1
- iwa/core/services/balance.py +0 -23
- iwa/core/services/safe.py +72 -45
- iwa/core/services/safe_executor.py +350 -0
- iwa/core/services/transaction.py +43 -13
- iwa/core/services/transfer/erc20.py +14 -3
- iwa/core/services/transfer/native.py +14 -31
- iwa/core/services/transfer/swap.py +1 -0
- iwa/core/tests/test_gnosis_fee.py +91 -0
- iwa/core/tests/test_ipfs.py +85 -0
- iwa/core/tests/test_pricing.py +65 -0
- iwa/core/tests/test_regression_fixes.py +97 -0
- iwa/core/utils.py +2 -0
- iwa/core/wallet.py +6 -4
- iwa/plugins/gnosis/cow/quotes.py +2 -2
- iwa/plugins/gnosis/cow/swap.py +18 -32
- iwa/plugins/gnosis/tests/test_cow.py +19 -10
- iwa/plugins/olas/constants.py +15 -5
- iwa/plugins/olas/contracts/activity_checker.py +3 -3
- iwa/plugins/olas/contracts/staking.py +0 -1
- iwa/plugins/olas/events.py +15 -13
- iwa/plugins/olas/importer.py +29 -25
- iwa/plugins/olas/models.py +0 -3
- iwa/plugins/olas/plugin.py +16 -14
- iwa/plugins/olas/service_manager/drain.py +16 -9
- iwa/plugins/olas/service_manager/lifecycle.py +23 -12
- iwa/plugins/olas/service_manager/staking.py +15 -10
- iwa/plugins/olas/tests/test_importer_error_handling.py +13 -0
- iwa/plugins/olas/tests/test_olas_archiving.py +83 -0
- iwa/plugins/olas/tests/test_olas_integration.py +49 -29
- iwa/plugins/olas/tests/test_olas_view.py +5 -1
- iwa/plugins/olas/tests/test_service_manager.py +15 -17
- iwa/plugins/olas/tests/test_service_manager_errors.py +6 -5
- iwa/plugins/olas/tests/test_service_manager_flows.py +7 -6
- iwa/plugins/olas/tests/test_service_manager_rewards.py +5 -1
- iwa/plugins/olas/tests/test_service_staking.py +64 -38
- iwa/tools/drain_accounts.py +61 -0
- iwa/tools/list_contracts.py +2 -0
- iwa/tools/reset_env.py +2 -1
- iwa/tools/test_chainlist.py +5 -1
- iwa/tui/screens/wallets.py +2 -4
- iwa/web/routers/accounts.py +1 -1
- iwa/web/routers/olas/services.py +10 -5
- iwa/web/static/app.js +21 -9
- iwa/web/static/style.css +4 -0
- iwa/web/tests/test_web_endpoints.py +2 -2
- {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/METADATA +6 -3
- {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/RECORD +82 -71
- {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/WHEEL +1 -1
- tests/test_balance_service.py +0 -43
- tests/test_chain.py +13 -5
- tests/test_cli.py +2 -2
- tests/test_drain_coverage.py +12 -6
- tests/test_keys.py +23 -23
- tests/test_rate_limiter.py +2 -2
- tests/test_rate_limiter_retry.py +103 -0
- tests/test_rpc_efficiency.py +4 -1
- tests/test_rpc_rate_limit.py +34 -0
- tests/test_rpc_rotation.py +59 -11
- tests/test_safe_coverage.py +37 -23
- tests/test_safe_executor.py +361 -0
- tests/test_safe_integration.py +153 -0
- tests/test_safe_service.py +1 -1
- tests/test_transfer_swap_unit.py +5 -1
- tests/test_pricing.py +0 -160
- {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/top_level.txt +0 -0
iwa/plugins/olas/importer.py
CHANGED
|
@@ -181,9 +181,7 @@ class OlasServiceImporter:
|
|
|
181
181
|
if service.service_id:
|
|
182
182
|
key = f"{service.chain_name}:{service.service_id}"
|
|
183
183
|
if key in seen_keys:
|
|
184
|
-
logger.debug(
|
|
185
|
-
f"Skipping duplicate service {key} from {service.source_folder}"
|
|
186
|
-
)
|
|
184
|
+
logger.debug(f"Skipping duplicate service {key} from {service.source_folder}")
|
|
187
185
|
duplicates += 1
|
|
188
186
|
continue
|
|
189
187
|
seen_keys.add(key)
|
|
@@ -482,9 +480,7 @@ class OlasServiceImporter:
|
|
|
482
480
|
staking_program_id, chain_name
|
|
483
481
|
)
|
|
484
482
|
|
|
485
|
-
def _resolve_staking_contract(
|
|
486
|
-
self, staking_program_id: str, chain_name: str
|
|
487
|
-
) -> Optional[str]:
|
|
483
|
+
def _resolve_staking_contract(self, staking_program_id: str, chain_name: str) -> Optional[str]:
|
|
488
484
|
"""Resolve a staking program ID to a contract address."""
|
|
489
485
|
address = STAKING_PROGRAM_MAP.get(staking_program_id)
|
|
490
486
|
if address:
|
|
@@ -540,8 +536,10 @@ class OlasServiceImporter:
|
|
|
540
536
|
|
|
541
537
|
# Check for "safes" entry which indicates the owner is a Safe
|
|
542
538
|
# Structure: "safes": { "gnosis": "0x..." }
|
|
543
|
-
if
|
|
544
|
-
|
|
539
|
+
if (
|
|
540
|
+
"safes" in data and FLAGS_OWNER_SAFE in data["safes"]
|
|
541
|
+
): # Need to detect chain dynamically or iterate
|
|
542
|
+
pass
|
|
545
543
|
|
|
546
544
|
# Logic update:
|
|
547
545
|
# 1. Capture EOA address always (it's the signer)
|
|
@@ -557,9 +555,13 @@ class OlasServiceImporter:
|
|
|
557
555
|
if safe_owner_address:
|
|
558
556
|
# CASE: Owner is Safe
|
|
559
557
|
service.service_owner_multisig_address = safe_owner_address
|
|
560
|
-
service.service_owner_eoa_address =
|
|
558
|
+
service.service_owner_eoa_address = (
|
|
559
|
+
eoa_address # The EOA is the signer/controller
|
|
560
|
+
)
|
|
561
561
|
|
|
562
|
-
logger.debug(
|
|
562
|
+
logger.debug(
|
|
563
|
+
f"Extracted Safe owner address: {safe_owner_address} (Signer: {eoa_address})"
|
|
564
|
+
)
|
|
563
565
|
elif eoa_address:
|
|
564
566
|
# CASE: Owner is EOA
|
|
565
567
|
service.service_owner_eoa_address = eoa_address
|
|
@@ -767,8 +769,8 @@ class OlasServiceImporter:
|
|
|
767
769
|
safe_result = self._import_safe(
|
|
768
770
|
address=service.safe_address,
|
|
769
771
|
signers=self._get_agent_signers(service),
|
|
770
|
-
tag_suffix="
|
|
771
|
-
service_name=service.service_name
|
|
772
|
+
tag_suffix="multisig", # e.g. trader_zeta_safe
|
|
773
|
+
service_name=service.service_name,
|
|
772
774
|
)
|
|
773
775
|
if safe_result[0]:
|
|
774
776
|
result.imported_safes.append(service.safe_address)
|
|
@@ -778,19 +780,22 @@ class OlasServiceImporter:
|
|
|
778
780
|
result.errors.append(f"Safe {service.safe_address}: {safe_result[1]}")
|
|
779
781
|
|
|
780
782
|
# 2. Import Owner Safe (if it exists and is different)
|
|
781
|
-
if
|
|
782
|
-
|
|
783
|
+
if (
|
|
784
|
+
service.service_owner_multisig_address
|
|
785
|
+
and service.service_owner_multisig_address != service.safe_address
|
|
786
|
+
):
|
|
787
|
+
# Signer for Owner Safe is the EOA owner key
|
|
783
788
|
owner_signers = self._get_owner_signers(service)
|
|
784
789
|
|
|
785
790
|
safe_result = self._import_safe(
|
|
786
791
|
address=service.service_owner_multisig_address,
|
|
787
792
|
signers=owner_signers,
|
|
788
|
-
tag_suffix="
|
|
789
|
-
service_name=service.service_name
|
|
793
|
+
tag_suffix="owner_multisig", # e.g. trader_zeta_owner_safe
|
|
794
|
+
service_name=service.service_name,
|
|
790
795
|
)
|
|
791
796
|
if safe_result[0]:
|
|
792
|
-
|
|
793
|
-
|
|
797
|
+
result.imported_safes.append(service.service_owner_multisig_address)
|
|
798
|
+
logger.info(f"Imported Owner Safe {service.service_owner_multisig_address}")
|
|
794
799
|
|
|
795
800
|
def _get_agent_signers(self, service: DiscoveredService) -> List[str]:
|
|
796
801
|
"""Get list of signers for the agent safe."""
|
|
@@ -878,8 +883,7 @@ class OlasServiceImporter:
|
|
|
878
883
|
self.key_storage._password,
|
|
879
884
|
tag,
|
|
880
885
|
)
|
|
881
|
-
self.key_storage.
|
|
882
|
-
self.key_storage.save()
|
|
886
|
+
self.key_storage.register_account(encrypted)
|
|
883
887
|
logger.info(f"Imported key {key.address} as '{tag}'")
|
|
884
888
|
return True, "ok"
|
|
885
889
|
except Exception as e:
|
|
@@ -926,8 +930,8 @@ class OlasServiceImporter:
|
|
|
926
930
|
self,
|
|
927
931
|
address: str,
|
|
928
932
|
signers: List[str] = None,
|
|
929
|
-
tag_suffix: str = "
|
|
930
|
-
service_name: Optional[str] = None
|
|
933
|
+
tag_suffix: str = "multisig",
|
|
934
|
+
service_name: Optional[str] = None,
|
|
931
935
|
) -> Tuple[bool, str]:
|
|
932
936
|
"""Import a generic Safe."""
|
|
933
937
|
if not address:
|
|
@@ -960,8 +964,7 @@ class OlasServiceImporter:
|
|
|
960
964
|
signers=signers or [],
|
|
961
965
|
)
|
|
962
966
|
|
|
963
|
-
self.key_storage.
|
|
964
|
-
self.key_storage.save()
|
|
967
|
+
self.key_storage.register_account(safe_account)
|
|
965
968
|
logger.info(f"Imported Safe {address} as '{tag}'")
|
|
966
969
|
return True, "ok"
|
|
967
970
|
|
|
@@ -1064,4 +1067,5 @@ class OlasServiceImporter:
|
|
|
1064
1067
|
key.signature_failed = True
|
|
1065
1068
|
logger.warning(f"Error verifying signature for key {key.address}: {e}")
|
|
1066
1069
|
|
|
1067
|
-
|
|
1070
|
+
|
|
1071
|
+
FLAGS_OWNER_SAFE = "deprecated"
|
iwa/plugins/olas/models.py
CHANGED
|
@@ -134,8 +134,5 @@ class OlasConfig(BaseModel):
|
|
|
134
134
|
target = multisig_address.lower()
|
|
135
135
|
for service in self.services.values():
|
|
136
136
|
if service.multisig_address and str(service.multisig_address).lower() == target:
|
|
137
|
-
# The following line is from the Code Edit, but it does not fit syntactically here.
|
|
138
|
-
# It appears to be from a different file (decoder.py) as indicated by the instruction.
|
|
139
|
-
# args_str = ", ".join(f"{n}={v}" for n, v in zip(d["arg_names"], decoded, strict=False))
|
|
140
137
|
return service
|
|
141
138
|
return None
|
iwa/plugins/olas/plugin.py
CHANGED
|
@@ -151,7 +151,9 @@ class OlasPlugin(Plugin):
|
|
|
151
151
|
|
|
152
152
|
is_signer = key_addr in [s.lower() for s in on_chain_signers]
|
|
153
153
|
if not is_signer:
|
|
154
|
-
safe_text +=
|
|
154
|
+
safe_text += (
|
|
155
|
+
f"\n[bold red]⚠ Agent {agent_key.address} - NOT A SIGNER![/bold red]"
|
|
156
|
+
)
|
|
155
157
|
else:
|
|
156
158
|
safe_text += f" (Signer: {agent_key.address[:6]}...)"
|
|
157
159
|
|
|
@@ -182,21 +184,21 @@ class OlasPlugin(Plugin):
|
|
|
182
184
|
# 1. Display Signer/EOA Owner
|
|
183
185
|
owner_key = next((k for k in service.keys if k.role == "owner"), None)
|
|
184
186
|
if owner_key:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
187
|
+
val = owner_key.address
|
|
188
|
+
if not val.startswith("0x"):
|
|
189
|
+
val = "0x" + val
|
|
190
|
+
|
|
191
|
+
if owner_key.signature_verified:
|
|
192
|
+
val = f"[green]{val}[/green]"
|
|
193
|
+
elif not owner_key.is_encrypted:
|
|
194
|
+
val = f"[red]{val}[/red]"
|
|
195
|
+
status = "🔒 encrypted" if owner_key.is_encrypted else "🔓 plaintext"
|
|
196
|
+
table.add_row("Owner (EOA)", f"{val} {status}")
|
|
195
197
|
elif service.service_owner_eoa_address:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
# Fallback if we have an address but no key object
|
|
199
|
+
table.add_row("Owner (EOA)", service.service_owner_eoa_address)
|
|
198
200
|
else:
|
|
199
|
-
|
|
201
|
+
table.add_row("Owner (EOA)", "[yellow]N/A[/yellow]")
|
|
200
202
|
|
|
201
203
|
# 2. Display Safe Owner
|
|
202
204
|
if service.service_owner_multisig_address:
|
|
@@ -107,12 +107,18 @@ class DrainManagerMixin:
|
|
|
107
107
|
logger.error("Service has no multisig address")
|
|
108
108
|
return False, 0
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
withdrawal_address = (
|
|
111
|
+
str(self.olas_config.withdrawal_address)
|
|
112
|
+
if self.olas_config.withdrawal_address
|
|
113
|
+
else str(self.wallet.master_account.address)
|
|
114
|
+
)
|
|
114
115
|
multisig_address = str(self.service.multisig_address)
|
|
115
|
-
|
|
116
|
+
|
|
117
|
+
if not self.olas_config.withdrawal_address:
|
|
118
|
+
logger.warning(
|
|
119
|
+
"No withdrawal address configured. Defaulting to master account: "
|
|
120
|
+
f"{withdrawal_address}"
|
|
121
|
+
)
|
|
116
122
|
|
|
117
123
|
# Get OLAS balance of the Safe
|
|
118
124
|
olas_token = ERC20Contract(
|
|
@@ -126,9 +132,10 @@ class DrainManagerMixin:
|
|
|
126
132
|
return False, 0
|
|
127
133
|
|
|
128
134
|
olas_amount = olas_balance / 1e18
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
withdrawal_tag = self.wallet.get_tag_by_address(withdrawal_address) or withdrawal_address
|
|
136
|
+
multisig_tag = self.wallet.get_tag_by_address(multisig_address) or multisig_address
|
|
137
|
+
|
|
138
|
+
logger.info(f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}")
|
|
132
139
|
|
|
133
140
|
# Transfer from Safe to withdrawal address
|
|
134
141
|
tx_hash = self.wallet.send(
|
|
@@ -143,7 +150,7 @@ class DrainManagerMixin:
|
|
|
143
150
|
logger.error("Failed to transfer OLAS")
|
|
144
151
|
return False, 0
|
|
145
152
|
|
|
146
|
-
logger.info(f"Withdrew {olas_amount:.4f} OLAS to {
|
|
153
|
+
logger.info(f"Withdrew {olas_amount:.4f} OLAS to {withdrawal_tag}")
|
|
147
154
|
return True, olas_amount
|
|
148
155
|
|
|
149
156
|
def drain_service(
|
|
@@ -429,9 +429,7 @@ class LifecycleManagerMixin:
|
|
|
429
429
|
)
|
|
430
430
|
|
|
431
431
|
if balance < bond_amount:
|
|
432
|
-
logger.error(
|
|
433
|
-
f"[ACTIVATE] FAIL: Owner balance {balance} < required {bond_amount}"
|
|
434
|
-
)
|
|
432
|
+
logger.error(f"[ACTIVATE] FAIL: Owner balance {balance} < required {bond_amount}")
|
|
435
433
|
return False
|
|
436
434
|
|
|
437
435
|
protocol_contracts = OLAS_CONTRACTS.get(self.chain_name.lower(), {})
|
|
@@ -632,7 +630,7 @@ class LifecycleManagerMixin:
|
|
|
632
630
|
# Use service_name for consistency with Safe naming
|
|
633
631
|
agent_tag = f"{self.service.service_name}_agent"
|
|
634
632
|
try:
|
|
635
|
-
agent_account = self.wallet.key_storage.
|
|
633
|
+
agent_account = self.wallet.key_storage.generate_new_account(agent_tag)
|
|
636
634
|
agent_account_address = agent_account.address
|
|
637
635
|
logger.info(f"Created new agent account: {agent_account_address}")
|
|
638
636
|
|
|
@@ -682,7 +680,9 @@ class LifecycleManagerMixin:
|
|
|
682
680
|
)
|
|
683
681
|
return True
|
|
684
682
|
|
|
685
|
-
logger.info(
|
|
683
|
+
logger.info(
|
|
684
|
+
f"[REGISTER] Service Owner approving Token Utility for bond: {bond_amount_wei} wei"
|
|
685
|
+
)
|
|
686
686
|
|
|
687
687
|
utility_address = str(
|
|
688
688
|
OLAS_CONTRACTS[self.chain_name]["OLAS_SERVICE_REGISTRY_TOKEN_UTILITY"]
|
|
@@ -697,9 +697,7 @@ class LifecycleManagerMixin:
|
|
|
697
697
|
)
|
|
698
698
|
|
|
699
699
|
if allowance >= bond_amount_wei:
|
|
700
|
-
logger.debug(
|
|
701
|
-
f"[REGISTER] Sufficient allowance ({allowance} >= {bond_amount_wei})"
|
|
702
|
-
)
|
|
700
|
+
logger.debug(f"[REGISTER] Sufficient allowance ({allowance} >= {bond_amount_wei})")
|
|
703
701
|
return True
|
|
704
702
|
|
|
705
703
|
# Use service owner which holds the OLAS tokens (not necessarily master)
|
|
@@ -800,7 +798,9 @@ class LifecycleManagerMixin:
|
|
|
800
798
|
)
|
|
801
799
|
return False
|
|
802
800
|
|
|
803
|
-
logger.debug(
|
|
801
|
+
logger.debug(
|
|
802
|
+
f"[DEPLOY] Preparing deploy tx for owner {self._get_label(self.service.service_owner_address)}"
|
|
803
|
+
)
|
|
804
804
|
deploy_tx = self.manager.prepare_deploy_tx(
|
|
805
805
|
from_address=self.service.service_owner_address,
|
|
806
806
|
service_id=self.service.service_id,
|
|
@@ -855,16 +855,27 @@ class LifecycleManagerMixin:
|
|
|
855
855
|
_, agent_instances = self.registry.call("getAgentInstances", self.service.service_id)
|
|
856
856
|
service_info = self.registry.get_service(self.service.service_id)
|
|
857
857
|
threshold = service_info["threshold"]
|
|
858
|
+
# Store the multisig in the wallet with tag
|
|
859
|
+
multisig_tag = f"{self.service.service_name}_multisig"
|
|
860
|
+
|
|
861
|
+
# ARCHIVING LOGIC: If tag is already taken by a different address, rename the old one
|
|
862
|
+
existing = self.wallet.key_storage.find_stored_account(multisig_tag)
|
|
863
|
+
if existing and existing.address != multisig_address:
|
|
864
|
+
archive_tag = f"{multisig_tag}_old_{existing.address[:6]}"
|
|
865
|
+
logger.info(f"[DEPLOY] Archiving old multisig: {multisig_tag} -> {archive_tag}")
|
|
866
|
+
try:
|
|
867
|
+
self.wallet.key_storage.rename_account(existing.address, archive_tag)
|
|
868
|
+
except Exception as ex:
|
|
869
|
+
logger.warning(f"[DEPLOY] Failed to rename old multisig (collision?): {ex}")
|
|
858
870
|
|
|
859
871
|
safe_account = StoredSafeAccount(
|
|
860
|
-
tag=
|
|
872
|
+
tag=multisig_tag,
|
|
861
873
|
address=multisig_address,
|
|
862
874
|
chains=[self.chain_name],
|
|
863
875
|
threshold=threshold,
|
|
864
876
|
signers=agent_instances,
|
|
865
877
|
)
|
|
866
|
-
self.wallet.key_storage.
|
|
867
|
-
self.wallet.key_storage.save()
|
|
878
|
+
self.wallet.key_storage.register_account(safe_account)
|
|
868
879
|
logger.debug("[DEPLOY] Registered multisig in wallet")
|
|
869
880
|
except Exception as e:
|
|
870
881
|
logger.warning(f"[DEPLOY] Failed to register multisig in wallet: {e}")
|
|
@@ -90,6 +90,7 @@ class StakingManagerMixin:
|
|
|
90
90
|
# Try token/contract names
|
|
91
91
|
try:
|
|
92
92
|
from iwa.core.chain import ChainInterfaces
|
|
93
|
+
|
|
93
94
|
chain_interface = ChainInterfaces().get(self.chain_name)
|
|
94
95
|
token_name = chain_interface.chain.get_token_name(address)
|
|
95
96
|
if token_name:
|
|
@@ -211,14 +212,14 @@ class StakingManagerMixin:
|
|
|
211
212
|
# Helper to safely get min_staking_duration
|
|
212
213
|
try:
|
|
213
214
|
min_duration = staking.min_staking_duration
|
|
214
|
-
logger.
|
|
215
|
+
logger.debug(f"min_staking_duration: {min_duration}")
|
|
215
216
|
except Exception as e:
|
|
216
217
|
logger.error(f"Failed to get min_staking_duration: {e}")
|
|
217
218
|
min_duration = 0
|
|
218
219
|
|
|
219
220
|
unstake_at = None
|
|
220
221
|
ts_start = info.get("ts_start", 0)
|
|
221
|
-
logger.
|
|
222
|
+
logger.debug(f"ts_start: {ts_start}")
|
|
222
223
|
|
|
223
224
|
if ts_start > 0:
|
|
224
225
|
try:
|
|
@@ -227,12 +228,12 @@ class StakingManagerMixin:
|
|
|
227
228
|
unstake_ts,
|
|
228
229
|
tz=timezone.utc,
|
|
229
230
|
).isoformat()
|
|
230
|
-
logger.
|
|
231
|
+
logger.debug(f"unstake_available_at: {unstake_at} (ts={unstake_ts})")
|
|
231
232
|
except Exception as e:
|
|
232
233
|
logger.error(f"calc error: {e}")
|
|
233
234
|
pass
|
|
234
235
|
else:
|
|
235
|
-
logger.
|
|
236
|
+
logger.debug("ts_start is 0, cannot calculate unstake time")
|
|
236
237
|
|
|
237
238
|
return unstake_at, ts_start, min_duration
|
|
238
239
|
|
|
@@ -407,7 +408,9 @@ class StakingManagerMixin:
|
|
|
407
408
|
# NOTE: We don't check OLAS balance here because OLAS was already
|
|
408
409
|
# deposited to the Token Utility during activation (min_staking_deposit)
|
|
409
410
|
# and registration (agent_bond). The staking contract pulls from there.
|
|
410
|
-
logger.debug(
|
|
411
|
+
logger.debug(
|
|
412
|
+
"[STAKE] OLAS already deposited to Token Utility during activation/registration"
|
|
413
|
+
)
|
|
411
414
|
|
|
412
415
|
return {"min_deposit": min_deposit, "staking_token": staking_token}
|
|
413
416
|
|
|
@@ -437,7 +440,9 @@ class StakingManagerMixin:
|
|
|
437
440
|
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
438
441
|
|
|
439
442
|
# Approve service NFT - this is an ERC-721 approval, not ERC-20
|
|
440
|
-
logger.debug(
|
|
443
|
+
logger.debug(
|
|
444
|
+
f"[STAKE] Approving service NFT for staking contract from {self._get_label(owner_address)}..."
|
|
445
|
+
)
|
|
441
446
|
approve_tx = self.registry.prepare_approve_tx(
|
|
442
447
|
from_address=owner_address,
|
|
443
448
|
spender=staking_contract.address,
|
|
@@ -483,7 +488,9 @@ class StakingManagerMixin:
|
|
|
483
488
|
# Use service owner which holds the NFT (not necessarily master)
|
|
484
489
|
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
485
490
|
|
|
486
|
-
logger.debug(
|
|
491
|
+
logger.debug(
|
|
492
|
+
f"[STAKE] Preparing stake transaction from {self._get_label(owner_address)}..."
|
|
493
|
+
)
|
|
487
494
|
stake_tx = staking_contract.prepare_stake_tx(
|
|
488
495
|
from_address=owner_address,
|
|
489
496
|
service_id=self.service.service_id,
|
|
@@ -601,9 +608,7 @@ class StakingManagerMixin:
|
|
|
601
608
|
return False
|
|
602
609
|
|
|
603
610
|
tx_hash = get_tx_hash(receipt)
|
|
604
|
-
logger.info(
|
|
605
|
-
f"Unstake transaction sent: {tx_hash if receipt else 'No Receipt'}"
|
|
606
|
-
)
|
|
611
|
+
logger.info(f"Unstake transaction sent: {tx_hash if receipt else 'No Receipt'}")
|
|
607
612
|
|
|
608
613
|
events = staking_contract.extract_events(receipt)
|
|
609
614
|
|
|
@@ -296,6 +296,12 @@ def test_import_key_success(importer):
|
|
|
296
296
|
importer.key_storage.find_stored_account.return_value = None
|
|
297
297
|
importer.key_storage.accounts = {}
|
|
298
298
|
|
|
299
|
+
# Define side effect to update accounts dict when register_account is called
|
|
300
|
+
def mock_register(acc):
|
|
301
|
+
importer.key_storage.accounts[acc.address] = acc
|
|
302
|
+
|
|
303
|
+
importer.key_storage.register_account.side_effect = mock_register
|
|
304
|
+
|
|
299
305
|
with patch("iwa.core.keys.EncryptedAccount.encrypt_private_key") as mock_enc:
|
|
300
306
|
mock_enc.return_value = MagicMock(address=addr)
|
|
301
307
|
success, msg = importer._import_key(key, "my_service")
|
|
@@ -310,10 +316,17 @@ def test_import_safe_success(importer):
|
|
|
310
316
|
importer.key_storage.find_stored_account.return_value = None
|
|
311
317
|
importer.key_storage.accounts = {}
|
|
312
318
|
|
|
319
|
+
# Define side effect to update accounts dict when register_account is called
|
|
320
|
+
def mock_register(acc):
|
|
321
|
+
importer.key_storage.accounts[acc.address] = acc
|
|
322
|
+
|
|
323
|
+
importer.key_storage.register_account.side_effect = mock_register
|
|
324
|
+
|
|
313
325
|
success, msg = importer._import_safe(safe_address, service_name="my_service")
|
|
314
326
|
assert success is True
|
|
315
327
|
assert msg == "ok"
|
|
316
328
|
assert safe_address in importer.key_storage.accounts
|
|
329
|
+
assert importer.key_storage.accounts[safe_address].tag == "my_service_multisig"
|
|
317
330
|
|
|
318
331
|
|
|
319
332
|
def test_import_service_config_success(importer):
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Tests for Olas multisig archiving logic."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
from iwa.core.models import StoredAccount
|
|
7
|
+
from iwa.plugins.olas.contracts.service import ServiceState
|
|
8
|
+
from iwa.plugins.olas.models import Service
|
|
9
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestOlasArchiving(unittest.TestCase):
|
|
13
|
+
"""Test multisig archiving logic during deployment."""
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
"""Set up test fixtures."""
|
|
17
|
+
self.mock_wallet = MagicMock()
|
|
18
|
+
# Mock KeyStorage
|
|
19
|
+
self.mock_key_storage = MagicMock()
|
|
20
|
+
self.mock_wallet.key_storage = self.mock_key_storage
|
|
21
|
+
|
|
22
|
+
# Initialize ServiceManager
|
|
23
|
+
with (
|
|
24
|
+
patch("iwa.core.models.Config"),
|
|
25
|
+
patch("iwa.plugins.olas.service_manager.base.ChainInterfaces"),
|
|
26
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache"),
|
|
27
|
+
patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract"),
|
|
28
|
+
):
|
|
29
|
+
self.sm = ServiceManager(self.mock_wallet)
|
|
30
|
+
self.sm.service = Service(service_name="trader_psi", chain_name="gnosis", service_id=1)
|
|
31
|
+
self.sm.chain_name = "gnosis"
|
|
32
|
+
|
|
33
|
+
def test_multisig_archiving_logic(self):
|
|
34
|
+
"""Test that old multisig is archived if tag is taken by different address."""
|
|
35
|
+
multisig_tag = "trader_psi_multisig"
|
|
36
|
+
old_address = "0x1111111111111111111111111111111111111111"
|
|
37
|
+
new_address = "0x2222222222222222222222222222222222222222"
|
|
38
|
+
|
|
39
|
+
# 1. Simulate existing multisig in wallet with same tag
|
|
40
|
+
existing_acc = StoredAccount(address=old_address, tag=multisig_tag)
|
|
41
|
+
self.mock_key_storage.find_stored_account.return_value = existing_acc
|
|
42
|
+
|
|
43
|
+
# 2. Mock required contract calls for deploy()
|
|
44
|
+
self.sm.registry.get_service.return_value = {
|
|
45
|
+
"state": ServiceState.FINISHED_REGISTRATION, # DEPLOYED
|
|
46
|
+
"security_deposit": 0,
|
|
47
|
+
"multisig": new_address,
|
|
48
|
+
"threshold": 1,
|
|
49
|
+
"configHash": b"\x00" * 32,
|
|
50
|
+
}
|
|
51
|
+
self.sm.registry.call.return_value = (None, []) # getAgentInstances
|
|
52
|
+
|
|
53
|
+
# 3. Trigger deploy (archiving happens here)
|
|
54
|
+
with (
|
|
55
|
+
patch.object(
|
|
56
|
+
self.mock_wallet, "sign_and_send_transaction", return_value=(True, {"status": 1})
|
|
57
|
+
),
|
|
58
|
+
patch.object(
|
|
59
|
+
self.sm.registry,
|
|
60
|
+
"extract_events",
|
|
61
|
+
return_value=[
|
|
62
|
+
{"name": "DeployService", "args": {}},
|
|
63
|
+
{"name": "CreateMultisigWithAgents", "args": {"multisig": new_address}},
|
|
64
|
+
],
|
|
65
|
+
),
|
|
66
|
+
patch("iwa.plugins.olas.service_manager.lifecycle.get_tx_hash", return_value="0xhash"),
|
|
67
|
+
patch("iwa.core.models.StoredSafeAccount") as mock_safe_cls,
|
|
68
|
+
):
|
|
69
|
+
self.sm.deploy(fund_multisig=False)
|
|
70
|
+
|
|
71
|
+
# 4. Verify rename_account was called for the old address
|
|
72
|
+
archive_tag = f"{multisig_tag}_old_{old_address[:6]}"
|
|
73
|
+
self.mock_key_storage.rename_account.assert_called_with(old_address, archive_tag)
|
|
74
|
+
|
|
75
|
+
# 5. Verify new account constructor was called with correct params
|
|
76
|
+
mock_safe_cls.assert_called()
|
|
77
|
+
_, kwargs = mock_safe_cls.call_args
|
|
78
|
+
self.assertEqual(kwargs["address"], new_address)
|
|
79
|
+
self.assertEqual(kwargs["tag"], multisig_tag)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
unittest.main()
|
|
@@ -309,9 +309,11 @@ def test_importer_import_service_config_duplicate(mock_wallet):
|
|
|
309
309
|
|
|
310
310
|
def test_sm_create_token_utility_missing(mock_wallet):
|
|
311
311
|
"""Cover create() with missing token utility (lines 204-206)."""
|
|
312
|
-
with
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
with (
|
|
313
|
+
patch("iwa.core.models.Config"),
|
|
314
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
315
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
316
|
+
):
|
|
315
317
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
316
318
|
sm = ServiceManager(mock_wallet)
|
|
317
319
|
|
|
@@ -323,9 +325,11 @@ def test_sm_create_token_utility_missing(mock_wallet):
|
|
|
323
325
|
|
|
324
326
|
def test_sm_get_staking_status_staked_info_fail(mock_wallet):
|
|
325
327
|
"""Cover get_staking_status with STAKED but get_service_info fails (lines 843-854)."""
|
|
326
|
-
with
|
|
327
|
-
|
|
328
|
-
|
|
328
|
+
with (
|
|
329
|
+
patch("iwa.core.models.Config"),
|
|
330
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
331
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
332
|
+
):
|
|
329
333
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
330
334
|
sm = ServiceManager(mock_wallet)
|
|
331
335
|
sm.service = Service(
|
|
@@ -347,9 +351,11 @@ def test_sm_get_staking_status_staked_info_fail(mock_wallet):
|
|
|
347
351
|
|
|
348
352
|
def test_sm_call_checkpoint_prepare_fail(mock_wallet):
|
|
349
353
|
"""Cover call_checkpoint prepare failure (lines 1100-1102)."""
|
|
350
|
-
with
|
|
351
|
-
|
|
352
|
-
|
|
354
|
+
with (
|
|
355
|
+
patch("iwa.core.models.Config"),
|
|
356
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
357
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
358
|
+
):
|
|
353
359
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
354
360
|
sm = ServiceManager(mock_wallet)
|
|
355
361
|
sm.service = Service(
|
|
@@ -369,9 +375,11 @@ def test_sm_call_checkpoint_prepare_fail(mock_wallet):
|
|
|
369
375
|
|
|
370
376
|
def test_sm_spin_up_no_service(mock_wallet):
|
|
371
377
|
"""Cover spin_up with no service (lines 1167-1170)."""
|
|
372
|
-
with
|
|
373
|
-
|
|
374
|
-
|
|
378
|
+
with (
|
|
379
|
+
patch("iwa.core.models.Config"),
|
|
380
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
381
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
382
|
+
):
|
|
375
383
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
376
384
|
sm = ServiceManager(mock_wallet)
|
|
377
385
|
sm.service = None
|
|
@@ -382,9 +390,11 @@ def test_sm_spin_up_no_service(mock_wallet):
|
|
|
382
390
|
|
|
383
391
|
def test_sm_spin_up_activation_fail(mock_wallet):
|
|
384
392
|
"""Cover spin_up activation failure (lines 1181-1183)."""
|
|
385
|
-
with
|
|
386
|
-
|
|
387
|
-
|
|
393
|
+
with (
|
|
394
|
+
patch("iwa.core.models.Config"),
|
|
395
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
396
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
397
|
+
):
|
|
388
398
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
389
399
|
sm = ServiceManager(mock_wallet)
|
|
390
400
|
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
@@ -402,9 +412,11 @@ def test_sm_spin_up_activation_fail(mock_wallet):
|
|
|
402
412
|
|
|
403
413
|
def test_sm_wind_down_no_service(mock_wallet):
|
|
404
414
|
"""Cover wind_down with no service (lines 1264-1266)."""
|
|
405
|
-
with
|
|
406
|
-
|
|
407
|
-
|
|
415
|
+
with (
|
|
416
|
+
patch("iwa.core.models.Config"),
|
|
417
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
418
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
419
|
+
):
|
|
408
420
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
409
421
|
sm = ServiceManager(mock_wallet)
|
|
410
422
|
sm.service = None
|
|
@@ -415,9 +427,11 @@ def test_sm_wind_down_no_service(mock_wallet):
|
|
|
415
427
|
|
|
416
428
|
def test_sm_wind_down_nonexistent(mock_wallet):
|
|
417
429
|
"""Cover wind_down with non-existent service (lines 1274-1276)."""
|
|
418
|
-
with
|
|
419
|
-
|
|
420
|
-
|
|
430
|
+
with (
|
|
431
|
+
patch("iwa.core.models.Config"),
|
|
432
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
433
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
434
|
+
):
|
|
421
435
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
422
436
|
sm = ServiceManager(mock_wallet)
|
|
423
437
|
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
@@ -432,9 +446,11 @@ def test_sm_wind_down_nonexistent(mock_wallet):
|
|
|
432
446
|
|
|
433
447
|
def test_sm_mech_request_no_service(mock_wallet):
|
|
434
448
|
"""Cover _send_legacy_mech_request with no service (lines 1502-1504)."""
|
|
435
|
-
with
|
|
436
|
-
|
|
437
|
-
|
|
449
|
+
with (
|
|
450
|
+
patch("iwa.core.models.Config"),
|
|
451
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
452
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
453
|
+
):
|
|
438
454
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
439
455
|
sm = ServiceManager(mock_wallet)
|
|
440
456
|
sm.service = None
|
|
@@ -445,9 +461,11 @@ def test_sm_mech_request_no_service(mock_wallet):
|
|
|
445
461
|
|
|
446
462
|
def test_sm_mech_request_no_address(mock_wallet):
|
|
447
463
|
"""Cover _send_legacy_mech_request missing mech address (lines 1510-1512)."""
|
|
448
|
-
with
|
|
449
|
-
|
|
450
|
-
|
|
464
|
+
with (
|
|
465
|
+
patch("iwa.core.models.Config"),
|
|
466
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache,
|
|
467
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache),
|
|
468
|
+
):
|
|
451
469
|
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
452
470
|
sm = ServiceManager(mock_wallet)
|
|
453
471
|
sm.service = Service(service_name="t", chain_name="unknown", service_id=1)
|
|
@@ -458,8 +476,10 @@ def test_sm_mech_request_no_address(mock_wallet):
|
|
|
458
476
|
|
|
459
477
|
def test_sm_marketplace_mech_no_service(mock_wallet):
|
|
460
478
|
"""Cover _send_marketplace_mech_request with no service (lines 1549-1551)."""
|
|
461
|
-
with
|
|
462
|
-
|
|
479
|
+
with (
|
|
480
|
+
patch("iwa.core.models.Config"),
|
|
481
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache"),
|
|
482
|
+
):
|
|
463
483
|
sm = ServiceManager(mock_wallet)
|
|
464
484
|
sm.service = None
|
|
465
485
|
|