iwa 0.0.0__py3-none-any.whl → 0.0.1a2__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.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Tests for Olas service staking and unstaking functionality."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from textual.app import App, ComposeResult
|
|
7
|
+
from textual.containers import VerticalScroll
|
|
8
|
+
|
|
9
|
+
from iwa.plugins.olas.contracts.service import ServiceState
|
|
10
|
+
from iwa.plugins.olas.contracts.staking import StakingState
|
|
11
|
+
from iwa.plugins.olas.models import Service, StakingStatus
|
|
12
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
13
|
+
from iwa.plugins.olas.tui.olas_view import OlasView
|
|
14
|
+
|
|
15
|
+
VALID_ADDR = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_wallet():
|
|
20
|
+
"""Mock wallet."""
|
|
21
|
+
w = MagicMock()
|
|
22
|
+
w.master_account.address = VALID_ADDR
|
|
23
|
+
w.sign_and_send_transaction.return_value = (True, {"status": 1})
|
|
24
|
+
w.key_storage = MagicMock()
|
|
25
|
+
w.key_storage._password = "pass"
|
|
26
|
+
w.balance_service = MagicMock()
|
|
27
|
+
w.drain.return_value = {"tx": "0x123"}
|
|
28
|
+
return w
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# === SERVICE MANAGER STAKE/UNSTAKE SIMPLE FAILURES ===
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_sm_unstake_not_staked(mock_wallet):
|
|
35
|
+
"""Cover unstake when not staked (lines 736-738)."""
|
|
36
|
+
with patch("iwa.core.models.Config"):
|
|
37
|
+
sm = ServiceManager(mock_wallet)
|
|
38
|
+
sm.service = Service(
|
|
39
|
+
service_name="t", chain_name="gnosis", service_id=1, multisig_address=VALID_ADDR
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
mock_staking = MagicMock()
|
|
43
|
+
mock_staking.get_staking_state.return_value = StakingState.NOT_STAKED
|
|
44
|
+
|
|
45
|
+
result = sm.unstake(mock_staking)
|
|
46
|
+
assert result is False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_sm_unstake_tx_fails(mock_wallet):
|
|
50
|
+
"""Cover unstake transaction failure (lines 766-768)."""
|
|
51
|
+
with patch("iwa.core.models.Config"):
|
|
52
|
+
sm = ServiceManager(mock_wallet)
|
|
53
|
+
sm.service = Service(
|
|
54
|
+
service_name="t", chain_name="gnosis", service_id=1, multisig_address=VALID_ADDR
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
mock_staking = MagicMock()
|
|
58
|
+
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
59
|
+
mock_staking.get_service_info.return_value = {"ts_start": 1}
|
|
60
|
+
mock_staking.min_staking_duration = 0
|
|
61
|
+
mock_staking.prepare_unstake_tx.return_value = {"to": VALID_ADDR}
|
|
62
|
+
|
|
63
|
+
mock_wallet.sign_and_send_transaction.return_value = (False, None)
|
|
64
|
+
|
|
65
|
+
result = sm.unstake(mock_staking)
|
|
66
|
+
assert result is False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# === SERVICE MANAGER STAKING STATUS EDGE CASES (lines 843-891) ===
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_sm_get_staking_status_no_staking_address(mock_wallet):
|
|
73
|
+
"""Cover get_staking_status with no staking address (lines 831)."""
|
|
74
|
+
with patch("iwa.core.models.Config"):
|
|
75
|
+
sm = ServiceManager(mock_wallet)
|
|
76
|
+
sm.service = Service(
|
|
77
|
+
service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Contract load fails
|
|
81
|
+
with patch(
|
|
82
|
+
"iwa.plugins.olas.service_manager.staking.StakingContract",
|
|
83
|
+
side_effect=Exception("fail"),
|
|
84
|
+
):
|
|
85
|
+
status = sm.get_staking_status()
|
|
86
|
+
assert status.staking_state == "ERROR"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_sm_get_staking_status_with_full_info(mock_wallet):
|
|
90
|
+
"""Cover get_staking_status with complete info (lines 866-891)."""
|
|
91
|
+
with patch("iwa.core.models.Config"):
|
|
92
|
+
sm = ServiceManager(mock_wallet)
|
|
93
|
+
sm.service = Service(
|
|
94
|
+
service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
mock_staking = MagicMock()
|
|
98
|
+
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
99
|
+
mock_staking.activity_checker_address = VALID_ADDR
|
|
100
|
+
mock_staking.activity_checker.liveness_ratio = 10
|
|
101
|
+
mock_staking.get_epoch_counter.return_value = 5
|
|
102
|
+
mock_staking.min_staking_duration = 86400
|
|
103
|
+
mock_staking.get_service_info.return_value = {
|
|
104
|
+
"ts_start": 1000,
|
|
105
|
+
"mech_requests_this_epoch": 3,
|
|
106
|
+
"required_mech_requests": 5,
|
|
107
|
+
"remaining_mech_requests": 2,
|
|
108
|
+
"has_enough_requests": False,
|
|
109
|
+
"liveness_ratio_passed": True,
|
|
110
|
+
"accrued_reward_wei": 1000000,
|
|
111
|
+
"epoch_end_utc": None,
|
|
112
|
+
"remaining_epoch_seconds": 3600,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
with (
|
|
116
|
+
patch(
|
|
117
|
+
"iwa.plugins.olas.service_manager.staking.StakingContract",
|
|
118
|
+
return_value=mock_staking,
|
|
119
|
+
),
|
|
120
|
+
patch("iwa.plugins.olas.service_manager.staking.Web3") as mock_web3,
|
|
121
|
+
):
|
|
122
|
+
mock_web3.from_wei.return_value = 0.001
|
|
123
|
+
status = sm.get_staking_status()
|
|
124
|
+
assert status.is_staked is True
|
|
125
|
+
assert status.epoch_number == 5
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# === SERVICE MANAGER CLAIM/WITHDRAW (lines 936-979) ===
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_sm_claim_rewards_no_service(mock_wallet):
|
|
132
|
+
"""Cover claim_rewards with no service (lines 936-938)."""
|
|
133
|
+
with patch("iwa.core.models.Config"):
|
|
134
|
+
sm = ServiceManager(mock_wallet)
|
|
135
|
+
sm.service = None
|
|
136
|
+
|
|
137
|
+
success, amount = sm.claim_rewards()
|
|
138
|
+
assert success is False
|
|
139
|
+
assert amount == 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_sm_claim_rewards_no_staking_address(mock_wallet):
|
|
143
|
+
"""Cover claim_rewards with no staking address (lines 939-943)."""
|
|
144
|
+
with patch("iwa.core.models.Config"):
|
|
145
|
+
sm = ServiceManager(mock_wallet)
|
|
146
|
+
sm.service = Service(
|
|
147
|
+
service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=None
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
success, amount = sm.claim_rewards()
|
|
151
|
+
assert success is False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_sm_claim_rewards_tx_fails(mock_wallet):
|
|
155
|
+
"""Cover claim_rewards transaction failure (lines 967-968)."""
|
|
156
|
+
with patch("iwa.core.models.Config"):
|
|
157
|
+
sm = ServiceManager(mock_wallet)
|
|
158
|
+
sm.service = Service(
|
|
159
|
+
service_name="t",
|
|
160
|
+
chain_name="gnosis",
|
|
161
|
+
service_id=1,
|
|
162
|
+
staking_contract_address=VALID_ADDR,
|
|
163
|
+
multisig_address=VALID_ADDR,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
mock_staking = MagicMock()
|
|
167
|
+
mock_staking.prepare_claim_tx.return_value = {"to": VALID_ADDR}
|
|
168
|
+
|
|
169
|
+
mock_wallet.sign_and_send_transaction.return_value = (False, None)
|
|
170
|
+
|
|
171
|
+
with patch("iwa.plugins.olas.service_manager.StakingContract", return_value=mock_staking):
|
|
172
|
+
success, amount = sm.claim_rewards()
|
|
173
|
+
assert success is False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# === SERVICE MANAGER SPIN_UP STATE TRANSITIONS (lines 1188-1241) ===
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_sm_spin_up_state_mismatch_after_activation(mock_wallet):
|
|
180
|
+
"""Cover spin_up state mismatch after activation (lines 1188-1191)."""
|
|
181
|
+
with patch("iwa.core.models.Config"):
|
|
182
|
+
sm = ServiceManager(mock_wallet)
|
|
183
|
+
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
184
|
+
|
|
185
|
+
mock_reg = MagicMock()
|
|
186
|
+
# First call: PRE_REGISTRATION, second call: still PRE_REGISTRATION (mismatch)
|
|
187
|
+
mock_reg.get_service.side_effect = [
|
|
188
|
+
{"state": ServiceState.PRE_REGISTRATION},
|
|
189
|
+
{"state": ServiceState.PRE_REGISTRATION},
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
with (
|
|
193
|
+
patch.object(sm, "registry", mock_reg),
|
|
194
|
+
patch.object(sm, "activate_registration", return_value=True),
|
|
195
|
+
):
|
|
196
|
+
result = sm.spin_up()
|
|
197
|
+
assert result is False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_sm_spin_up_registration_fails(mock_wallet):
|
|
201
|
+
"""Cover spin_up registration failure (lines 1199-1201)."""
|
|
202
|
+
with patch("iwa.core.models.Config"):
|
|
203
|
+
sm = ServiceManager(mock_wallet)
|
|
204
|
+
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
205
|
+
|
|
206
|
+
mock_reg = MagicMock()
|
|
207
|
+
mock_reg.get_service.return_value = {"state": ServiceState.ACTIVE_REGISTRATION}
|
|
208
|
+
|
|
209
|
+
with (
|
|
210
|
+
patch.object(sm, "registry", mock_reg),
|
|
211
|
+
patch.object(sm, "register_agent", return_value=False),
|
|
212
|
+
):
|
|
213
|
+
result = sm.spin_up()
|
|
214
|
+
assert result is False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_sm_spin_up_deploy_fails(mock_wallet):
|
|
218
|
+
"""Cover spin_up deploy failure (lines 1216-1218)."""
|
|
219
|
+
with patch("iwa.core.models.Config"):
|
|
220
|
+
sm = ServiceManager(mock_wallet)
|
|
221
|
+
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
222
|
+
|
|
223
|
+
mock_reg = MagicMock()
|
|
224
|
+
mock_reg.get_service.return_value = {"state": ServiceState.FINISHED_REGISTRATION}
|
|
225
|
+
|
|
226
|
+
with patch.object(sm, "registry", mock_reg), patch.object(sm, "deploy", return_value=None):
|
|
227
|
+
result = sm.spin_up()
|
|
228
|
+
assert result is False
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# === SERVICE MANAGER WIND DOWN TRANSITIONS (lines 1306-1334) ===
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_sm_wind_down_terminate_fails(mock_wallet):
|
|
235
|
+
"""Cover wind_down terminate failure (lines 1299-1301)."""
|
|
236
|
+
with patch("iwa.core.models.Config"):
|
|
237
|
+
sm = ServiceManager(mock_wallet)
|
|
238
|
+
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
239
|
+
|
|
240
|
+
mock_reg = MagicMock()
|
|
241
|
+
mock_reg.get_service.return_value = {"state": ServiceState.DEPLOYED}
|
|
242
|
+
|
|
243
|
+
with (
|
|
244
|
+
patch.object(sm, "registry", mock_reg),
|
|
245
|
+
patch.object(sm, "terminate", return_value=False),
|
|
246
|
+
):
|
|
247
|
+
result = sm.wind_down()
|
|
248
|
+
assert result is False
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_sm_wind_down_unbond_fails(mock_wallet):
|
|
252
|
+
"""Cover wind_down unbond failure (lines 1315-1317)."""
|
|
253
|
+
with patch("iwa.core.models.Config"):
|
|
254
|
+
sm = ServiceManager(mock_wallet)
|
|
255
|
+
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
256
|
+
|
|
257
|
+
mock_reg = MagicMock()
|
|
258
|
+
mock_reg.get_service.return_value = {"state": ServiceState.TERMINATED_BONDED}
|
|
259
|
+
|
|
260
|
+
with patch.object(sm, "registry", mock_reg), patch.object(sm, "unbond", return_value=False):
|
|
261
|
+
result = sm.wind_down()
|
|
262
|
+
assert result is False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# === OLAS VIEW EDGE CASES (lines 233-257, 319-323) ===
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class OlasTestApp(App):
|
|
269
|
+
"""Test App for OlasView."""
|
|
270
|
+
|
|
271
|
+
def __init__(self, wallet=None):
|
|
272
|
+
"""Initialize test app."""
|
|
273
|
+
super().__init__()
|
|
274
|
+
self.wallet = wallet
|
|
275
|
+
|
|
276
|
+
def compose(self) -> ComposeResult:
|
|
277
|
+
"""Compose layout."""
|
|
278
|
+
yield VerticalScroll(OlasView(self.wallet), id="root")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@pytest.mark.asyncio
|
|
282
|
+
async def test_view_render_exception(mock_wallet):
|
|
283
|
+
"""Cover _render_services exception handling (lines 233-235)."""
|
|
284
|
+
with patch("iwa.core.models.Config"):
|
|
285
|
+
app = OlasTestApp(mock_wallet)
|
|
286
|
+
async with app.run_test():
|
|
287
|
+
view = app.query_one(OlasView)
|
|
288
|
+
|
|
289
|
+
# Force exception by passing invalid data
|
|
290
|
+
with patch.object(view, "query_one", side_effect=Exception("test")):
|
|
291
|
+
# Should not raise, just logs
|
|
292
|
+
await view._render_services([("k", MagicMock(), None)])
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@pytest.mark.asyncio
|
|
296
|
+
async def test_view_mount_cards_exception(mock_wallet):
|
|
297
|
+
"""Cover _mount_cards exception handling (lines 239-247)."""
|
|
298
|
+
with patch("iwa.core.models.Config"):
|
|
299
|
+
app = OlasTestApp(mock_wallet)
|
|
300
|
+
async with app.run_test():
|
|
301
|
+
view = app.query_one(OlasView)
|
|
302
|
+
|
|
303
|
+
with patch.object(view, "query_one", side_effect=Exception("test")):
|
|
304
|
+
# Should not raise
|
|
305
|
+
view._mount_cards([MagicMock()])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@pytest.mark.asyncio
|
|
309
|
+
async def test_view_mount_error_exception(mock_wallet):
|
|
310
|
+
"""Cover _mount_error exception handling (lines 255-257)."""
|
|
311
|
+
with patch("iwa.core.models.Config"):
|
|
312
|
+
app = OlasTestApp(mock_wallet)
|
|
313
|
+
async with app.run_test():
|
|
314
|
+
view = app.query_one(OlasView)
|
|
315
|
+
|
|
316
|
+
with patch.object(view, "query_one", side_effect=Exception("test")):
|
|
317
|
+
# Should not raise
|
|
318
|
+
view._mount_error("test error")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@pytest.mark.asyncio
|
|
322
|
+
async def test_view_create_service_card_variants(mock_wallet):
|
|
323
|
+
"""Cover _create_service_card with various inputs (lines 319-323)."""
|
|
324
|
+
with patch("iwa.core.models.Config"):
|
|
325
|
+
app = OlasTestApp(mock_wallet)
|
|
326
|
+
async with app.run_test():
|
|
327
|
+
view = app.query_one(OlasView)
|
|
328
|
+
|
|
329
|
+
service = Service(
|
|
330
|
+
service_name="test", chain_name="gnosis", service_id=1, multisig_address=VALID_ADDR
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# With staking status
|
|
334
|
+
status = StakingStatus(
|
|
335
|
+
is_staked=True, staking_state="STAKED", accrued_reward_wei=1000000
|
|
336
|
+
)
|
|
337
|
+
card = view._create_service_card("gnosis:1", service, status)
|
|
338
|
+
assert card is not None
|
|
339
|
+
|
|
340
|
+
# Without staking status
|
|
341
|
+
card2 = view._create_service_card("gnosis:2", service, None)
|
|
342
|
+
assert card2 is not None
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Integration tests for Olas staking contracts."""
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import json
|
|
5
|
+
from unittest.mock import MagicMock, mock_open, patch
|
|
6
|
+
|
|
7
|
+
from eth_account import Account
|
|
8
|
+
|
|
9
|
+
from iwa.plugins.olas.contracts.service import (
|
|
10
|
+
ServiceManagerContract,
|
|
11
|
+
ServiceRegistryContract,
|
|
12
|
+
get_deployment_payload,
|
|
13
|
+
)
|
|
14
|
+
from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
|
|
15
|
+
|
|
16
|
+
# --- Helpers ---
|
|
17
|
+
VALID_ADDR_1 = Account.create().address
|
|
18
|
+
VALID_ADDR_2 = Account.create().address
|
|
19
|
+
VALID_ADDR_3 = Account.create().address
|
|
20
|
+
VALID_ADDR_4 = Account.create().address
|
|
21
|
+
|
|
22
|
+
original_open = builtins.open
|
|
23
|
+
|
|
24
|
+
# Minimal ABI
|
|
25
|
+
MINIMAL_ABI = [
|
|
26
|
+
{
|
|
27
|
+
"constant": True,
|
|
28
|
+
"inputs": [],
|
|
29
|
+
"name": "agentMech",
|
|
30
|
+
"outputs": [{"name": "", "type": "address"}],
|
|
31
|
+
"payable": False,
|
|
32
|
+
"stateMutability": "view",
|
|
33
|
+
"type": "function",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"constant": True,
|
|
37
|
+
"inputs": [],
|
|
38
|
+
"name": "livenessRatio",
|
|
39
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
40
|
+
"payable": False,
|
|
41
|
+
"stateMutability": "view",
|
|
42
|
+
"type": "function",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"constant": True,
|
|
46
|
+
"inputs": [{"name": "_multisig", "type": "address"}],
|
|
47
|
+
"name": "getMultisigNonces",
|
|
48
|
+
"outputs": [{"name": "", "type": "uint256[]"}],
|
|
49
|
+
"payable": False,
|
|
50
|
+
"stateMutability": "view",
|
|
51
|
+
"type": "function",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"constant": True,
|
|
55
|
+
"inputs": [
|
|
56
|
+
{"name": "currentNonces", "type": "uint256"},
|
|
57
|
+
{"name": "lastNonces", "type": "uint256"},
|
|
58
|
+
{"name": "timestamp", "type": "uint256"},
|
|
59
|
+
],
|
|
60
|
+
"name": "isRatioPass",
|
|
61
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
62
|
+
"payable": False,
|
|
63
|
+
"stateMutability": "view",
|
|
64
|
+
"type": "function",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "create",
|
|
68
|
+
"type": "function",
|
|
69
|
+
"inputs": [{"name": "", "type": "address"}] * 6,
|
|
70
|
+
"outputs": [],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "activateRegistration",
|
|
74
|
+
"type": "function",
|
|
75
|
+
"inputs": [{"name": "", "type": "uint256"}],
|
|
76
|
+
"outputs": [],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "registerAgents",
|
|
80
|
+
"type": "function",
|
|
81
|
+
"inputs": [
|
|
82
|
+
{"name": "", "type": "uint256"},
|
|
83
|
+
{"name": "", "type": "address[]"},
|
|
84
|
+
{"name": "", "type": "uint256[]"},
|
|
85
|
+
],
|
|
86
|
+
"outputs": [],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "deploy",
|
|
90
|
+
"type": "function",
|
|
91
|
+
"inputs": [
|
|
92
|
+
{"name": "", "type": "uint256"},
|
|
93
|
+
{"name": "", "type": "address"},
|
|
94
|
+
{"name": "", "type": "bytes"},
|
|
95
|
+
],
|
|
96
|
+
"outputs": [],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "terminate",
|
|
100
|
+
"type": "function",
|
|
101
|
+
"inputs": [{"name": "", "type": "uint256"}],
|
|
102
|
+
"outputs": [],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "unbond",
|
|
106
|
+
"type": "function",
|
|
107
|
+
"inputs": [{"name": "", "type": "uint256"}],
|
|
108
|
+
"outputs": [],
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def side_effect_open(*args, **kwargs):
|
|
114
|
+
"""Side effect for open() to return mock ABI content."""
|
|
115
|
+
filename = args[0] if args else kwargs.get("file")
|
|
116
|
+
s_file = str(filename)
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
"service_registry.json" in s_file
|
|
120
|
+
or "service_manager.json" in s_file
|
|
121
|
+
or "staking.json" in s_file
|
|
122
|
+
or "activity_checker.json" in s_file
|
|
123
|
+
):
|
|
124
|
+
return mock_open(read_data=json.dumps(MINIMAL_ABI))(*args, **kwargs)
|
|
125
|
+
|
|
126
|
+
return original_open(*args, **kwargs)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# --- Contract Tests ---
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_service_contracts():
|
|
133
|
+
"""Test ServiceRegistry and ServiceManager contract interactions."""
|
|
134
|
+
with patch("builtins.open", side_effect=side_effect_open):
|
|
135
|
+
registry = ServiceRegistryContract(VALID_ADDR_1)
|
|
136
|
+
|
|
137
|
+
# Test get_service
|
|
138
|
+
with patch.object(registry, "call") as mock_call:
|
|
139
|
+
mock_call.return_value = (100, VALID_ADDR_2, b"hash", 3, 4, 4, 4, [1, 2])
|
|
140
|
+
data = registry.get_service(1)
|
|
141
|
+
assert data["state"].name == "DEPLOYED"
|
|
142
|
+
assert data["config_hash"] == b"hash".hex()
|
|
143
|
+
|
|
144
|
+
# Test prepare_approve_tx
|
|
145
|
+
with patch.object(registry, "prepare_transaction") as mock_prep:
|
|
146
|
+
mock_prep.return_value = {"data": "0xTx"}
|
|
147
|
+
tx = registry.prepare_approve_tx(VALID_ADDR_2, VALID_ADDR_3, 1)
|
|
148
|
+
assert tx == {"data": "0xTx"}
|
|
149
|
+
|
|
150
|
+
manager = ServiceManagerContract(VALID_ADDR_1)
|
|
151
|
+
|
|
152
|
+
# Mock ChainInterfaces for get_contract_address
|
|
153
|
+
with patch.object(manager.chain_interface, "get_contract_address") as mock_get_addr:
|
|
154
|
+
mock_get_addr.return_value = VALID_ADDR_4
|
|
155
|
+
|
|
156
|
+
# Test prepare methods
|
|
157
|
+
with patch.object(manager, "prepare_transaction") as mock_prep:
|
|
158
|
+
mock_prep.return_value = {}
|
|
159
|
+
|
|
160
|
+
manager.prepare_create_tx(
|
|
161
|
+
VALID_ADDR_2, VALID_ADDR_3, VALID_ADDR_1, "hash", [], [], 3
|
|
162
|
+
)
|
|
163
|
+
assert mock_prep.called
|
|
164
|
+
mock_prep.reset_mock()
|
|
165
|
+
|
|
166
|
+
manager.prepare_activate_registration_tx(VALID_ADDR_2, 1)
|
|
167
|
+
assert mock_prep.called
|
|
168
|
+
mock_prep.reset_mock()
|
|
169
|
+
|
|
170
|
+
manager.prepare_register_agents_tx(VALID_ADDR_2, 1, [], [])
|
|
171
|
+
assert mock_prep.called
|
|
172
|
+
mock_prep.reset_mock()
|
|
173
|
+
|
|
174
|
+
manager.prepare_deploy_tx(VALID_ADDR_2, 1)
|
|
175
|
+
assert mock_prep.called
|
|
176
|
+
mock_prep.reset_mock()
|
|
177
|
+
|
|
178
|
+
manager.prepare_terminate_tx(VALID_ADDR_2, 1)
|
|
179
|
+
assert mock_prep.called
|
|
180
|
+
mock_prep.reset_mock()
|
|
181
|
+
|
|
182
|
+
manager.prepare_unbond_tx(VALID_ADDR_2, 1)
|
|
183
|
+
assert mock_prep.called
|
|
184
|
+
|
|
185
|
+
# Test get_deployment_payload
|
|
186
|
+
payload = get_deployment_payload(VALID_ADDR_4)
|
|
187
|
+
assert isinstance(payload, str)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_staking_contract(tmp_path): # noqa: C901
|
|
191
|
+
"""Test StakingContract logic and integration."""
|
|
192
|
+
with patch("builtins.open", side_effect=side_effect_open):
|
|
193
|
+
with patch("iwa.core.contracts.contract.ChainInterfaces") as mock_interfaces:
|
|
194
|
+
mock_chain = MagicMock()
|
|
195
|
+
mock_interfaces.return_value.get.return_value = mock_chain
|
|
196
|
+
|
|
197
|
+
# Mock web3
|
|
198
|
+
mock_web3 = MagicMock()
|
|
199
|
+
mock_chain.web3 = mock_web3
|
|
200
|
+
|
|
201
|
+
# Mock contract factory
|
|
202
|
+
mock_contract = MagicMock()
|
|
203
|
+
mock_web3.eth.contract.return_value = mock_contract
|
|
204
|
+
|
|
205
|
+
# Mock function calls (ActivityChecker)
|
|
206
|
+
mock_contract.functions.agentMech.return_value.call.return_value = VALID_ADDR_2
|
|
207
|
+
mock_contract.functions.livenessRatio.return_value.call.return_value = 10**18
|
|
208
|
+
|
|
209
|
+
with patch(
|
|
210
|
+
"iwa.plugins.olas.contracts.staking.ContractInstance.call"
|
|
211
|
+
) as mock_call_base:
|
|
212
|
+
# Initialization side effect
|
|
213
|
+
def init_side_effect(method, *args):
|
|
214
|
+
if method == "activityChecker":
|
|
215
|
+
return VALID_ADDR_4
|
|
216
|
+
if method == "stakingToken":
|
|
217
|
+
return VALID_ADDR_2
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
mock_call_base.side_effect = init_side_effect
|
|
221
|
+
|
|
222
|
+
staking = StakingContract(VALID_ADDR_1)
|
|
223
|
+
|
|
224
|
+
# Logic side effect
|
|
225
|
+
def logic_side_effect(method, *args):
|
|
226
|
+
if method == "getServiceInfo":
|
|
227
|
+
# Returns: (multisig, owner, nonces_on_last_checkpoint, ts_start, accrued_reward, inactivity)
|
|
228
|
+
# nonces_on_last_checkpoint must be [safe_nonce, mech_requests]
|
|
229
|
+
return (VALID_ADDR_2, VALID_ADDR_3, [1, 1], 1000, 50, 0)
|
|
230
|
+
if method == "getNextRewardCheckpointTimestamp":
|
|
231
|
+
return 4700000000 # Timestamp in future
|
|
232
|
+
if method == "calculateStakingLastReward":
|
|
233
|
+
return 50
|
|
234
|
+
if method == "calculateStakingReward":
|
|
235
|
+
return 50
|
|
236
|
+
if method == "getStakingState":
|
|
237
|
+
return 1
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
mock_call_base.side_effect = logic_side_effect
|
|
241
|
+
|
|
242
|
+
# Test methods
|
|
243
|
+
# Note: logic_side_effect handles different calls now
|
|
244
|
+
assert staking.calculate_accrued_staking_reward(1) == 50
|
|
245
|
+
assert staking.calculate_staking_reward(1) == 50
|
|
246
|
+
assert staking.get_staking_state(1) == StakingState.STAKED
|
|
247
|
+
assert staking.call("nonexistent") == 0
|
|
248
|
+
|
|
249
|
+
# Activity checker interactions - nonces now returns [safe_nonce, mech_requests]
|
|
250
|
+
mock_contract.functions.getMultisigNonces.return_value.call.return_value = [5, 3]
|
|
251
|
+
staking.activity_checker.contract = mock_contract
|
|
252
|
+
|
|
253
|
+
staking.ts_checkpoint = MagicMock(return_value=0)
|
|
254
|
+
|
|
255
|
+
# Trigger original_open hit
|
|
256
|
+
try:
|
|
257
|
+
test_file = tmp_path / "test_lookup.txt"
|
|
258
|
+
with builtins.open(str(test_file), "w") as f:
|
|
259
|
+
f.write("test")
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
info = staking.get_service_info(1)
|
|
264
|
+
assert info["owner_address"] == VALID_ADDR_3
|
|
265
|
+
assert "remaining_epoch_seconds" in info
|
|
266
|
+
assert info["remaining_epoch_seconds"] > 0
|
|
267
|
+
# Verify new nonces fields
|
|
268
|
+
assert info["current_safe_nonce"] == 5
|
|
269
|
+
assert info["current_mech_requests"] == 3
|