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