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,360 @@
1
+ """Tests for staking rewards claim, withdraw, and checkpoint functionality."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.plugins.olas.contracts.staking import StakingState
8
+ from iwa.plugins.olas.models import Service
9
+ from iwa.plugins.olas.service_manager import ServiceManager
10
+
11
+ VALID_ADDR = "0x1234567890123456789012345678901234567890"
12
+ WITHDRAWAL_ADDR = "0xABCDEFabcdefABCDEFabcdefABCDEFabcdefABCD"
13
+
14
+
15
+ @pytest.fixture
16
+ def mock_wallet():
17
+ """Create a mock wallet for tests."""
18
+ wallet = MagicMock()
19
+ wallet.master_account.address = VALID_ADDR
20
+ wallet.chain_interface.chain_name = "gnosis"
21
+ wallet.sign_and_send_transaction.return_value = (True, {"transactionHash": b"\x00" * 32})
22
+ wallet.send.return_value = "0xtxhash"
23
+ return wallet
24
+
25
+
26
+ def setup_manager(mock_wallet):
27
+ """Setup a ServiceManager with mocked dependencies."""
28
+ with patch("iwa.plugins.olas.service_manager.base.Config") as mock_cfg_cls:
29
+ mock_cfg = mock_cfg_cls.return_value
30
+ mock_olas_config = MagicMock()
31
+ mock_olas_config.get_service.return_value = None
32
+ mock_olas_config.withdrawal_address = None
33
+ mock_cfg.plugins = {"olas": mock_olas_config}
34
+ with patch(
35
+ "iwa.plugins.olas.service_manager.OLAS_CONTRACTS",
36
+ {
37
+ "gnosis": {
38
+ "OLAS_SERVICE_REGISTRY": VALID_ADDR,
39
+ "OLAS_SERVICE_MANAGER": VALID_ADDR,
40
+ }
41
+ },
42
+ ):
43
+ with patch("iwa.plugins.olas.service_manager.base.ChainInterfaces") as mock_if_cls:
44
+ mock_if = mock_if_cls.return_value
45
+ mock_if.get.return_value.chain.name.lower.return_value = "gnosis"
46
+ mock_if.get.return_value.get_contract_address.return_value = VALID_ADDR
47
+ manager = ServiceManager(mock_wallet)
48
+ manager.registry = MagicMock()
49
+ manager.manager_contract = MagicMock()
50
+ manager.olas_config = mock_olas_config
51
+ manager.chain_name = "gnosis"
52
+ return manager
53
+
54
+
55
+ def test_claim_rewards_no_service(mock_wallet):
56
+ """Test claim_rewards fails when no service is loaded."""
57
+ manager = setup_manager(mock_wallet)
58
+ manager.service = None
59
+
60
+ success, amount = manager.claim_rewards()
61
+
62
+ assert success is False
63
+ assert amount == 0
64
+
65
+
66
+ def test_claim_rewards_not_staked(mock_wallet):
67
+ """Test claim_rewards fails when service is not staked."""
68
+ manager = setup_manager(mock_wallet)
69
+ manager.service = Service(service_name="test", chain_name="gnosis", service_id=1, agent_ids=[1])
70
+ manager.service.staking_contract_address = None
71
+
72
+ success, amount = manager.claim_rewards()
73
+
74
+ assert success is False
75
+ assert amount == 0
76
+
77
+
78
+ def test_claim_rewards_service_not_staked_state(mock_wallet):
79
+ """Test claim_rewards fails when staking state is NOT_STAKED."""
80
+ manager = setup_manager(mock_wallet)
81
+ manager.service = Service(
82
+ service_name="test",
83
+ chain_name="gnosis",
84
+ service_id=1,
85
+ agent_ids=[1],
86
+ staking_contract_address=VALID_ADDR,
87
+ )
88
+
89
+ mock_staking = MagicMock()
90
+ mock_staking.get_staking_state.return_value = StakingState.NOT_STAKED
91
+
92
+ success, amount = manager.claim_rewards(staking_contract=mock_staking)
93
+
94
+ assert success is False
95
+ assert amount == 0
96
+
97
+
98
+ def test_claim_rewards_no_accrued_rewards(mock_wallet):
99
+ """Test claim_rewards fails when no rewards are accrued."""
100
+ manager = setup_manager(mock_wallet)
101
+ manager.service = Service(
102
+ service_name="test",
103
+ chain_name="gnosis",
104
+ service_id=1,
105
+ agent_ids=[1],
106
+ staking_contract_address=VALID_ADDR,
107
+ )
108
+
109
+ mock_staking = MagicMock()
110
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
111
+ mock_staking.get_accrued_rewards.return_value = 0
112
+
113
+ success, amount = manager.claim_rewards(staking_contract=mock_staking)
114
+
115
+ assert success is False
116
+ assert amount == 0
117
+
118
+
119
+ def test_claim_rewards_success(mock_wallet):
120
+ """Test claim_rewards succeeds with proper setup."""
121
+ manager = setup_manager(mock_wallet)
122
+ manager.service = Service(
123
+ service_name="test",
124
+ chain_name="gnosis",
125
+ service_id=1,
126
+ agent_ids=[1],
127
+ staking_contract_address=VALID_ADDR,
128
+ )
129
+
130
+ mock_staking = MagicMock()
131
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
132
+ mock_staking.get_accrued_rewards.return_value = 10 * 10**18 # 10 OLAS
133
+ mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
134
+ mock_staking.extract_events.return_value = [{"name": "RewardClaimed"}]
135
+
136
+ success, amount = manager.claim_rewards(staking_contract=mock_staking)
137
+
138
+ assert success is True
139
+ assert amount == 10 * 10**18
140
+
141
+
142
+ def test_claim_rewards_tx_fails(mock_wallet):
143
+ """Test claim_rewards fails when transaction fails."""
144
+ manager = setup_manager(mock_wallet)
145
+ manager.service = Service(
146
+ service_name="test",
147
+ chain_name="gnosis",
148
+ service_id=1,
149
+ agent_ids=[1],
150
+ staking_contract_address=VALID_ADDR,
151
+ )
152
+
153
+ mock_staking = MagicMock()
154
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
155
+ mock_staking.get_accrued_rewards.return_value = 10 * 10**18
156
+ mock_staking.prepare_claim_tx.return_value = {"data": "0x"}
157
+ mock_wallet.sign_and_send_transaction.return_value = (False, {})
158
+
159
+ success, amount = manager.claim_rewards(staking_contract=mock_staking)
160
+
161
+ assert success is False
162
+ assert amount == 0
163
+
164
+
165
+ def test_withdraw_rewards_no_service(mock_wallet):
166
+ """Test withdraw_rewards fails when no service is loaded."""
167
+ manager = setup_manager(mock_wallet)
168
+ manager.service = None
169
+
170
+ success, amount = manager.withdraw_rewards()
171
+
172
+ assert success is False
173
+ assert amount == 0
174
+
175
+
176
+ def test_withdraw_rewards_no_multisig(mock_wallet):
177
+ """Test withdraw_rewards fails when service has no multisig."""
178
+ manager = setup_manager(mock_wallet)
179
+ manager.service = Service(service_name="test", chain_name="gnosis", service_id=1, agent_ids=[1])
180
+ manager.service.multisig_address = None
181
+
182
+ success, amount = manager.withdraw_rewards()
183
+
184
+ assert success is False
185
+ assert amount == 0
186
+
187
+
188
+ def test_withdraw_rewards_no_withdrawal_address(mock_wallet):
189
+ """Test withdraw_rewards fails when no withdrawal address configured."""
190
+ manager = setup_manager(mock_wallet)
191
+ manager.service = Service(
192
+ service_name="test",
193
+ chain_name="gnosis",
194
+ service_id=1,
195
+ agent_ids=[1],
196
+ multisig_address=VALID_ADDR,
197
+ )
198
+ manager.olas_config.withdrawal_address = None
199
+
200
+ success, amount = manager.withdraw_rewards()
201
+
202
+ assert success is False
203
+ assert amount == 0
204
+
205
+
206
+ def test_withdraw_rewards_no_olas_balance(mock_wallet):
207
+ """Test withdraw_rewards fails when Safe has no OLAS balance."""
208
+ manager = setup_manager(mock_wallet)
209
+ manager.service = Service(
210
+ service_name="test",
211
+ chain_name="gnosis",
212
+ service_id=1,
213
+ agent_ids=[1],
214
+ multisig_address=VALID_ADDR,
215
+ )
216
+ manager.olas_config.withdrawal_address = WITHDRAWAL_ADDR
217
+
218
+ with patch("iwa.plugins.olas.service_manager.drain.ERC20Contract") as mock_erc20_cls:
219
+ mock_erc20 = mock_erc20_cls.return_value
220
+ mock_erc20.balance_of_wei.return_value = 0
221
+
222
+ success, amount = manager.withdraw_rewards()
223
+
224
+ assert success is False
225
+ assert amount == 0
226
+
227
+
228
+ def test_withdraw_rewards_success(mock_wallet):
229
+ """Test withdraw_rewards succeeds with proper setup."""
230
+ manager = setup_manager(mock_wallet)
231
+ manager.service = Service(
232
+ service_name="test",
233
+ chain_name="gnosis",
234
+ service_id=1,
235
+ agent_ids=[1],
236
+ multisig_address=VALID_ADDR,
237
+ )
238
+ manager.olas_config.withdrawal_address = WITHDRAWAL_ADDR
239
+
240
+ with patch("iwa.plugins.olas.service_manager.drain.ERC20Contract") as mock_erc20_cls:
241
+ mock_erc20 = mock_erc20_cls.return_value
242
+ mock_erc20.balance_of_wei.return_value = 50 * 10**18 # 50 OLAS
243
+
244
+ success, amount = manager.withdraw_rewards()
245
+
246
+ assert success is True
247
+ assert amount == 50.0
248
+ mock_wallet.send.assert_called_once()
249
+
250
+
251
+ def test_withdraw_rewards_transfer_fails(mock_wallet):
252
+ """Test withdraw_rewards fails when transfer fails."""
253
+ manager = setup_manager(mock_wallet)
254
+ manager.service = Service(
255
+ service_name="test",
256
+ chain_name="gnosis",
257
+ service_id=1,
258
+ agent_ids=[1],
259
+ multisig_address=VALID_ADDR,
260
+ )
261
+ manager.olas_config.withdrawal_address = WITHDRAWAL_ADDR
262
+ mock_wallet.send.return_value = None # Transfer fails
263
+
264
+ with patch("iwa.plugins.olas.service_manager.drain.ERC20Contract") as mock_erc20_cls:
265
+ mock_erc20 = mock_erc20_cls.return_value
266
+ mock_erc20.balance_of_wei.return_value = 50 * 10**18
267
+
268
+ success, amount = manager.withdraw_rewards()
269
+
270
+ assert success is False
271
+ assert amount == 0
272
+
273
+
274
+ # ============================================================================
275
+ # Checkpoint Tests
276
+ # ============================================================================
277
+
278
+
279
+ def test_call_checkpoint_no_service(mock_wallet):
280
+ """Test call_checkpoint fails when no service is loaded."""
281
+ manager = setup_manager(mock_wallet)
282
+ manager.service = None
283
+
284
+ result = manager.call_checkpoint()
285
+
286
+ assert result is False
287
+
288
+
289
+ def test_call_checkpoint_not_staked(mock_wallet):
290
+ """Test call_checkpoint fails when service is not staked."""
291
+ manager = setup_manager(mock_wallet)
292
+ manager.service = Service(service_name="test", chain_name="gnosis", service_id=1, agent_ids=[1])
293
+ manager.service.staking_contract_address = None
294
+
295
+ result = manager.call_checkpoint()
296
+
297
+ assert result is False
298
+
299
+
300
+ def test_call_checkpoint_not_needed(mock_wallet):
301
+ """Test call_checkpoint returns False when checkpoint is not needed."""
302
+ manager = setup_manager(mock_wallet)
303
+ manager.service = Service(
304
+ service_name="test",
305
+ chain_name="gnosis",
306
+ service_id=1,
307
+ agent_ids=[1],
308
+ staking_contract_address=VALID_ADDR,
309
+ )
310
+
311
+ mock_staking = MagicMock()
312
+ mock_staking.is_checkpoint_needed.return_value = False
313
+ mock_staking.get_next_epoch_start.return_value = MagicMock()
314
+
315
+ result = manager.call_checkpoint(staking_contract=mock_staking)
316
+
317
+ assert result is False
318
+
319
+
320
+ def test_call_checkpoint_success(mock_wallet):
321
+ """Test call_checkpoint succeeds with proper setup."""
322
+ manager = setup_manager(mock_wallet)
323
+ manager.service = Service(
324
+ service_name="test",
325
+ chain_name="gnosis",
326
+ service_id=1,
327
+ agent_ids=[1],
328
+ staking_contract_address=VALID_ADDR,
329
+ )
330
+
331
+ mock_staking = MagicMock()
332
+ mock_staking.is_checkpoint_needed.return_value = True
333
+ mock_staking.prepare_checkpoint_tx.return_value = {"data": "0x", "gas": 4_000_000}
334
+ mock_staking.extract_events.return_value = [{"name": "Checkpoint"}]
335
+
336
+ result = manager.call_checkpoint(staking_contract=mock_staking)
337
+
338
+ assert result is True
339
+ mock_staking.prepare_checkpoint_tx.assert_called_once()
340
+
341
+
342
+ def test_call_checkpoint_tx_fails(mock_wallet):
343
+ """Test call_checkpoint fails when transaction fails."""
344
+ manager = setup_manager(mock_wallet)
345
+ manager.service = Service(
346
+ service_name="test",
347
+ chain_name="gnosis",
348
+ service_id=1,
349
+ agent_ids=[1],
350
+ staking_contract_address=VALID_ADDR,
351
+ )
352
+
353
+ mock_staking = MagicMock()
354
+ mock_staking.is_checkpoint_needed.return_value = True
355
+ mock_staking.prepare_checkpoint_tx.return_value = {"data": "0x", "gas": 4_000_000}
356
+ mock_wallet.sign_and_send_transaction.return_value = (False, {})
357
+
358
+ result = manager.call_checkpoint(staking_contract=mock_staking)
359
+
360
+ assert result is False
@@ -0,0 +1,145 @@
1
+ """Tests for Olas ServiceManager validation and failure handling."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.plugins.olas.contracts.service import ServiceState
8
+ from iwa.plugins.olas.models import Service
9
+ from iwa.plugins.olas.service_manager import ServiceManager
10
+
11
+ VALID_ADDR_1 = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
12
+ VALID_ADDR_2 = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D"
13
+
14
+
15
+ @pytest.fixture
16
+ def mock_wallet():
17
+ """Mock wallet."""
18
+ wallet = MagicMock()
19
+ wallet.master_account.address = VALID_ADDR_1
20
+ wallet.transfer_service = MagicMock()
21
+ return wallet
22
+
23
+
24
+ @pytest.fixture
25
+ def sm(mock_wallet):
26
+ """ServiceManager fixture."""
27
+ with patch("iwa.core.models.Config"):
28
+ manager = ServiceManager(mock_wallet)
29
+ # Mock service
30
+ manager.service = Service(
31
+ service_id=1,
32
+ service_name="Test",
33
+ chain_name="gnosis",
34
+ agent_address=VALID_ADDR_1,
35
+ multisig_address=VALID_ADDR_2,
36
+ staking_contract_address=VALID_ADDR_1,
37
+ )
38
+ return manager
39
+
40
+
41
+ def test_drain_service_partial_failures(sm, mock_wallet):
42
+ """Test drain_service handles partial failures across accounts."""
43
+ # Setup:
44
+ # 1. Claim success
45
+ # 2. Safe drain failure
46
+ # 3. Agent drain success
47
+
48
+ with patch.object(sm, "claim_rewards", return_value=(True, 10**18)):
49
+ # Wallet.drain is called for Safe and Agent
50
+ def mock_drain(from_address_or_tag=None, to_address_or_tag=None, chain_name=None):
51
+ if from_address_or_tag == VALID_ADDR_2: # Safe
52
+ raise Exception("Safe drain failed")
53
+ return {"native": 0.5}
54
+
55
+ mock_wallet.drain.side_effect = mock_drain
56
+
57
+ result = sm.drain_service()
58
+
59
+ assert "safe" not in result
60
+ assert "agent" in result
61
+ assert result["agent"]["native"] == 0.5
62
+ # Verify it continued after Safe failure
63
+
64
+
65
+ def test_unstake_failed_event_extraction(sm):
66
+ """Test unstake when transaction succeeds but event extraction fails."""
67
+ staking_mock = MagicMock()
68
+ sm.wallet.sign_and_send_transaction.return_value = (True, {"tx_hash": "0x123"})
69
+ staking_mock.extract_events.return_value = [] # No Unstaked event
70
+
71
+ success = sm.unstake(staking_mock)
72
+ assert success is False # Should return False if event missing
73
+
74
+
75
+ def test_call_checkpoint_grace_period(sm):
76
+ """Test call_checkpoint respect for grace period."""
77
+ staking_mock = MagicMock()
78
+ # Mock status where epoch ended very recently (within grace period)
79
+ staking_mock.get_staking_status.return_value = {
80
+ "remaining_epoch_seconds": -50 # Ended 50s ago
81
+ }
82
+ # Mock is_checkpoint_needed to return False based on grace period
83
+ staking_mock.is_checkpoint_needed.return_value = False
84
+ staking_mock.get_next_epoch_start.return_value = MagicMock()
85
+
86
+ # grace_period_seconds defaults to 600
87
+ success = sm.call_checkpoint(staking_mock, grace_period_seconds=600)
88
+
89
+ # Should skip checkpoint and return False (matching SM logic)
90
+ assert success is False
91
+ sm.wallet.sign_and_send_transaction.assert_not_called()
92
+
93
+
94
+ def test_call_checkpoint_success(sm):
95
+ """Test successful call_checkpoint."""
96
+ staking_mock = MagicMock()
97
+ staking_mock.is_checkpoint_needed.return_value = True
98
+ staking_mock.prepare_checkpoint_tx.return_value = {"to": "0x123", "data": "0x"}
99
+ sm.wallet.sign_and_send_transaction.return_value = (True, {"tx_hash": "0x123", "logs": []})
100
+ staking_mock.extract_events.return_value = [
101
+ {"name": "Checkpoint", "args": {"epoch": 1, "availableRewards": 10**18}}
102
+ ]
103
+
104
+ success = sm.call_checkpoint(staking_mock)
105
+ assert success is True
106
+ sm.wallet.sign_and_send_transaction.assert_called_once()
107
+
108
+
109
+ def test_spin_up_intermediate_failure(sm):
110
+ """Test spin_up stops at first failure."""
111
+ # Mock sequential calls using ServiceState enum on the registry mock
112
+ with patch.object(
113
+ sm.registry,
114
+ "get_service",
115
+ side_effect=[
116
+ {"state": ServiceState.PRE_REGISTRATION},
117
+ {"state": ServiceState.ACTIVE_REGISTRATION},
118
+ {"state": ServiceState.ACTIVE_REGISTRATION}, # Verification after activate
119
+ {"state": ServiceState.ACTIVE_REGISTRATION}, # Final verification (if it reached there)
120
+ {"state": ServiceState.ACTIVE_REGISTRATION}, # One more just in case
121
+ ],
122
+ ):
123
+ with (
124
+ patch.object(sm, "activate_registration", return_value=True) as m1,
125
+ patch.object(sm, "register_agent", return_value=False) as m2,
126
+ patch.object(sm, "deploy") as m3,
127
+ ):
128
+ success = sm.spin_up()
129
+
130
+ assert success is False
131
+ m1.assert_called_once()
132
+ m2.assert_called_once()
133
+ m3.assert_not_called()
134
+
135
+
136
+ def test_service_manager_no_service_error_handling(mock_wallet):
137
+ """Test methods return gracefully when no service is selected."""
138
+ with patch("iwa.core.models.Config"):
139
+ manager = ServiceManager(mock_wallet)
140
+ manager.service = None
141
+
142
+ assert manager.get() is None
143
+ assert manager.drain_service() == {}
144
+ assert manager.get_staking_status() is None
145
+ assert manager.claim_rewards() == (False, 0)