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,561 @@
|
|
|
1
|
+
"""Integration tests for Olas plugin: importer, service manager, plugin, and TUI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from textual.app import App, ComposeResult
|
|
9
|
+
from textual.containers import VerticalScroll
|
|
10
|
+
|
|
11
|
+
from iwa.plugins.olas.contracts.service import ServiceState
|
|
12
|
+
from iwa.plugins.olas.contracts.staking import StakingState
|
|
13
|
+
from iwa.plugins.olas.importer import DiscoveredKey, DiscoveredService, OlasServiceImporter
|
|
14
|
+
from iwa.plugins.olas.models import Service, StakingStatus
|
|
15
|
+
from iwa.plugins.olas.plugin import OlasPlugin
|
|
16
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
17
|
+
from iwa.plugins.olas.tui.olas_view import OlasView
|
|
18
|
+
|
|
19
|
+
VALID_ADDR = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_wallet():
|
|
24
|
+
"""Mock wallet."""
|
|
25
|
+
w = MagicMock()
|
|
26
|
+
w.master_account.address = VALID_ADDR
|
|
27
|
+
w.sign_and_send_transaction.return_value = (True, {"status": 1})
|
|
28
|
+
w.key_storage = MagicMock()
|
|
29
|
+
w.key_storage._password = "pass"
|
|
30
|
+
w.balance_service = MagicMock()
|
|
31
|
+
w.drain.return_value = {"tx": "0x123"}
|
|
32
|
+
return w
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# === IMPORTER GAPS (lines 63-73, 114-115, 181-186, etc) ===
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_discovered_service_properties():
|
|
39
|
+
"""Cover DiscoveredService.agent_key and operator_key properties (lines 60-73)."""
|
|
40
|
+
# No keys - should return None
|
|
41
|
+
svc = DiscoveredService()
|
|
42
|
+
assert svc.agent_key is None
|
|
43
|
+
assert svc.operator_key is None
|
|
44
|
+
|
|
45
|
+
# With agent key
|
|
46
|
+
svc.keys.append(DiscoveredKey(address="0x1", role="agent"))
|
|
47
|
+
assert svc.agent_key is not None
|
|
48
|
+
assert svc.agent_key.role == "agent"
|
|
49
|
+
assert svc.operator_key is None
|
|
50
|
+
|
|
51
|
+
# With operator key
|
|
52
|
+
svc.keys.append(DiscoveredKey(address="0x2", role="operator"))
|
|
53
|
+
assert svc.operator_key is not None
|
|
54
|
+
assert svc.operator_key.role == "operator"
|
|
55
|
+
|
|
56
|
+
# With owner key (also matches operator_key)
|
|
57
|
+
svc2 = DiscoveredService()
|
|
58
|
+
svc2.keys.append(DiscoveredKey(address="0x3", role="owner"))
|
|
59
|
+
assert svc2.operator_key is not None
|
|
60
|
+
assert svc2.operator_key.role == "owner"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_importer_scan_nonexistent_path(mock_wallet, tmp_path):
|
|
64
|
+
"""Cover scan_directory with non-existent path (line 114-115)."""
|
|
65
|
+
with patch("iwa.core.models.Config"):
|
|
66
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
67
|
+
result = importer.scan_directory(tmp_path / "nonexistent")
|
|
68
|
+
assert result == []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_importer_parse_keys_json_variations(mock_wallet, tmp_path):
|
|
72
|
+
"""Cover _parse_keys_json edge cases (lines 181-186, 355-377)."""
|
|
73
|
+
with patch("iwa.core.models.Config"):
|
|
74
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
75
|
+
|
|
76
|
+
# Valid array
|
|
77
|
+
keys_file = tmp_path / "keys.json"
|
|
78
|
+
keys_file.write_text(
|
|
79
|
+
json.dumps([{"address": "abc123", "crypto": {}}, {"address": "0xdef456", "crypto": {}}])
|
|
80
|
+
)
|
|
81
|
+
keys = importer._parse_keys_json(keys_file)
|
|
82
|
+
assert len(keys) == 2
|
|
83
|
+
assert keys[0].address == "0xabc123" # 0x prefix added
|
|
84
|
+
assert keys[1].address == "0xdef456" # Already has 0x
|
|
85
|
+
|
|
86
|
+
# Not an array
|
|
87
|
+
keys_file.write_text(json.dumps({"address": "0x123"}))
|
|
88
|
+
assert importer._parse_keys_json(keys_file) == []
|
|
89
|
+
|
|
90
|
+
# IO error
|
|
91
|
+
keys_file.unlink()
|
|
92
|
+
assert importer._parse_keys_json(keys_file) == []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_importer_parse_trader_runner_keys_json(mock_wallet, tmp_path):
|
|
96
|
+
"""Cover keys.json parsing in trader_runner format (lines 178-186)."""
|
|
97
|
+
with patch("iwa.core.models.Config"):
|
|
98
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
99
|
+
|
|
100
|
+
# Create .trader_runner with keys.json
|
|
101
|
+
trader = tmp_path / ".trader_runner"
|
|
102
|
+
trader.mkdir()
|
|
103
|
+
(trader / "service_id.txt").write_text("123")
|
|
104
|
+
(trader / "service_safe_address.txt").write_text(VALID_ADDR)
|
|
105
|
+
(trader / "keys.json").write_text(json.dumps([{"address": "0xkey1", "crypto": {}}]))
|
|
106
|
+
|
|
107
|
+
services = importer.scan_directory(tmp_path)
|
|
108
|
+
assert len(services) == 1
|
|
109
|
+
assert len(services[0].keys) == 1
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_importer_trader_runner_no_data(mock_wallet, tmp_path):
|
|
113
|
+
"""Cover trader_runner with no valid data (line 192-193)."""
|
|
114
|
+
with patch("iwa.core.models.Config"):
|
|
115
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
116
|
+
|
|
117
|
+
# Empty .trader_runner folder
|
|
118
|
+
trader = tmp_path / ".trader_runner"
|
|
119
|
+
trader.mkdir()
|
|
120
|
+
|
|
121
|
+
services = importer.scan_directory(tmp_path)
|
|
122
|
+
assert len(services) == 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_importer_operate_wallets_only(mock_wallet, tmp_path):
|
|
126
|
+
"""Cover _parse_operate_format with wallets but no services (lines 222-250)."""
|
|
127
|
+
with patch("iwa.core.models.Config"):
|
|
128
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
129
|
+
|
|
130
|
+
# Create .operate with wallets only
|
|
131
|
+
operate = tmp_path / ".operate"
|
|
132
|
+
wallets = operate / "wallets"
|
|
133
|
+
wallets.mkdir(parents=True)
|
|
134
|
+
|
|
135
|
+
# ethereum.txt with valid plaintext key JSON
|
|
136
|
+
(wallets / "ethereum.txt").write_text(
|
|
137
|
+
json.dumps({"address": VALID_ADDR, "private_key": "a" * 64})
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# ethereum.json with Safe info
|
|
141
|
+
(wallets / "ethereum.json").write_text(json.dumps({"safes": {"gnosis": VALID_ADDR}}))
|
|
142
|
+
|
|
143
|
+
services = importer._parse_operate_format(operate)
|
|
144
|
+
assert len(services) == 1
|
|
145
|
+
assert services[0].safe_address == VALID_ADDR
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_importer_operate_ethereum_json_error(mock_wallet, tmp_path):
|
|
149
|
+
"""Cover _parse_operate_format with invalid ethereum.json (line 246-247)."""
|
|
150
|
+
with patch("iwa.core.models.Config"):
|
|
151
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
152
|
+
|
|
153
|
+
operate = tmp_path / ".operate"
|
|
154
|
+
wallets = operate / "wallets"
|
|
155
|
+
wallets.mkdir(parents=True)
|
|
156
|
+
|
|
157
|
+
# Valid key file
|
|
158
|
+
(wallets / "ethereum.txt").write_text(
|
|
159
|
+
json.dumps({"address": VALID_ADDR, "private_key": "a" * 64})
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Invalid JSON
|
|
163
|
+
(wallets / "ethereum.json").write_text("{invalid")
|
|
164
|
+
|
|
165
|
+
services = importer._parse_operate_format(operate)
|
|
166
|
+
assert len(services) == 1 # Still works, just no safe
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_importer_parse_keystore_no_crypto(mock_wallet, tmp_path):
|
|
170
|
+
"""Cover _parse_keystore_file validation (lines 337-338)."""
|
|
171
|
+
with patch("iwa.core.models.Config"):
|
|
172
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
173
|
+
|
|
174
|
+
# Missing crypto field
|
|
175
|
+
ks = tmp_path / "key.json"
|
|
176
|
+
ks.write_text(json.dumps({"address": "0x123"}))
|
|
177
|
+
assert importer._parse_keystore_file(ks) is None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_importer_parse_plaintext_raw_hex(mock_wallet, tmp_path):
|
|
181
|
+
"""Cover _parse_plaintext_key_file with raw hex (lines 400-412)."""
|
|
182
|
+
with patch("iwa.core.models.Config"):
|
|
183
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
184
|
+
|
|
185
|
+
# Raw hex (64 chars)
|
|
186
|
+
pk = tmp_path / "raw.txt"
|
|
187
|
+
pk.write_text("a" * 64)
|
|
188
|
+
key = importer._parse_plaintext_key_file(pk)
|
|
189
|
+
assert key is not None
|
|
190
|
+
assert key.private_key == "a" * 64
|
|
191
|
+
|
|
192
|
+
# With 0x prefix (66 chars)
|
|
193
|
+
pk.write_text("0x" + "b" * 64)
|
|
194
|
+
key = importer._parse_plaintext_key_file(pk)
|
|
195
|
+
assert key is not None
|
|
196
|
+
assert key.private_key == "b" * 64
|
|
197
|
+
|
|
198
|
+
# Invalid length
|
|
199
|
+
pk.write_text("short")
|
|
200
|
+
assert importer._parse_plaintext_key_file(pk) is None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_importer_decrypt_already_decrypted(mock_wallet):
|
|
204
|
+
"""Cover decrypt_key when already decrypted (line 428-429)."""
|
|
205
|
+
with patch("iwa.core.models.Config"):
|
|
206
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
207
|
+
|
|
208
|
+
key = DiscoveredKey(address="0x1", private_key="abc")
|
|
209
|
+
assert importer.decrypt_key(key, "pass") is True
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_importer_decrypt_no_keystore(mock_wallet):
|
|
213
|
+
"""Cover decrypt_key with no keystore (lines 431-433)."""
|
|
214
|
+
with patch("iwa.core.models.Config"):
|
|
215
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
216
|
+
|
|
217
|
+
key = DiscoveredKey(address="0x1", is_encrypted=True, encrypted_keystore=None)
|
|
218
|
+
assert importer.decrypt_key(key, "pass") is False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_importer_import_service_key_errors(mock_wallet):
|
|
222
|
+
"""Cover import_service key failure paths (lines 466-470)."""
|
|
223
|
+
with patch("iwa.core.models.Config"):
|
|
224
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
225
|
+
mock_wallet.key_storage.find_stored_account.return_value = None
|
|
226
|
+
|
|
227
|
+
# Key that needs password but none provided
|
|
228
|
+
svc = DiscoveredService(service_name="t")
|
|
229
|
+
svc.keys.append(
|
|
230
|
+
DiscoveredKey(
|
|
231
|
+
address=VALID_ADDR,
|
|
232
|
+
is_encrypted=True,
|
|
233
|
+
encrypted_keystore={"crypto": {}, "address": "abc"},
|
|
234
|
+
private_key=None,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
result = importer.import_service(svc, password=None)
|
|
239
|
+
assert result.success is False or len(result.errors) > 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_importer_import_service_duplicate(mock_wallet):
|
|
243
|
+
"""Cover import_service duplicate handling (lines 522-524)."""
|
|
244
|
+
with patch("iwa.core.models.Config"):
|
|
245
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
246
|
+
|
|
247
|
+
# Mock find_stored_account to return existing
|
|
248
|
+
mock_wallet.key_storage.find_stored_account.return_value = MagicMock()
|
|
249
|
+
|
|
250
|
+
svc = DiscoveredService(service_name="t")
|
|
251
|
+
svc.keys.append(DiscoveredKey(address=VALID_ADDR, private_key="abc"))
|
|
252
|
+
|
|
253
|
+
result = importer.import_service(svc)
|
|
254
|
+
assert any("already exists" in s for s in result.skipped)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_importer_generate_tag_collision(mock_wallet):
|
|
258
|
+
"""Cover _generate_tag with collisions (lines 570-577, 605-606)."""
|
|
259
|
+
with patch("iwa.core.models.Config"):
|
|
260
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
261
|
+
|
|
262
|
+
# Pre-populate accounts with existing tags
|
|
263
|
+
mock_wallet.key_storage.accounts = {
|
|
264
|
+
"0x1": MagicMock(tag="svc_agent"),
|
|
265
|
+
"0x2": MagicMock(tag="svc_agent_2"),
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
key = DiscoveredKey(address="0x3", role="agent")
|
|
269
|
+
tag = importer._generate_tag(key, "svc")
|
|
270
|
+
assert tag == "svc_agent_3"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_importer_import_safe_duplicate(mock_wallet):
|
|
274
|
+
"""Cover _import_safe duplicate (lines 582-587)."""
|
|
275
|
+
with patch("iwa.core.models.Config"):
|
|
276
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
277
|
+
|
|
278
|
+
mock_wallet.key_storage.find_stored_account.return_value = MagicMock()
|
|
279
|
+
|
|
280
|
+
svc = DiscoveredService(service_name="t", safe_address=VALID_ADDR)
|
|
281
|
+
success, msg = importer._import_safe(svc)
|
|
282
|
+
assert success is False
|
|
283
|
+
assert msg == "duplicate"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_importer_import_service_config_duplicate(mock_wallet):
|
|
287
|
+
"""Cover _import_service_config duplicate (lines 634-635)."""
|
|
288
|
+
with patch("iwa.core.models.Config") as mock_config_cls:
|
|
289
|
+
# Set up the config mock properly
|
|
290
|
+
mock_config = MagicMock()
|
|
291
|
+
mock_olas = MagicMock()
|
|
292
|
+
mock_olas.services = {"gnosis:123": MagicMock()}
|
|
293
|
+
mock_config.plugins = {"olas": mock_olas}
|
|
294
|
+
mock_config_cls.return_value = mock_config
|
|
295
|
+
|
|
296
|
+
importer = OlasServiceImporter(mock_wallet.key_storage)
|
|
297
|
+
# Replace the config instance
|
|
298
|
+
importer.config = mock_config
|
|
299
|
+
|
|
300
|
+
svc = DiscoveredService(service_name="t", service_id=123, chain_name="gnosis")
|
|
301
|
+
|
|
302
|
+
success, msg = importer._import_service_config(svc)
|
|
303
|
+
assert success is False
|
|
304
|
+
assert msg == "duplicate"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# === SERVICE MANAGER GAPS ===
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_sm_create_token_utility_missing(mock_wallet):
|
|
311
|
+
"""Cover create() with missing token utility (lines 204-206)."""
|
|
312
|
+
with patch("iwa.core.models.Config"):
|
|
313
|
+
sm = ServiceManager(mock_wallet)
|
|
314
|
+
|
|
315
|
+
with patch.dict("iwa.plugins.olas.service_manager.base.OLAS_CONTRACTS", {"unknown": {}}):
|
|
316
|
+
# Should not crash, just log error
|
|
317
|
+
sm.chain_name = "unknown"
|
|
318
|
+
# Can't easily test create without more mocks, but we test the path
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_sm_get_staking_status_staked_info_fail(mock_wallet):
|
|
322
|
+
"""Cover get_staking_status with STAKED but get_service_info fails (lines 843-854)."""
|
|
323
|
+
with patch("iwa.core.models.Config"):
|
|
324
|
+
sm = ServiceManager(mock_wallet)
|
|
325
|
+
sm.service = Service(
|
|
326
|
+
service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
mock_staking = MagicMock()
|
|
330
|
+
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
331
|
+
mock_staking.activity_checker_address = VALID_ADDR
|
|
332
|
+
mock_staking.activity_checker.liveness_ratio = 10
|
|
333
|
+
mock_staking.get_service_info.side_effect = Exception("fail")
|
|
334
|
+
|
|
335
|
+
with patch(
|
|
336
|
+
"iwa.plugins.olas.service_manager.staking.StakingContract", return_value=mock_staking
|
|
337
|
+
):
|
|
338
|
+
status = sm.get_staking_status()
|
|
339
|
+
assert status.staking_state == "STAKED"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_sm_call_checkpoint_prepare_fail(mock_wallet):
|
|
343
|
+
"""Cover call_checkpoint prepare failure (lines 1100-1102)."""
|
|
344
|
+
with patch("iwa.core.models.Config"):
|
|
345
|
+
sm = ServiceManager(mock_wallet)
|
|
346
|
+
sm.service = Service(
|
|
347
|
+
service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
mock_staking = MagicMock()
|
|
351
|
+
mock_staking.is_checkpoint_needed.return_value = True
|
|
352
|
+
mock_staking.prepare_checkpoint_tx.return_value = None
|
|
353
|
+
|
|
354
|
+
with patch(
|
|
355
|
+
"iwa.plugins.olas.service_manager.staking.StakingContract", return_value=mock_staking
|
|
356
|
+
):
|
|
357
|
+
result = sm.call_checkpoint()
|
|
358
|
+
assert result is False
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_sm_spin_up_no_service(mock_wallet):
|
|
362
|
+
"""Cover spin_up with no service (lines 1167-1170)."""
|
|
363
|
+
with patch("iwa.core.models.Config"):
|
|
364
|
+
sm = ServiceManager(mock_wallet)
|
|
365
|
+
sm.service = None
|
|
366
|
+
|
|
367
|
+
result = sm.spin_up()
|
|
368
|
+
assert result is False
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_sm_spin_up_activation_fail(mock_wallet):
|
|
372
|
+
"""Cover spin_up activation failure (lines 1181-1183)."""
|
|
373
|
+
with patch("iwa.core.models.Config"):
|
|
374
|
+
sm = ServiceManager(mock_wallet)
|
|
375
|
+
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
376
|
+
|
|
377
|
+
mock_reg = MagicMock()
|
|
378
|
+
mock_reg.get_service.return_value = {"state": ServiceState.PRE_REGISTRATION}
|
|
379
|
+
|
|
380
|
+
with (
|
|
381
|
+
patch.object(sm, "registry", mock_reg),
|
|
382
|
+
patch.object(sm, "activate_registration", return_value=False),
|
|
383
|
+
):
|
|
384
|
+
result = sm.spin_up()
|
|
385
|
+
assert result is False
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_sm_wind_down_no_service(mock_wallet):
|
|
389
|
+
"""Cover wind_down with no service (lines 1264-1266)."""
|
|
390
|
+
with patch("iwa.core.models.Config"):
|
|
391
|
+
sm = ServiceManager(mock_wallet)
|
|
392
|
+
sm.service = None
|
|
393
|
+
|
|
394
|
+
result = sm.wind_down()
|
|
395
|
+
assert result is False
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_sm_wind_down_nonexistent(mock_wallet):
|
|
399
|
+
"""Cover wind_down with non-existent service (lines 1274-1276)."""
|
|
400
|
+
with patch("iwa.core.models.Config"):
|
|
401
|
+
sm = ServiceManager(mock_wallet)
|
|
402
|
+
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
403
|
+
|
|
404
|
+
mock_reg = MagicMock()
|
|
405
|
+
mock_reg.get_service.return_value = {"state": ServiceState.NON_EXISTENT}
|
|
406
|
+
|
|
407
|
+
with patch.object(sm, "registry", mock_reg):
|
|
408
|
+
result = sm.wind_down()
|
|
409
|
+
assert result is False
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_sm_mech_request_no_service(mock_wallet):
|
|
413
|
+
"""Cover _send_legacy_mech_request with no service (lines 1502-1504)."""
|
|
414
|
+
with patch("iwa.core.models.Config"):
|
|
415
|
+
sm = ServiceManager(mock_wallet)
|
|
416
|
+
sm.service = None
|
|
417
|
+
|
|
418
|
+
result = sm._send_legacy_mech_request(b"data")
|
|
419
|
+
assert result is None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_sm_mech_request_no_address(mock_wallet):
|
|
423
|
+
"""Cover _send_legacy_mech_request missing mech address (lines 1510-1512)."""
|
|
424
|
+
with patch("iwa.core.models.Config"):
|
|
425
|
+
sm = ServiceManager(mock_wallet)
|
|
426
|
+
sm.service = Service(service_name="t", chain_name="unknown", service_id=1)
|
|
427
|
+
|
|
428
|
+
result = sm._send_legacy_mech_request(b"data")
|
|
429
|
+
assert result is None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def test_sm_marketplace_mech_no_service(mock_wallet):
|
|
433
|
+
"""Cover _send_marketplace_mech_request with no service (lines 1549-1551)."""
|
|
434
|
+
with patch("iwa.core.models.Config"):
|
|
435
|
+
sm = ServiceManager(mock_wallet)
|
|
436
|
+
sm.service = None
|
|
437
|
+
|
|
438
|
+
result = sm._send_marketplace_mech_request(b"data")
|
|
439
|
+
assert result is None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# === PLUGIN GAPS ===
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_plugin_import_display_variants(mock_wallet):
|
|
446
|
+
"""Cover plugin import display paths (lines 141-166)."""
|
|
447
|
+
import click
|
|
448
|
+
|
|
449
|
+
with (
|
|
450
|
+
patch("iwa.core.models.Config"),
|
|
451
|
+
patch("iwa.plugins.olas.importer.OlasServiceImporter") as mock_imp,
|
|
452
|
+
patch("rich.console.Console"),
|
|
453
|
+
patch("typer.confirm", return_value=False),
|
|
454
|
+
):
|
|
455
|
+
# Service with various states
|
|
456
|
+
svc = DiscoveredService(
|
|
457
|
+
service_name="test",
|
|
458
|
+
format="trader",
|
|
459
|
+
source_folder=Path("/tmp"),
|
|
460
|
+
chain_name="gnosis",
|
|
461
|
+
safe_address=VALID_ADDR,
|
|
462
|
+
)
|
|
463
|
+
svc.keys.append(DiscoveredKey(address=VALID_ADDR, is_encrypted=True, role="agent"))
|
|
464
|
+
|
|
465
|
+
mock_imp.return_value.scan_directory.return_value = [svc]
|
|
466
|
+
|
|
467
|
+
plugin = OlasPlugin()
|
|
468
|
+
|
|
469
|
+
# Test safe_exists=None (cannot verify)
|
|
470
|
+
with patch.object(plugin, "_get_safe_signers", return_value=([], None)):
|
|
471
|
+
try:
|
|
472
|
+
plugin.import_services(path="/tmp", dry_run=True, yes=False)
|
|
473
|
+
except (SystemExit, click.exceptions.Exit):
|
|
474
|
+
pass
|
|
475
|
+
|
|
476
|
+
# Test safe_exists=False (doesn't exist)
|
|
477
|
+
with patch.object(plugin, "_get_safe_signers", return_value=([], False)):
|
|
478
|
+
try:
|
|
479
|
+
plugin.import_services(path="/tmp", dry_run=True, yes=False)
|
|
480
|
+
except (SystemExit, click.exceptions.Exit):
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
# Test safe_exists=True but not a signer
|
|
484
|
+
with patch.object(plugin, "_get_safe_signers", return_value=(["0xother"], True)):
|
|
485
|
+
try:
|
|
486
|
+
plugin.import_services(path="/tmp", dry_run=True, yes=False)
|
|
487
|
+
except (SystemExit, click.exceptions.Exit):
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# === OLAS VIEW GAPS ===
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class OlasTestApp(App):
|
|
495
|
+
"""Test app to host OlasView."""
|
|
496
|
+
|
|
497
|
+
def __init__(self, wallet=None):
|
|
498
|
+
"""Initialize test app."""
|
|
499
|
+
super().__init__()
|
|
500
|
+
self.wallet = wallet
|
|
501
|
+
|
|
502
|
+
def compose(self) -> ComposeResult:
|
|
503
|
+
"""Compose layout."""
|
|
504
|
+
yield VerticalScroll(OlasView(self.wallet), id="root-container")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@pytest.mark.asyncio
|
|
508
|
+
async def test_view_button_handlers(mock_wallet):
|
|
509
|
+
"""Cover on_button_pressed handlers (lines 121-149)."""
|
|
510
|
+
with patch("iwa.core.models.Config"):
|
|
511
|
+
app = OlasTestApp(mock_wallet)
|
|
512
|
+
async with app.run_test():
|
|
513
|
+
view = app.query_one(OlasView)
|
|
514
|
+
|
|
515
|
+
# Test various button events
|
|
516
|
+
for btn_id, method in [
|
|
517
|
+
("olas-refresh-btn", "load_services"),
|
|
518
|
+
("olas-create-service-btn", "show_create_service_modal"),
|
|
519
|
+
("claim-gnosis_1", "claim_rewards"),
|
|
520
|
+
("unstake-gnosis_1", "unstake_service"),
|
|
521
|
+
("stake-gnosis_1", "stake_service"),
|
|
522
|
+
("drain-gnosis_1", "drain_service"),
|
|
523
|
+
("fund-gnosis_1", "show_fund_service_modal"),
|
|
524
|
+
("terminate-gnosis_1", "terminate_service"),
|
|
525
|
+
("checkpoint-gnosis_1", "checkpoint_service"),
|
|
526
|
+
]:
|
|
527
|
+
mock_event = MagicMock()
|
|
528
|
+
mock_event.button.id = btn_id
|
|
529
|
+
|
|
530
|
+
with patch.object(view, method, create=True) as mock_method:
|
|
531
|
+
view.on_button_pressed(mock_event)
|
|
532
|
+
assert mock_method.called or btn_id.startswith("olas-")
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@pytest.mark.asyncio
|
|
536
|
+
async def test_view_render_empty(mock_wallet):
|
|
537
|
+
"""Cover _render_services with empty list (line 226)."""
|
|
538
|
+
with patch("iwa.core.models.Config"):
|
|
539
|
+
app = OlasTestApp(mock_wallet)
|
|
540
|
+
async with app.run_test() as pilot:
|
|
541
|
+
view = app.query_one(OlasView)
|
|
542
|
+
|
|
543
|
+
await view._render_services([])
|
|
544
|
+
await pilot.pause()
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@pytest.mark.asyncio
|
|
548
|
+
async def test_view_render_with_services(mock_wallet):
|
|
549
|
+
"""Cover _render_services with services (lines 228-232)."""
|
|
550
|
+
with patch("iwa.core.models.Config"):
|
|
551
|
+
app = OlasTestApp(mock_wallet)
|
|
552
|
+
async with app.run_test() as pilot:
|
|
553
|
+
view = app.query_one(OlasView)
|
|
554
|
+
|
|
555
|
+
service = Service(
|
|
556
|
+
service_name="test", chain_name="gnosis", service_id=1, multisig_address=VALID_ADDR
|
|
557
|
+
)
|
|
558
|
+
status = StakingStatus(is_staked=False, staking_state="NOT_STAKED")
|
|
559
|
+
|
|
560
|
+
await view._render_services([("gnosis:1", service, status)])
|
|
561
|
+
await pilot.pause()
|