iwa 0.0.62__py3-none-any.whl → 0.0.64__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/plugins/olas/models.py +5 -22
- iwa/plugins/olas/service_manager/drain.py +34 -12
- iwa/plugins/olas/service_manager/lifecycle.py +3 -3
- iwa/plugins/olas/tests/test_olas_models.py +5 -5
- iwa/plugins/olas/tests/test_olas_view_actions.py +1 -1
- iwa/plugins/olas/tests/test_service_manager_rewards.py +6 -1
- iwa/plugins/olas/tests/test_service_staking.py +1 -0
- iwa/web/tests/test_web_olas.py +1 -1
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/METADATA +1 -1
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/RECORD +18 -17
- tests/test_chainlist_enrichment.py +233 -0
- tests/test_contract_cache.py +253 -0
- tests/test_drain_coverage.py +265 -3
- tests/test_staking_simple.py +478 -6
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/WHEEL +0 -0
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.62.dist-info → iwa-0.0.64.dist-info}/top_level.txt +0 -0
iwa/plugins/olas/models.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Dict, List, Optional
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
7
|
from iwa.core.models import EthereumAddress
|
|
8
8
|
|
|
@@ -15,37 +15,20 @@ class Service(BaseModel):
|
|
|
15
15
|
service_id: int # Unique per chain
|
|
16
16
|
agent_ids: List[int] = Field(default_factory=list) # List of agent type IDs
|
|
17
17
|
|
|
18
|
-
#
|
|
18
|
+
# Owner fields:
|
|
19
|
+
# - service_owner_eoa_address: EOA that owns the service (or signs for the multisig)
|
|
20
|
+
# - service_owner_multisig_address: Safe multisig owner (optional, if service is owned by a Safe)
|
|
19
21
|
service_owner_eoa_address: Optional[EthereumAddress] = None
|
|
20
22
|
service_owner_multisig_address: Optional[EthereumAddress] = None
|
|
21
23
|
|
|
22
|
-
# Deprecated fields (kept for migration, removed from physical model via aliasing/validation)
|
|
23
|
-
# Actually, we keep it optional but not used, or use migration logic.
|
|
24
|
-
# Let's remove it from fields and rely on before validator to map it to eoa.
|
|
25
|
-
|
|
26
24
|
agent_address: Optional[EthereumAddress] = None
|
|
27
25
|
multisig_address: Optional[EthereumAddress] = None
|
|
28
26
|
staking_contract_address: Optional[EthereumAddress] = None
|
|
29
27
|
token_address: Optional[EthereumAddress] = None
|
|
30
28
|
|
|
31
|
-
@root_validator(pre=True)
|
|
32
|
-
def migrate_owner_fields(cls, values): # noqa: N805
|
|
33
|
-
"""Migrate legacy service_owner_address to service_owner_eoa_address."""
|
|
34
|
-
# Check for legacy 'service_owner_address'
|
|
35
|
-
if "service_owner_address" in values and values["service_owner_address"]:
|
|
36
|
-
legacy_addr = values["service_owner_address"]
|
|
37
|
-
|
|
38
|
-
# If service_owner_eoa_address is missing, use legacy
|
|
39
|
-
if "service_owner_eoa_address" not in values or not values["service_owner_eoa_address"]:
|
|
40
|
-
values["service_owner_eoa_address"] = legacy_addr
|
|
41
|
-
|
|
42
|
-
# Remove legacy field from values so it doesn't cause extra field errors if we removed it from model
|
|
43
|
-
# Or if strict.
|
|
44
|
-
return values
|
|
45
|
-
|
|
46
29
|
@property
|
|
47
30
|
def service_owner_address(self) -> Optional[EthereumAddress]:
|
|
48
|
-
"""
|
|
31
|
+
"""Returns effective owner address (Safe multisig if present, else EOA)."""
|
|
49
32
|
return self.service_owner_multisig_address or self.service_owner_eoa_address
|
|
50
33
|
|
|
51
34
|
@property
|
|
@@ -12,7 +12,9 @@ from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
|
|
|
12
12
|
class DrainManagerMixin:
|
|
13
13
|
"""Mixin for draining and service token management."""
|
|
14
14
|
|
|
15
|
-
def claim_rewards(
|
|
15
|
+
def claim_rewards( # noqa: C901
|
|
16
|
+
self, staking_contract: Optional[StakingContract] = None
|
|
17
|
+
) -> Tuple[bool, int]:
|
|
16
18
|
"""Claim staking rewards for the active service.
|
|
17
19
|
|
|
18
20
|
The claimed OLAS tokens will be sent to the service's multisig (Safe).
|
|
@@ -51,13 +53,19 @@ class DrainManagerMixin:
|
|
|
51
53
|
logger.info("Service not staked, skipping claim")
|
|
52
54
|
return False, 0
|
|
53
55
|
|
|
54
|
-
# Check
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
# Check claimable rewards using calculate_staking_reward for accurate value
|
|
57
|
+
# (get_accrued_rewards returns stored value which may be outdated)
|
|
58
|
+
try:
|
|
59
|
+
claimable_rewards = staking_contract.calculate_staking_reward(service_id)
|
|
60
|
+
except Exception:
|
|
61
|
+
# Fallback to stored value if calculation fails
|
|
62
|
+
claimable_rewards = staking_contract.get_accrued_rewards(service_id)
|
|
63
|
+
|
|
64
|
+
if claimable_rewards == 0:
|
|
65
|
+
logger.info("No rewards to claim")
|
|
58
66
|
return False, 0
|
|
59
67
|
|
|
60
|
-
logger.info(f"Claiming {
|
|
68
|
+
logger.info(f"Claiming ~{claimable_rewards / 1e18:.4f} OLAS rewards for service {service_id}")
|
|
61
69
|
|
|
62
70
|
# Use service owner which holds the reward rights (not necessarily master)
|
|
63
71
|
owner_address = self.service.service_owner_address or self.wallet.master_account.address
|
|
@@ -83,11 +91,19 @@ class DrainManagerMixin:
|
|
|
83
91
|
return False, 0
|
|
84
92
|
|
|
85
93
|
events = staking_contract.extract_events(receipt)
|
|
86
|
-
if "RewardClaimed" not in [event["name"] for event in events]:
|
|
87
|
-
logger.warning("RewardClaimed event not found, but transaction succeeded")
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
# Extract actual claimed amount from RewardClaimed event
|
|
96
|
+
claimed_amount = claimable_rewards # Default to estimated
|
|
97
|
+
for event in events:
|
|
98
|
+
if event["name"] == "RewardClaimed":
|
|
99
|
+
# RewardClaimed event has 'amount' or 'reward' field
|
|
100
|
+
claimed_amount = event["args"].get("amount", event["args"].get("reward", claimed_amount))
|
|
101
|
+
break
|
|
102
|
+
else:
|
|
103
|
+
logger.warning("RewardClaimed event not found, using estimated amount")
|
|
104
|
+
|
|
105
|
+
logger.info(f"Successfully claimed {claimed_amount / 1e18:.4f} OLAS rewards")
|
|
106
|
+
return True, claimed_amount
|
|
91
107
|
|
|
92
108
|
def withdraw_rewards(self) -> Tuple[bool, float]:
|
|
93
109
|
"""Withdraw OLAS from the service Safe to the configured withdrawal address.
|
|
@@ -132,8 +148,14 @@ class DrainManagerMixin:
|
|
|
132
148
|
return False, 0
|
|
133
149
|
|
|
134
150
|
olas_amount = olas_balance / 1e18
|
|
135
|
-
withdrawal_tag =
|
|
136
|
-
|
|
151
|
+
withdrawal_tag = (
|
|
152
|
+
self.wallet.account_service.get_tag_by_address(withdrawal_address)
|
|
153
|
+
or withdrawal_address
|
|
154
|
+
)
|
|
155
|
+
multisig_tag = (
|
|
156
|
+
self.wallet.account_service.get_tag_by_address(multisig_address)
|
|
157
|
+
or multisig_address
|
|
158
|
+
)
|
|
137
159
|
|
|
138
160
|
logger.info(f"Withdrawing {olas_amount:.4f} OLAS from {multisig_tag} to {withdrawal_tag}")
|
|
139
161
|
|
|
@@ -183,7 +183,7 @@ class LifecycleManagerMixin:
|
|
|
183
183
|
service_name=service_name,
|
|
184
184
|
chain_name=chain_name,
|
|
185
185
|
agent_id_values=agent_id_values,
|
|
186
|
-
|
|
186
|
+
service_owner_eoa_address=service_owner_account.address,
|
|
187
187
|
token_address=token_address,
|
|
188
188
|
)
|
|
189
189
|
|
|
@@ -263,7 +263,7 @@ class LifecycleManagerMixin:
|
|
|
263
263
|
service_name: Optional[str],
|
|
264
264
|
chain_name: str,
|
|
265
265
|
agent_id_values: List[int],
|
|
266
|
-
|
|
266
|
+
service_owner_eoa_address: str,
|
|
267
267
|
token_address: Optional[str],
|
|
268
268
|
) -> None:
|
|
269
269
|
"""Create and save the new Service model."""
|
|
@@ -272,7 +272,7 @@ class LifecycleManagerMixin:
|
|
|
272
272
|
chain_name=chain_name,
|
|
273
273
|
service_id=service_id,
|
|
274
274
|
agent_ids=agent_id_values,
|
|
275
|
-
|
|
275
|
+
service_owner_eoa_address=service_owner_eoa_address,
|
|
276
276
|
token_address=token_address,
|
|
277
277
|
)
|
|
278
278
|
|
|
@@ -14,7 +14,7 @@ class TestOlasConfig:
|
|
|
14
14
|
chain_name="gnosis",
|
|
15
15
|
service_id=456,
|
|
16
16
|
agent_ids=[25],
|
|
17
|
-
|
|
17
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
config.add_service(service)
|
|
@@ -30,7 +30,7 @@ class TestOlasConfig:
|
|
|
30
30
|
chain_name="gnosis",
|
|
31
31
|
service_id=789,
|
|
32
32
|
agent_ids=[25],
|
|
33
|
-
|
|
33
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
34
34
|
)
|
|
35
35
|
config.services["gnosis:789"] = service
|
|
36
36
|
|
|
@@ -53,7 +53,7 @@ class TestOlasConfig:
|
|
|
53
53
|
chain_name="ethereum",
|
|
54
54
|
service_id=200,
|
|
55
55
|
agent_ids=[25],
|
|
56
|
-
|
|
56
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
57
57
|
)
|
|
58
58
|
config.services["ethereum:200"] = service
|
|
59
59
|
|
|
@@ -116,7 +116,7 @@ class TestService:
|
|
|
116
116
|
chain_name="gnosis",
|
|
117
117
|
service_id=123,
|
|
118
118
|
agent_ids=[25],
|
|
119
|
-
|
|
119
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
assert service.key == "gnosis:123"
|
|
@@ -133,7 +133,7 @@ class TestService:
|
|
|
133
133
|
chain_name="gnosis",
|
|
134
134
|
service_id=456,
|
|
135
135
|
agent_ids=[25],
|
|
136
|
-
|
|
136
|
+
service_owner_eoa_address="0x1234567890123456789012345678901234567890",
|
|
137
137
|
staking_contract_address=staking_addr,
|
|
138
138
|
token_address=token_addr,
|
|
139
139
|
)
|
|
@@ -35,7 +35,7 @@ def mock_olas_config():
|
|
|
35
35
|
chain_name="gnosis",
|
|
36
36
|
agent_address=VALID_ADDR_1,
|
|
37
37
|
multisig_address=VALID_ADDR_2,
|
|
38
|
-
|
|
38
|
+
service_owner_eoa_address=VALID_ADDR_3,
|
|
39
39
|
staking_contract_address=VALID_ADDR_1,
|
|
40
40
|
)
|
|
41
41
|
config = OlasConfig(services={"gnosis:1": service})
|
|
@@ -108,6 +108,7 @@ def test_claim_rewards_no_accrued_rewards(mock_wallet):
|
|
|
108
108
|
|
|
109
109
|
mock_staking = MagicMock()
|
|
110
110
|
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
111
|
+
mock_staking.calculate_staking_reward.return_value = 0
|
|
111
112
|
mock_staking.get_accrued_rewards.return_value = 0
|
|
112
113
|
|
|
113
114
|
success, amount = manager.claim_rewards(staking_contract=mock_staking)
|
|
@@ -129,9 +130,12 @@ def test_claim_rewards_success(mock_wallet):
|
|
|
129
130
|
|
|
130
131
|
mock_staking = MagicMock()
|
|
131
132
|
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
133
|
+
mock_staking.calculate_staking_reward.return_value = 10 * 10**18 # 10 OLAS
|
|
132
134
|
mock_staking.get_accrued_rewards.return_value = 10 * 10**18 # 10 OLAS
|
|
133
135
|
mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
|
|
134
|
-
mock_staking.extract_events.return_value = [
|
|
136
|
+
mock_staking.extract_events.return_value = [
|
|
137
|
+
{"name": "RewardClaimed", "args": {"amount": 10 * 10**18}}
|
|
138
|
+
]
|
|
135
139
|
|
|
136
140
|
success, amount = manager.claim_rewards(staking_contract=mock_staking)
|
|
137
141
|
|
|
@@ -152,6 +156,7 @@ def test_claim_rewards_tx_fails(mock_wallet):
|
|
|
152
156
|
|
|
153
157
|
mock_staking = MagicMock()
|
|
154
158
|
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
159
|
+
mock_staking.calculate_staking_reward.return_value = 10 * 10**18
|
|
155
160
|
mock_staking.get_accrued_rewards.return_value = 10 * 10**18
|
|
156
161
|
mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
|
|
157
162
|
mock_wallet.sign_and_send_transaction.return_value = (False, {})
|
|
@@ -201,6 +201,7 @@ def test_sm_claim_rewards_tx_fails(mock_wallet):
|
|
|
201
201
|
mock_staking = MagicMock()
|
|
202
202
|
mock_staking.prepare_claim_tx.return_value = {"to": VALID_ADDR}
|
|
203
203
|
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
204
|
+
mock_staking.calculate_staking_reward.return_value = 100
|
|
204
205
|
mock_staking.get_accrued_rewards.return_value = 100
|
|
205
206
|
|
|
206
207
|
def get_contract_side_effect(cls, *args, **kwargs):
|
iwa/web/tests/test_web_olas.py
CHANGED
|
@@ -43,7 +43,7 @@ def mock_olas_config():
|
|
|
43
43
|
chain_name="gnosis",
|
|
44
44
|
agent_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
|
|
45
45
|
multisig_address="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
|
|
46
|
-
|
|
46
|
+
service_owner_eoa_address="0x1111111111111111111111111111111111111111",
|
|
47
47
|
staking_contract_address="0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
|
|
48
48
|
)
|
|
49
49
|
return OlasConfig(services={"gnosis:1": service})
|
|
@@ -70,7 +70,7 @@ iwa/plugins/olas/constants.py,sha256=BbEDho_TAh10cCGsrlk2vP1OVrS_ZWBE_cAEITd_658
|
|
|
70
70
|
iwa/plugins/olas/events.py,sha256=HHjYu4pN3tuZATIh8vGWWzDb7z9wuqhsaTqI3_4H0-I,6086
|
|
71
71
|
iwa/plugins/olas/importer.py,sha256=5xTtlQe5hO5bUCOg1sRuFkN2UcohYdJEQwfMm2JckyI,42088
|
|
72
72
|
iwa/plugins/olas/mech_reference.py,sha256=CaSCpQnQL4F7wOG6Ox6Zdoy-uNEQ78YBwVLILQZKL8Q,5782
|
|
73
|
-
iwa/plugins/olas/models.py,sha256=
|
|
73
|
+
iwa/plugins/olas/models.py,sha256=VtDjSyc63Yxs3aManmALrcf7asehdQ5f-5Y6MtAdWIk,4056
|
|
74
74
|
iwa/plugins/olas/plugin.py,sha256=kz21CxIGQw3Is6HC2dvKvFRIm9m1FRKHO0YgUuDEplQ,15650
|
|
75
75
|
iwa/plugins/olas/contracts/activity_checker.py,sha256=OXh0SFPGfcpeD665ay-I19LqcIx38qEz8o62dw0A9zE,5361
|
|
76
76
|
iwa/plugins/olas/contracts/base.py,sha256=y73aQbDq6l4zUpz_eQAg4MsLkTAEqjjupXlcvxjfgCI,240
|
|
@@ -93,8 +93,8 @@ iwa/plugins/olas/scripts/test_full_mech_flow.py,sha256=Fqoq5bn7Z_3YyRrnuqNAZy9cw
|
|
|
93
93
|
iwa/plugins/olas/scripts/test_simple_lifecycle.py,sha256=8T50tOZx3afeECSfCNAb0rAHNtYOsBaeXlMwKXElCk8,2099
|
|
94
94
|
iwa/plugins/olas/service_manager/__init__.py,sha256=GXiThMEY3nPgHUl1i-DLrF4h96z9jPxxI8Jepo2E1PM,1926
|
|
95
95
|
iwa/plugins/olas/service_manager/base.py,sha256=EBPg0ymqgtAb7ZvVSfTt31QYgv_6gp4UAc6je00NLAg,5009
|
|
96
|
-
iwa/plugins/olas/service_manager/drain.py,sha256=
|
|
97
|
-
iwa/plugins/olas/service_manager/lifecycle.py,sha256=
|
|
96
|
+
iwa/plugins/olas/service_manager/drain.py,sha256=oIBZON_ypshqaI2MZGKHW63dLEkkIcyAqS5lTZy0Qc4,13714
|
|
97
|
+
iwa/plugins/olas/service_manager/lifecycle.py,sha256=Jz2WTsBE_kGjFnsnpQlUBmMM2csOuqEfYdvj4cLrNuU,50586
|
|
98
98
|
iwa/plugins/olas/service_manager/mech.py,sha256=NVzVbEmyOe3wK92VEzCCOSuy3HDkEP1MSoVt7Av8Psk,27949
|
|
99
99
|
iwa/plugins/olas/service_manager/staking.py,sha256=kT9OOQ4fi3FrIJB2T2gsvmv7DBRD6pDxqcXXh2o6iwc,29600
|
|
100
100
|
iwa/plugins/olas/tests/conftest.py,sha256=4vM7EI00SrTGyeP0hNzsGSQHEj2-iznVgzlNh2_OGfo,739
|
|
@@ -104,9 +104,9 @@ iwa/plugins/olas/tests/test_mech_contracts.py,sha256=wvxuigPafF-ySIHVBdWVei3AO41
|
|
|
104
104
|
iwa/plugins/olas/tests/test_olas_archiving.py,sha256=rwyP-9eZ1cNvvV4h4bBOrs_8qV-du9yt-VNMuH_13nY,3443
|
|
105
105
|
iwa/plugins/olas/tests/test_olas_contracts.py,sha256=B8X-5l1KfYMoZOiM94_rcNzbILLl78rqt_jhyxzAOqE,10835
|
|
106
106
|
iwa/plugins/olas/tests/test_olas_integration.py,sha256=LGkdeso5lvi7_0GjlS9EFlSs6PEEn_b5aD2USmperDA,23086
|
|
107
|
-
iwa/plugins/olas/tests/test_olas_models.py,sha256=
|
|
107
|
+
iwa/plugins/olas/tests/test_olas_models.py,sha256=LCtU01v2LhPgSjzMBGlhRQ9nBb7sgcd7n7F1z-T-IuA,4977
|
|
108
108
|
iwa/plugins/olas/tests/test_olas_view.py,sha256=2SsQYayeV3rf_mAPVvt4vINcMysAXmICkkQe3MRn4K8,10662
|
|
109
|
-
iwa/plugins/olas/tests/test_olas_view_actions.py,sha256=
|
|
109
|
+
iwa/plugins/olas/tests/test_olas_view_actions.py,sha256=67al8ffNWMETI-foq-FjsXqpjdEruuH1sRCsGo_x9GE,5113
|
|
110
110
|
iwa/plugins/olas/tests/test_olas_view_modals.py,sha256=8j0PNFjKqFC5V1kBdVFWNLMvqGt49H6fLSYGxn02c8o,5562
|
|
111
111
|
iwa/plugins/olas/tests/test_plugin.py,sha256=RVgU-Cq6t_3mOh90xFAGwlJOV7ZIgp0VNaK5ZAxisAQ,2565
|
|
112
112
|
iwa/plugins/olas/tests/test_plugin_full.py,sha256=55EBa07JhJLVG3IMi6QKlR_ivWLYCdLQTySP66qbEXo,8584
|
|
@@ -115,9 +115,9 @@ iwa/plugins/olas/tests/test_service_manager.py,sha256=_mFRptssimITHhjvZA5jUPU2bI
|
|
|
115
115
|
iwa/plugins/olas/tests/test_service_manager_errors.py,sha256=-qpLmU4Uiqqtre59L2wXpO4WPMs4ej_K_gAL3naEvRg,8554
|
|
116
116
|
iwa/plugins/olas/tests/test_service_manager_flows.py,sha256=ZSmBJNa18d_MyAaLQRoPpfFYRwzmk9k-5AhSAGd7WeI,20737
|
|
117
117
|
iwa/plugins/olas/tests/test_service_manager_mech.py,sha256=qG6qu5IPRNypXUsblU2OEkuiuwDJ0TH8RXZbibmTFcQ,4937
|
|
118
|
-
iwa/plugins/olas/tests/test_service_manager_rewards.py,sha256=
|
|
118
|
+
iwa/plugins/olas/tests/test_service_manager_rewards.py,sha256=2YCrXBU5bEkPuhBoGBhjnO1nA2qwHxn5Ivrror18FHM,12248
|
|
119
119
|
iwa/plugins/olas/tests/test_service_manager_validation.py,sha256=ajlfH5uc4mAHf8A7GLE5cW7X8utM2vUilM0JdGDdlVg,5382
|
|
120
|
-
iwa/plugins/olas/tests/test_service_staking.py,sha256=
|
|
120
|
+
iwa/plugins/olas/tests/test_service_staking.py,sha256=78yyPoLo51N1aQyDxjzj7I0a263JHKHqekL-W3oQAsw,15914
|
|
121
121
|
iwa/plugins/olas/tests/test_staking_integration.py,sha256=QCBQf6P2ZmmsEGt2k8W2r53lG2aVRuoMJE-aFxVDLss,9701
|
|
122
122
|
iwa/plugins/olas/tests/test_staking_validation.py,sha256=uug64jFcXYJ3Nw_lNa3O4fnhNr5wAWHHIrchSbR2MVE,4020
|
|
123
123
|
iwa/plugins/olas/tui/__init__.py,sha256=5ZRsbC7J3z1xfkZRiwr4bLEklf78rNVjdswe2p7SlS8,28
|
|
@@ -163,10 +163,10 @@ iwa/web/static/app.js,sha256=hBjUSivxf5Uyy2H6BR-rfdvF8e7qBoDPK23aY3dagNY,114418
|
|
|
163
163
|
iwa/web/static/index.html,sha256=q7s7plnMbN1Nkzr5bRxZgvgOFerUChEGIZW7SpAVtPc,28514
|
|
164
164
|
iwa/web/static/style.css,sha256=7i6T96pS7gXSLDZfyp_87gRlyB9rpsFWJEHJ-dRY1ug,24371
|
|
165
165
|
iwa/web/tests/test_web_endpoints.py,sha256=vA25YghHNB23sbmhD4ciesn_f_okSq0tjlkrSiKZ0rs,24007
|
|
166
|
-
iwa/web/tests/test_web_olas.py,sha256=
|
|
166
|
+
iwa/web/tests/test_web_olas.py,sha256=GunKEAzcbzL7FoUGMtEl8wqiqwYwA5lB9sOhfCNj0TA,16312
|
|
167
167
|
iwa/web/tests/test_web_swap.py,sha256=7A4gBJFL01kIXPtW1E1J17SCsVc_0DmUn-R8kKrnnVA,2974
|
|
168
168
|
iwa/web/tests/test_web_swap_coverage.py,sha256=zGNrzlhZ_vWDCvWmLcoUwFgqxnrp_ACbo49AtWBS_Kw,5584
|
|
169
|
-
iwa-0.0.
|
|
169
|
+
iwa-0.0.64.dist-info/licenses/LICENSE,sha256=eIubm_IlBHPYRQlLNZKbBNKhJUUP3JH0A2miZUhAVfI,1078
|
|
170
170
|
tests/legacy_cow.py,sha256=oOkZvIxL70ReEoD9oHQbOD5GpjIr6AGNHcOCgfPlerU,8389
|
|
171
171
|
tests/legacy_safe.py,sha256=AssM2g13E74dNGODu_H0Q0y412lgqsrYnEzI97nm_Ts,2972
|
|
172
172
|
tests/legacy_transaction_retry_logic.py,sha256=D9RqZ7DBu61Xr2djBAodU2p9UE939LL-DnQXswX5iQk,1497
|
|
@@ -178,11 +178,12 @@ tests/test_balance_service.py,sha256=wcuCOVszxPy8nPkldAVcEiygcOK3BuQt797fqAJvbp4
|
|
|
178
178
|
tests/test_chain.py,sha256=VZoidSojWyt1y4mQdZdoZsjuuDZjLC6neTC-2SF_Q7I,13957
|
|
179
179
|
tests/test_chain_interface.py,sha256=bgqGM8wJGZjc-BOX6i0K4sh06KCJl-6UAvrwl8x24lA,8324
|
|
180
180
|
tests/test_chain_interface_coverage.py,sha256=fvrVvw8-DMwdsSFKQHUhpbfutrVRxnnTc-tjB7Bb-jo,3327
|
|
181
|
-
tests/test_chainlist_enrichment.py,sha256=
|
|
181
|
+
tests/test_chainlist_enrichment.py,sha256=hxfYjI54hT2pBXeacmQkLJGEPyuaAwDtNL5sEVZURp8,22065
|
|
182
182
|
tests/test_cli.py,sha256=Pl4RC2xp1omiJUnL3Dza6pCmIoO29LJ0vGw33_ZpT5c,3980
|
|
183
183
|
tests/test_contract.py,sha256=tApHAxsfKGawYJWA9PhTNrOZUE0VVAq79ruIe3KxeWY,14412
|
|
184
|
+
tests/test_contract_cache.py,sha256=TYIYPwIm_pVsdDjXJj_9-yDah_RFYB3PBn_fyE-a_YU,8454
|
|
184
185
|
tests/test_db.py,sha256=dmbrupj0qlUeiiycZ2mzMFjf7HrDa6tcqMPY8zpiKIk,5710
|
|
185
|
-
tests/test_drain_coverage.py,sha256=
|
|
186
|
+
tests/test_drain_coverage.py,sha256=x-ANNt2YVJcuJApMe7VlzZE-1RRpSvGtDz5M_UXLc4I,18497
|
|
186
187
|
tests/test_erc20.py,sha256=kNEw1afpm5EbXRNXkjpkBNZIy7Af1nqGlztKH5IWAwU,3074
|
|
187
188
|
tests/test_gnosis_plugin.py,sha256=XMoHBCTrnVBq9bXYPzMUIrhr95caucMVRxooCjKrzjg,3454
|
|
188
189
|
tests/test_keys.py,sha256=Qk4n3QDZ2HjXYRvehdrSlvDS_q3NLRLMnCq45Eo1Q9o,17551
|
|
@@ -210,7 +211,7 @@ tests/test_service_manager_integration.py,sha256=I_BLUzEKrVTyg_8jqsUK0oFD3aQVPCR
|
|
|
210
211
|
tests/test_service_manager_structure.py,sha256=zK506ucCXCBHcjPYKrKEuK1bgq0xsbawyL8Y-wahXf8,868
|
|
211
212
|
tests/test_service_transaction.py,sha256=IeqYhmRD-pIXffBJrBQwfPx-qnfNEJs0iPM3eCb8MLo,7054
|
|
212
213
|
tests/test_staking_router.py,sha256=cnOtwWeQPu09kecVhlCf1WA4ONqs13OcQJhJCx2EOPY,3067
|
|
213
|
-
tests/test_staking_simple.py,sha256=
|
|
214
|
+
tests/test_staking_simple.py,sha256=LN1ehVpNlT2oGGNhj0B1DAmazoL5xRmSCHB5AUL8900,19643
|
|
214
215
|
tests/test_tables.py,sha256=1KQHgxuizoOrRxpubDdnzk9iaU5Lwyp3bcWP_hZD5uU,2686
|
|
215
216
|
tests/test_transaction_service.py,sha256=q2IQ6cJ6sZtzc_pVCM_dv0vW7LW2sONNrK5Pvrm63rU,12816
|
|
216
217
|
tests/test_transfer_multisend.py,sha256=PErjNqNwN66TMh4oVa307re64Ucccg1LkXqB0KlkmsI,6677
|
|
@@ -223,8 +224,8 @@ tests/test_utils.py,sha256=vkP49rYNI8BRzLpWR3WnKdDr8upeZjZcs7Rx0pjbQMo,1292
|
|
|
223
224
|
tests/test_workers.py,sha256=MInwdkFY5LdmFB3o1odIaSD7AQZb3263hNafO1De5PE,2793
|
|
224
225
|
tools/create_and_stake_service.py,sha256=1xwy_bJQI1j9yIQ968Oc9Db_F6mk1659LuuZntTASDE,3742
|
|
225
226
|
tools/verify_drain.py,sha256=PkMjblyOOAuQge88FwfEzRtCYeEtJxXhPBmtQYCoQ-8,6743
|
|
226
|
-
iwa-0.0.
|
|
227
|
-
iwa-0.0.
|
|
228
|
-
iwa-0.0.
|
|
229
|
-
iwa-0.0.
|
|
230
|
-
iwa-0.0.
|
|
227
|
+
iwa-0.0.64.dist-info/METADATA,sha256=9wa9UGdHrJz-WdMtiNRkwK9yvqAk-QDETX5UXJJaxWA,7337
|
|
228
|
+
iwa-0.0.64.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
229
|
+
iwa-0.0.64.dist-info/entry_points.txt,sha256=nwB6kscrfA7M00pYmL2j-sBH6eF6h2ga9IK1BZxdiyQ,241
|
|
230
|
+
iwa-0.0.64.dist-info/top_level.txt,sha256=kedS9cRUbm4JE2wYeabIXilhHjN8KCw0IGbqqqsw0Bs,16
|
|
231
|
+
iwa-0.0.64.dist-info/RECORD,,
|
|
@@ -352,3 +352,236 @@ class TestEnrichFromChainlist:
|
|
|
352
352
|
|
|
353
353
|
# Already at MAX_RPCS=20, ChainlistRPC should not be called
|
|
354
354
|
mock_cl_cls.assert_not_called()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestRPCNode:
|
|
358
|
+
"""Test RPCNode dataclass."""
|
|
359
|
+
|
|
360
|
+
def test_is_tracking_privacy(self):
|
|
361
|
+
"""Test is_tracking returns True for privacy tracking."""
|
|
362
|
+
node = RPCNode(url="https://example.com", is_working=True, privacy="privacy")
|
|
363
|
+
assert node.is_tracking is True
|
|
364
|
+
|
|
365
|
+
def test_is_tracking_limited(self):
|
|
366
|
+
"""Test is_tracking returns True for limited tracking."""
|
|
367
|
+
node = RPCNode(url="https://example.com", is_working=True, tracking="limited")
|
|
368
|
+
assert node.is_tracking is True
|
|
369
|
+
|
|
370
|
+
def test_is_tracking_yes(self):
|
|
371
|
+
"""Test is_tracking returns True for explicit yes tracking."""
|
|
372
|
+
node = RPCNode(url="https://example.com", is_working=True, tracking="yes")
|
|
373
|
+
assert node.is_tracking is True
|
|
374
|
+
|
|
375
|
+
def test_is_tracking_none(self):
|
|
376
|
+
"""Test is_tracking returns False for no tracking."""
|
|
377
|
+
node = RPCNode(url="https://example.com", is_working=True, tracking="none")
|
|
378
|
+
assert node.is_tracking is False
|
|
379
|
+
|
|
380
|
+
def test_is_tracking_default(self):
|
|
381
|
+
"""Test is_tracking returns False by default."""
|
|
382
|
+
node = RPCNode(url="https://example.com", is_working=True)
|
|
383
|
+
assert node.is_tracking is False
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class TestFilterCandidates:
|
|
387
|
+
"""Test _filter_candidates function."""
|
|
388
|
+
|
|
389
|
+
def test_max_candidates_limit(self):
|
|
390
|
+
"""Test that _filter_candidates respects MAX_CHAINLIST_CANDIDATES."""
|
|
391
|
+
from iwa.core.chainlist import MAX_CHAINLIST_CANDIDATES, _filter_candidates
|
|
392
|
+
|
|
393
|
+
# Create more nodes than MAX_CHAINLIST_CANDIDATES
|
|
394
|
+
nodes = [
|
|
395
|
+
RPCNode(url=f"https://rpc{i}.example.com", is_working=True)
|
|
396
|
+
for i in range(MAX_CHAINLIST_CANDIDATES + 10)
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
result = _filter_candidates(nodes, set())
|
|
400
|
+
|
|
401
|
+
# Should be limited to MAX_CHAINLIST_CANDIDATES
|
|
402
|
+
assert len(result) == MAX_CHAINLIST_CANDIDATES
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class TestChainlistRPCFetchData:
|
|
406
|
+
"""Test ChainlistRPC.fetch_data with caching."""
|
|
407
|
+
|
|
408
|
+
def test_fetch_data_uses_cache(self, tmp_path):
|
|
409
|
+
"""Test fetch_data uses cached data when valid."""
|
|
410
|
+
import json
|
|
411
|
+
from unittest.mock import patch
|
|
412
|
+
|
|
413
|
+
cache_file = tmp_path / "chainlist_rpcs.json"
|
|
414
|
+
cache_data = [{"chainId": 100, "name": "Test", "rpc": []}]
|
|
415
|
+
cache_file.write_text(json.dumps(cache_data))
|
|
416
|
+
|
|
417
|
+
with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
|
|
418
|
+
with patch("iwa.core.chainlist.requests.Session") as mock_session:
|
|
419
|
+
cl = ChainlistRPC()
|
|
420
|
+
cl.fetch_data()
|
|
421
|
+
|
|
422
|
+
# Should not make network request when cache is valid
|
|
423
|
+
mock_session.return_value.get.assert_not_called()
|
|
424
|
+
assert cl._data == cache_data
|
|
425
|
+
|
|
426
|
+
def test_fetch_data_force_refresh(self, tmp_path):
|
|
427
|
+
"""Test fetch_data ignores cache when force_refresh=True."""
|
|
428
|
+
import json
|
|
429
|
+
|
|
430
|
+
cache_file = tmp_path / "chainlist_rpcs.json"
|
|
431
|
+
cache_data = [{"chainId": 100, "name": "Cached", "rpc": []}]
|
|
432
|
+
cache_file.write_text(json.dumps(cache_data))
|
|
433
|
+
|
|
434
|
+
fresh_data = [{"chainId": 100, "name": "Fresh", "rpc": []}]
|
|
435
|
+
|
|
436
|
+
with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
|
|
437
|
+
with patch("iwa.core.chainlist.requests.Session") as mock_session_cls:
|
|
438
|
+
mock_session = MagicMock()
|
|
439
|
+
mock_session_cls.return_value.__enter__ = MagicMock(
|
|
440
|
+
return_value=mock_session
|
|
441
|
+
)
|
|
442
|
+
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
|
443
|
+
mock_resp = MagicMock()
|
|
444
|
+
mock_resp.json.return_value = fresh_data
|
|
445
|
+
mock_session.get.return_value = mock_resp
|
|
446
|
+
|
|
447
|
+
cl = ChainlistRPC()
|
|
448
|
+
cl.fetch_data(force_refresh=True)
|
|
449
|
+
|
|
450
|
+
mock_session.get.assert_called_once()
|
|
451
|
+
assert cl._data == fresh_data
|
|
452
|
+
|
|
453
|
+
def test_fetch_data_network_error_falls_back_to_cache(self, tmp_path):
|
|
454
|
+
"""Test fetch_data falls back to expired cache on network error."""
|
|
455
|
+
import json
|
|
456
|
+
|
|
457
|
+
cache_file = tmp_path / "chainlist_rpcs.json"
|
|
458
|
+
cache_data = [{"chainId": 100, "name": "ExpiredCache", "rpc": []}]
|
|
459
|
+
cache_file.write_text(json.dumps(cache_data))
|
|
460
|
+
|
|
461
|
+
with patch.object(ChainlistRPC, "CACHE_PATH", cache_file):
|
|
462
|
+
# Force cache to be expired by setting CACHE_TTL to 0
|
|
463
|
+
with patch.object(ChainlistRPC, "CACHE_TTL", 0):
|
|
464
|
+
with patch("iwa.core.chainlist.requests.Session") as mock_session_cls:
|
|
465
|
+
mock_session = MagicMock()
|
|
466
|
+
mock_session_cls.return_value.__enter__ = MagicMock(
|
|
467
|
+
return_value=mock_session
|
|
468
|
+
)
|
|
469
|
+
mock_session_cls.return_value.__exit__ = MagicMock(return_value=False)
|
|
470
|
+
mock_session.get.side_effect = requests.RequestException("Network error")
|
|
471
|
+
|
|
472
|
+
cl = ChainlistRPC()
|
|
473
|
+
cl.fetch_data()
|
|
474
|
+
|
|
475
|
+
# Should fall back to expired cache
|
|
476
|
+
assert cl._data == cache_data
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class TestChainlistRPCGetRpcs:
|
|
480
|
+
"""Test ChainlistRPC.get_rpcs and related methods."""
|
|
481
|
+
|
|
482
|
+
def test_get_chain_data_no_data(self):
|
|
483
|
+
"""Test get_chain_data returns None when no data."""
|
|
484
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
485
|
+
cl = ChainlistRPC()
|
|
486
|
+
cl._data = []
|
|
487
|
+
result = cl.get_chain_data(999)
|
|
488
|
+
assert result is None
|
|
489
|
+
|
|
490
|
+
def test_get_chain_data_found(self):
|
|
491
|
+
"""Test get_chain_data returns chain data when found."""
|
|
492
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
493
|
+
cl = ChainlistRPC()
|
|
494
|
+
cl._data = [{"chainId": 100, "name": "Gnosis"}, {"chainId": 1, "name": "Ethereum"}]
|
|
495
|
+
result = cl.get_chain_data(100)
|
|
496
|
+
assert result == {"chainId": 100, "name": "Gnosis"}
|
|
497
|
+
|
|
498
|
+
def test_get_rpcs_parses_nodes(self):
|
|
499
|
+
"""Test get_rpcs parses RPC data into RPCNode objects."""
|
|
500
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
501
|
+
cl = ChainlistRPC()
|
|
502
|
+
cl._data = [
|
|
503
|
+
{
|
|
504
|
+
"chainId": 100,
|
|
505
|
+
"rpc": [
|
|
506
|
+
{"url": "https://rpc1.example.com", "privacy": "privacy"},
|
|
507
|
+
{"url": "https://rpc2.example.com", "tracking": "none"},
|
|
508
|
+
],
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
result = cl.get_rpcs(100)
|
|
512
|
+
|
|
513
|
+
assert len(result) == 2
|
|
514
|
+
assert result[0].url == "https://rpc1.example.com"
|
|
515
|
+
assert result[0].privacy == "privacy"
|
|
516
|
+
assert result[1].url == "https://rpc2.example.com"
|
|
517
|
+
assert result[1].tracking == "none"
|
|
518
|
+
|
|
519
|
+
def test_get_rpcs_chain_not_found(self):
|
|
520
|
+
"""Test get_rpcs returns empty list when chain not found."""
|
|
521
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
522
|
+
cl = ChainlistRPC()
|
|
523
|
+
cl._data = [{"chainId": 1, "rpc": []}]
|
|
524
|
+
result = cl.get_rpcs(999)
|
|
525
|
+
assert result == []
|
|
526
|
+
|
|
527
|
+
def test_get_https_rpcs(self):
|
|
528
|
+
"""Test get_https_rpcs filters to HTTPS/HTTP URLs."""
|
|
529
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
530
|
+
cl = ChainlistRPC()
|
|
531
|
+
cl._data = [
|
|
532
|
+
{
|
|
533
|
+
"chainId": 100,
|
|
534
|
+
"rpc": [
|
|
535
|
+
{"url": "https://rpc1.example.com"},
|
|
536
|
+
{"url": "http://rpc2.example.com"},
|
|
537
|
+
{"url": "wss://ws.example.com"},
|
|
538
|
+
],
|
|
539
|
+
}
|
|
540
|
+
]
|
|
541
|
+
result = cl.get_https_rpcs(100)
|
|
542
|
+
|
|
543
|
+
assert len(result) == 2
|
|
544
|
+
assert "https://rpc1.example.com" in result
|
|
545
|
+
assert "http://rpc2.example.com" in result
|
|
546
|
+
assert "wss://ws.example.com" not in result
|
|
547
|
+
|
|
548
|
+
def test_get_wss_rpcs(self):
|
|
549
|
+
"""Test get_wss_rpcs filters to WSS/WS URLs."""
|
|
550
|
+
with patch.object(ChainlistRPC, "fetch_data"):
|
|
551
|
+
cl = ChainlistRPC()
|
|
552
|
+
cl._data = [
|
|
553
|
+
{
|
|
554
|
+
"chainId": 100,
|
|
555
|
+
"rpc": [
|
|
556
|
+
{"url": "https://rpc1.example.com"},
|
|
557
|
+
{"url": "wss://ws.example.com"},
|
|
558
|
+
{"url": "ws://ws2.example.com"},
|
|
559
|
+
],
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
result = cl.get_wss_rpcs(100)
|
|
563
|
+
|
|
564
|
+
assert len(result) == 2
|
|
565
|
+
assert "wss://ws.example.com" in result
|
|
566
|
+
assert "ws://ws2.example.com" in result
|
|
567
|
+
assert "https://rpc1.example.com" not in result
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class TestGetValidatedRpcsEdgeCases:
|
|
571
|
+
"""Test edge cases in get_validated_rpcs."""
|
|
572
|
+
|
|
573
|
+
def _make_node(self, url):
|
|
574
|
+
return RPCNode(url=url, is_working=True)
|
|
575
|
+
|
|
576
|
+
@patch.object(ChainlistRPC, "get_rpcs")
|
|
577
|
+
def test_returns_empty_when_all_filtered(self, mock_get_rpcs):
|
|
578
|
+
"""Test returns empty list when all candidates are filtered."""
|
|
579
|
+
mock_get_rpcs.return_value = [
|
|
580
|
+
self._make_node("https://template.com/${API_KEY}"),
|
|
581
|
+
self._make_node("http://insecure.com"), # Not HTTPS
|
|
582
|
+
]
|
|
583
|
+
|
|
584
|
+
cl = ChainlistRPC()
|
|
585
|
+
result = cl.get_validated_rpcs(100, existing_rpcs=[])
|
|
586
|
+
|
|
587
|
+
assert result == []
|