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.
Files changed (191) hide show
  1. conftest.py +22 -0
  2. iwa/__init__.py +1 -0
  3. iwa/__main__.py +6 -0
  4. iwa/core/__init__.py +1 -0
  5. iwa/core/chain/__init__.py +68 -0
  6. iwa/core/chain/errors.py +47 -0
  7. iwa/core/chain/interface.py +514 -0
  8. iwa/core/chain/manager.py +38 -0
  9. iwa/core/chain/models.py +128 -0
  10. iwa/core/chain/rate_limiter.py +193 -0
  11. iwa/core/cli.py +210 -0
  12. iwa/core/constants.py +28 -0
  13. iwa/core/contracts/__init__.py +1 -0
  14. iwa/core/contracts/contract.py +297 -0
  15. iwa/core/contracts/erc20.py +79 -0
  16. iwa/core/contracts/multisend.py +71 -0
  17. iwa/core/db.py +317 -0
  18. iwa/core/keys.py +361 -0
  19. iwa/core/mnemonic.py +385 -0
  20. iwa/core/models.py +344 -0
  21. iwa/core/monitor.py +209 -0
  22. iwa/core/plugins.py +45 -0
  23. iwa/core/pricing.py +91 -0
  24. iwa/core/services/__init__.py +17 -0
  25. iwa/core/services/account.py +57 -0
  26. iwa/core/services/balance.py +113 -0
  27. iwa/core/services/plugin.py +88 -0
  28. iwa/core/services/safe.py +392 -0
  29. iwa/core/services/transaction.py +172 -0
  30. iwa/core/services/transfer/__init__.py +166 -0
  31. iwa/core/services/transfer/base.py +260 -0
  32. iwa/core/services/transfer/erc20.py +247 -0
  33. iwa/core/services/transfer/multisend.py +386 -0
  34. iwa/core/services/transfer/native.py +262 -0
  35. iwa/core/services/transfer/swap.py +326 -0
  36. iwa/core/settings.py +95 -0
  37. iwa/core/tables.py +60 -0
  38. iwa/core/test.py +27 -0
  39. iwa/core/tests/test_wallet.py +255 -0
  40. iwa/core/types.py +59 -0
  41. iwa/core/ui.py +99 -0
  42. iwa/core/utils.py +59 -0
  43. iwa/core/wallet.py +380 -0
  44. iwa/plugins/__init__.py +1 -0
  45. iwa/plugins/gnosis/__init__.py +5 -0
  46. iwa/plugins/gnosis/cow/__init__.py +6 -0
  47. iwa/plugins/gnosis/cow/quotes.py +148 -0
  48. iwa/plugins/gnosis/cow/swap.py +403 -0
  49. iwa/plugins/gnosis/cow/types.py +20 -0
  50. iwa/plugins/gnosis/cow_utils.py +44 -0
  51. iwa/plugins/gnosis/plugin.py +68 -0
  52. iwa/plugins/gnosis/safe.py +157 -0
  53. iwa/plugins/gnosis/tests/test_cow.py +227 -0
  54. iwa/plugins/gnosis/tests/test_safe.py +100 -0
  55. iwa/plugins/olas/__init__.py +5 -0
  56. iwa/plugins/olas/constants.py +106 -0
  57. iwa/plugins/olas/contracts/activity_checker.py +93 -0
  58. iwa/plugins/olas/contracts/base.py +10 -0
  59. iwa/plugins/olas/contracts/mech.py +49 -0
  60. iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
  61. iwa/plugins/olas/contracts/service.py +215 -0
  62. iwa/plugins/olas/contracts/staking.py +403 -0
  63. iwa/plugins/olas/importer.py +736 -0
  64. iwa/plugins/olas/mech_reference.py +135 -0
  65. iwa/plugins/olas/models.py +110 -0
  66. iwa/plugins/olas/plugin.py +243 -0
  67. iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
  68. iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
  69. iwa/plugins/olas/service_manager/__init__.py +60 -0
  70. iwa/plugins/olas/service_manager/base.py +113 -0
  71. iwa/plugins/olas/service_manager/drain.py +336 -0
  72. iwa/plugins/olas/service_manager/lifecycle.py +839 -0
  73. iwa/plugins/olas/service_manager/mech.py +322 -0
  74. iwa/plugins/olas/service_manager/staking.py +530 -0
  75. iwa/plugins/olas/tests/conftest.py +30 -0
  76. iwa/plugins/olas/tests/test_importer.py +128 -0
  77. iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
  78. iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
  79. iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
  80. iwa/plugins/olas/tests/test_olas_integration.py +561 -0
  81. iwa/plugins/olas/tests/test_olas_models.py +144 -0
  82. iwa/plugins/olas/tests/test_olas_view.py +258 -0
  83. iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
  84. iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
  85. iwa/plugins/olas/tests/test_plugin.py +70 -0
  86. iwa/plugins/olas/tests/test_plugin_full.py +212 -0
  87. iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
  88. iwa/plugins/olas/tests/test_service_manager.py +1065 -0
  89. iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
  90. iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
  91. iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
  92. iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
  93. iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
  94. iwa/plugins/olas/tests/test_service_staking.py +342 -0
  95. iwa/plugins/olas/tests/test_staking_integration.py +269 -0
  96. iwa/plugins/olas/tests/test_staking_validation.py +109 -0
  97. iwa/plugins/olas/tui/__init__.py +1 -0
  98. iwa/plugins/olas/tui/olas_view.py +952 -0
  99. iwa/tools/check_profile.py +67 -0
  100. iwa/tools/release.py +111 -0
  101. iwa/tools/reset_env.py +111 -0
  102. iwa/tools/reset_tenderly.py +362 -0
  103. iwa/tools/restore_backup.py +82 -0
  104. iwa/tui/__init__.py +1 -0
  105. iwa/tui/app.py +174 -0
  106. iwa/tui/modals/__init__.py +5 -0
  107. iwa/tui/modals/base.py +406 -0
  108. iwa/tui/rpc.py +63 -0
  109. iwa/tui/screens/__init__.py +1 -0
  110. iwa/tui/screens/wallets.py +749 -0
  111. iwa/tui/tests/test_app.py +125 -0
  112. iwa/tui/tests/test_rpc.py +139 -0
  113. iwa/tui/tests/test_wallets_refactor.py +30 -0
  114. iwa/tui/tests/test_widgets.py +123 -0
  115. iwa/tui/widgets/__init__.py +5 -0
  116. iwa/tui/widgets/base.py +100 -0
  117. iwa/tui/workers.py +42 -0
  118. iwa/web/dependencies.py +76 -0
  119. iwa/web/models.py +76 -0
  120. iwa/web/routers/accounts.py +115 -0
  121. iwa/web/routers/olas/__init__.py +24 -0
  122. iwa/web/routers/olas/admin.py +169 -0
  123. iwa/web/routers/olas/funding.py +135 -0
  124. iwa/web/routers/olas/general.py +29 -0
  125. iwa/web/routers/olas/services.py +378 -0
  126. iwa/web/routers/olas/staking.py +341 -0
  127. iwa/web/routers/state.py +65 -0
  128. iwa/web/routers/swap.py +617 -0
  129. iwa/web/routers/transactions.py +153 -0
  130. iwa/web/server.py +155 -0
  131. iwa/web/tests/test_web_endpoints.py +713 -0
  132. iwa/web/tests/test_web_olas.py +430 -0
  133. iwa/web/tests/test_web_swap.py +103 -0
  134. iwa-0.0.1a2.dist-info/METADATA +234 -0
  135. iwa-0.0.1a2.dist-info/RECORD +186 -0
  136. iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
  137. iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
  138. iwa-0.0.1a2.dist-info/top_level.txt +4 -0
  139. tests/legacy_cow.py +248 -0
  140. tests/legacy_safe.py +93 -0
  141. tests/legacy_transaction_retry_logic.py +51 -0
  142. tests/legacy_tui.py +440 -0
  143. tests/legacy_wallets_screen.py +554 -0
  144. tests/legacy_web.py +243 -0
  145. tests/test_account_service.py +120 -0
  146. tests/test_balance_service.py +186 -0
  147. tests/test_chain.py +490 -0
  148. tests/test_chain_interface.py +210 -0
  149. tests/test_cli.py +139 -0
  150. tests/test_contract.py +195 -0
  151. tests/test_db.py +180 -0
  152. tests/test_drain_coverage.py +174 -0
  153. tests/test_erc20.py +95 -0
  154. tests/test_gnosis_plugin.py +111 -0
  155. tests/test_keys.py +449 -0
  156. tests/test_legacy_wallet.py +1285 -0
  157. tests/test_main.py +13 -0
  158. tests/test_mnemonic.py +217 -0
  159. tests/test_modals.py +109 -0
  160. tests/test_models.py +213 -0
  161. tests/test_monitor.py +202 -0
  162. tests/test_multisend.py +84 -0
  163. tests/test_plugin_service.py +119 -0
  164. tests/test_pricing.py +143 -0
  165. tests/test_rate_limiter.py +199 -0
  166. tests/test_reset_tenderly.py +202 -0
  167. tests/test_rpc_view.py +73 -0
  168. tests/test_safe_coverage.py +139 -0
  169. tests/test_safe_service.py +168 -0
  170. tests/test_service_manager_integration.py +61 -0
  171. tests/test_service_manager_structure.py +31 -0
  172. tests/test_service_transaction.py +176 -0
  173. tests/test_staking_router.py +71 -0
  174. tests/test_staking_simple.py +31 -0
  175. tests/test_tables.py +76 -0
  176. tests/test_transaction_service.py +161 -0
  177. tests/test_transfer_multisend.py +179 -0
  178. tests/test_transfer_native.py +220 -0
  179. tests/test_transfer_security.py +93 -0
  180. tests/test_transfer_structure.py +37 -0
  181. tests/test_transfer_swap_unit.py +155 -0
  182. tests/test_ui_coverage.py +66 -0
  183. tests/test_utils.py +53 -0
  184. tests/test_workers.py +91 -0
  185. tools/verify_drain.py +183 -0
  186. __init__.py +0 -2
  187. hello.py +0 -6
  188. iwa-0.0.0.dist-info/METADATA +0 -10
  189. iwa-0.0.0.dist-info/RECORD +0 -6
  190. iwa-0.0.0.dist-info/top_level.txt +0 -2
  191. {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()