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,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