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,168 @@
|
|
|
1
|
+
"""Tests for core SafeService."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.keys import EncryptedAccount, KeyStorage
|
|
8
|
+
from iwa.core.services.safe import SafeService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_key_storage():
|
|
13
|
+
"""Mock key storage."""
|
|
14
|
+
mock = MagicMock(spec=KeyStorage)
|
|
15
|
+
mock.accounts = {}
|
|
16
|
+
|
|
17
|
+
# Mock find_stored_account to return appropriate account types
|
|
18
|
+
def find_account(tag_or_addr):
|
|
19
|
+
if tag_or_addr == "deployer":
|
|
20
|
+
acc = MagicMock(spec=EncryptedAccount)
|
|
21
|
+
# Valid checksum address - Deployer
|
|
22
|
+
acc.address = "0xAB7C8803962c0f2F5BBBe3FA8BF0Dcd705084223"
|
|
23
|
+
return acc
|
|
24
|
+
if tag_or_addr == "owner1":
|
|
25
|
+
acc = MagicMock(spec=EncryptedAccount)
|
|
26
|
+
# Valid checksum address - Owner
|
|
27
|
+
acc.address = "0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c"
|
|
28
|
+
return acc
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
mock.find_stored_account.side_effect = find_account
|
|
32
|
+
|
|
33
|
+
# Mock private key retrieval
|
|
34
|
+
mock._get_private_key.return_value = (
|
|
35
|
+
"0x1234567890123456789012345678901234567890123456789012345678901234"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return mock
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def mock_account_service():
|
|
43
|
+
"""Mock account service."""
|
|
44
|
+
mock = MagicMock()
|
|
45
|
+
mock.get_tag_by_address.return_value = "deployer_tag"
|
|
46
|
+
return mock
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def mock_dependencies():
|
|
51
|
+
"""Mock external dependencies (Safe, EthereumClient, etc)."""
|
|
52
|
+
with (
|
|
53
|
+
patch("iwa.core.services.safe.EthereumClient") as mock_client,
|
|
54
|
+
patch("iwa.core.services.safe.Safe") as mock_safe,
|
|
55
|
+
patch("iwa.core.services.safe.ProxyFactory") as mock_proxy_factory,
|
|
56
|
+
patch("iwa.core.services.safe.settings") as mock_settings,
|
|
57
|
+
patch("iwa.core.services.safe.log_transaction") as mock_log,
|
|
58
|
+
patch("iwa.core.services.safe.get_safe_master_copy_address") as mock_master,
|
|
59
|
+
patch("iwa.core.services.safe.get_safe_proxy_factory_address") as mock_factory,
|
|
60
|
+
):
|
|
61
|
+
mock_settings.gnosis_rpc.get_secret_value.return_value = "http://rpc"
|
|
62
|
+
|
|
63
|
+
# Setup Safe creation return
|
|
64
|
+
mock_create_tx = MagicMock()
|
|
65
|
+
# Valid Checksum Address - New Safe (Matches Pydantic output)
|
|
66
|
+
mock_create_tx.contract_address = "0xbEC49fa140ACaa83533f900357DCD37866d50618"
|
|
67
|
+
mock_create_tx.tx_hash.hex.return_value = "0xTxHash"
|
|
68
|
+
|
|
69
|
+
mock_safe.create.return_value = mock_create_tx
|
|
70
|
+
|
|
71
|
+
# Setup ProxyFactory return
|
|
72
|
+
mock_deploy_tx = MagicMock()
|
|
73
|
+
# Valid checksum address - Salted Safe
|
|
74
|
+
mock_deploy_tx.contract_address = "0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"
|
|
75
|
+
mock_deploy_tx.tx_hash.hex.return_value = "0xTxHashSalted"
|
|
76
|
+
|
|
77
|
+
mock_proxy_factory.return_value.deploy_proxy_contract_with_nonce.return_value = (
|
|
78
|
+
mock_deploy_tx
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Fix for setup_data chaining
|
|
82
|
+
mock_function = MagicMock()
|
|
83
|
+
mock_function.build_transaction.return_value = {"data": "0x1234"}
|
|
84
|
+
|
|
85
|
+
mock_contract = MagicMock()
|
|
86
|
+
mock_contract.functions.setup.return_value = mock_function
|
|
87
|
+
|
|
88
|
+
mock_safe_instance = MagicMock()
|
|
89
|
+
mock_safe_instance.contract = mock_contract
|
|
90
|
+
|
|
91
|
+
def safe_side_effect(*args, **kwargs):
|
|
92
|
+
return mock_safe_instance
|
|
93
|
+
|
|
94
|
+
mock_safe.side_effect = safe_side_effect
|
|
95
|
+
mock_safe.create.return_value = mock_create_tx
|
|
96
|
+
|
|
97
|
+
# Mock get_transaction_receipt for gas calc
|
|
98
|
+
mock_client.return_value.w3.eth.get_transaction_receipt.return_value = {
|
|
99
|
+
"gasUsed": 50000,
|
|
100
|
+
"effectiveGasPrice": 20,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
yield {
|
|
104
|
+
"client": mock_client,
|
|
105
|
+
"safe": mock_safe,
|
|
106
|
+
"proxy_factory": mock_proxy_factory,
|
|
107
|
+
"settings": mock_settings,
|
|
108
|
+
"log": mock_log,
|
|
109
|
+
"master": mock_master,
|
|
110
|
+
"factory": mock_factory,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_create_safe_standard(mock_key_storage, mock_account_service, mock_dependencies):
|
|
115
|
+
"""Test standard create_safe without salt."""
|
|
116
|
+
service = SafeService(mock_key_storage, mock_account_service)
|
|
117
|
+
|
|
118
|
+
safe_account, tx_hash = service.create_safe(
|
|
119
|
+
deployer_tag_or_address="deployer",
|
|
120
|
+
owner_tags_or_addresses=["owner1"],
|
|
121
|
+
threshold=1,
|
|
122
|
+
chain_name="gnosis",
|
|
123
|
+
tag="MySafe",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Checksum address matching what Pydantic/Web3 produces
|
|
127
|
+
assert safe_account.address == "0xbEC49fa140ACaa83533f900357DCD37866d50618"
|
|
128
|
+
assert safe_account.tag == "MySafe"
|
|
129
|
+
assert tx_hash == "0xTxHash"
|
|
130
|
+
|
|
131
|
+
mock_dependencies["safe"].create.assert_called_once()
|
|
132
|
+
mock_key_storage.save.assert_called_once()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_create_safe_with_salt(mock_key_storage, mock_account_service, mock_dependencies):
|
|
136
|
+
"""Test create_safe with salt nonce."""
|
|
137
|
+
service = SafeService(mock_key_storage, mock_account_service)
|
|
138
|
+
|
|
139
|
+
mock_dependencies["client"].return_value.w3.eth.gas_price = 1000
|
|
140
|
+
|
|
141
|
+
safe_account, tx_hash = service.create_safe(
|
|
142
|
+
deployer_tag_or_address="deployer",
|
|
143
|
+
owner_tags_or_addresses=["owner1"],
|
|
144
|
+
threshold=1,
|
|
145
|
+
chain_name="gnosis",
|
|
146
|
+
tag="MySaltedSafe",
|
|
147
|
+
salt_nonce=123,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5
|
|
151
|
+
assert safe_account.address == "0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"
|
|
152
|
+
assert tx_hash == "0xTxHashSalted"
|
|
153
|
+
|
|
154
|
+
# Check that manual ProxyFactory logic was used
|
|
155
|
+
mock_dependencies[
|
|
156
|
+
"proxy_factory"
|
|
157
|
+
].return_value.deploy_proxy_contract_with_nonce.assert_called_once()
|
|
158
|
+
# Safe.create should NOT be called
|
|
159
|
+
mock_dependencies["safe"].create.assert_not_called()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_create_safe_invalid_deployer(mock_key_storage, mock_account_service):
|
|
163
|
+
"""Test error when deployer invalid."""
|
|
164
|
+
mock_key_storage.find_stored_account.return_value = None
|
|
165
|
+
service = SafeService(mock_key_storage, mock_account_service)
|
|
166
|
+
|
|
167
|
+
with pytest.raises(ValueError, match="Deployer account .* not found"):
|
|
168
|
+
service.create_safe("invalid", [], 1, "gnosis")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_main():
|
|
9
|
+
# Create mocks for all required modules
|
|
10
|
+
mock_cowpy = MagicMock()
|
|
11
|
+
modules_to_patch = {
|
|
12
|
+
"cowdao_cowpy": mock_cowpy,
|
|
13
|
+
"cowdao_cowpy.common": MagicMock(),
|
|
14
|
+
"cowdao_cowpy.common.chains": MagicMock(),
|
|
15
|
+
"cowdao_cowpy.app_data": MagicMock(),
|
|
16
|
+
"cowdao_cowpy.app_data.utils": MagicMock(),
|
|
17
|
+
"cowdao_cowpy.contracts": MagicMock(),
|
|
18
|
+
"cowdao_cowpy.contracts.order": MagicMock(),
|
|
19
|
+
"cowdao_cowpy.contracts.sign": MagicMock(),
|
|
20
|
+
"cowdao_cowpy.cow": MagicMock(),
|
|
21
|
+
"cowdao_cowpy.cow.swap": MagicMock(),
|
|
22
|
+
"cowdao_cowpy.order_book": MagicMock(),
|
|
23
|
+
"cowdao_cowpy.order_book.api": MagicMock(),
|
|
24
|
+
"cowdao_cowpy.order_book.config": MagicMock(),
|
|
25
|
+
"cowdao_cowpy.order_book.generated": MagicMock(),
|
|
26
|
+
"cowdao_cowpy.order_book.generated.model": MagicMock(),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
with patch.dict(sys.modules, modules_to_patch):
|
|
30
|
+
# Ensure iwa.core.wallet is imported so we can patch it
|
|
31
|
+
# We need to make sure it's re-imported if it was already imported,
|
|
32
|
+
# to use the mocked cowdao_cowpy if needed, but actually we just need to patch Wallet.
|
|
33
|
+
# If iwa.core.wallet is already imported, patching Wallet is enough.
|
|
34
|
+
# But iwa.core.test might be already imported.
|
|
35
|
+
if "iwa.core.test" in sys.modules:
|
|
36
|
+
del sys.modules["iwa.core.test"]
|
|
37
|
+
|
|
38
|
+
# We need to ensure iwa.core.wallet is importable.
|
|
39
|
+
# If it's not in sys.modules, import it.
|
|
40
|
+
# But importing it triggers cowdao_cowpy import.
|
|
41
|
+
# Since we patched sys.modules, it should use our mocks.
|
|
42
|
+
if "iwa.core.wallet" not in sys.modules:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
with (
|
|
46
|
+
patch("iwa.core.wallet.Wallet"),
|
|
47
|
+
patch("iwa.core.test.ServiceManager") as mock_service_manager,
|
|
48
|
+
):
|
|
49
|
+
# Import main here
|
|
50
|
+
from iwa.core.test import main
|
|
51
|
+
|
|
52
|
+
# Mock the instance returned by ServiceManager()
|
|
53
|
+
mock_sm_instance = mock_service_manager.return_value
|
|
54
|
+
|
|
55
|
+
await main()
|
|
56
|
+
|
|
57
|
+
# Verify ServiceManager was initialized with wallet
|
|
58
|
+
mock_service_manager.assert_called()
|
|
59
|
+
|
|
60
|
+
# Verify create was called
|
|
61
|
+
mock_sm_instance.create.assert_called_once()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_service_manager_structure():
|
|
7
|
+
"""Verify that ServiceManager has all expected methods from mixins."""
|
|
8
|
+
wallet_mock = MagicMock()
|
|
9
|
+
sm = ServiceManager(wallet=wallet_mock)
|
|
10
|
+
|
|
11
|
+
# Check Lifecycle methods
|
|
12
|
+
assert hasattr(sm, "create")
|
|
13
|
+
assert hasattr(sm, "deploy")
|
|
14
|
+
assert hasattr(sm, "spin_up")
|
|
15
|
+
assert hasattr(sm, "wind_down")
|
|
16
|
+
|
|
17
|
+
# Check Staking methods
|
|
18
|
+
assert hasattr(sm, "stake")
|
|
19
|
+
assert hasattr(sm, "unstake")
|
|
20
|
+
assert hasattr(sm, "get_staking_status")
|
|
21
|
+
|
|
22
|
+
# Check Drain methods
|
|
23
|
+
assert hasattr(sm, "drain_service")
|
|
24
|
+
assert hasattr(sm, "claim_rewards")
|
|
25
|
+
|
|
26
|
+
# Check Mech methods
|
|
27
|
+
assert hasattr(sm, "send_mech_request")
|
|
28
|
+
|
|
29
|
+
# Check Base methods
|
|
30
|
+
assert hasattr(sm, "get")
|
|
31
|
+
assert hasattr(sm, "_init_contracts")
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from iwa.core.chain import Gnosis
|
|
6
|
+
from iwa.core.services.transaction import TransactionService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_chain_interfaces():
|
|
11
|
+
with patch("iwa.core.services.transaction.ChainInterfaces") as mock:
|
|
12
|
+
instance = mock.return_value
|
|
13
|
+
gnosis_interface = MagicMock()
|
|
14
|
+
mock_chain = MagicMock(spec=Gnosis)
|
|
15
|
+
mock_chain.name = "Gnosis"
|
|
16
|
+
mock_chain.chain_id = 100
|
|
17
|
+
gnosis_interface.chain = mock_chain
|
|
18
|
+
gnosis_interface.web3 = MagicMock()
|
|
19
|
+
instance.get.return_value = gnosis_interface
|
|
20
|
+
yield instance
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_key_storage():
|
|
25
|
+
return MagicMock()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def mock_account_service():
|
|
30
|
+
return MagicMock()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def transaction_service(mock_key_storage, mock_account_service):
|
|
35
|
+
return TransactionService(mock_key_storage, mock_account_service)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_sign_and_send_success(
|
|
39
|
+
transaction_service, mock_key_storage, mock_chain_interfaces, mock_account_service
|
|
40
|
+
):
|
|
41
|
+
# Setup
|
|
42
|
+
tx = {"to": "0x123", "value": 100, "nonce": 5, "chainId": 100}
|
|
43
|
+
mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
|
|
44
|
+
|
|
45
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
46
|
+
chain_interface.web3.eth.send_raw_transaction.return_value = b"tx_hash"
|
|
47
|
+
chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
|
|
48
|
+
|
|
49
|
+
mock_account_service.resolve_account.return_value = MagicMock(address="0xSigner")
|
|
50
|
+
|
|
51
|
+
# Call
|
|
52
|
+
success, receipt = transaction_service.sign_and_send(tx, "signer")
|
|
53
|
+
|
|
54
|
+
# Assert
|
|
55
|
+
assert success is True
|
|
56
|
+
assert receipt.status == 1
|
|
57
|
+
mock_key_storage.sign_transaction.assert_called_with(tx, "signer")
|
|
58
|
+
chain_interface.web3.eth.send_raw_transaction.assert_called_with(b"raw_tx")
|
|
59
|
+
chain_interface.wait_for_no_pending_tx.assert_called_with("0xSigner")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_sign_and_send_retry_on_low_gas(
|
|
63
|
+
transaction_service, mock_key_storage, mock_chain_interfaces
|
|
64
|
+
):
|
|
65
|
+
# Setup
|
|
66
|
+
tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 10000}
|
|
67
|
+
mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
|
|
68
|
+
|
|
69
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
70
|
+
|
|
71
|
+
# Simulate low gas error then success
|
|
72
|
+
from web3 import exceptions
|
|
73
|
+
|
|
74
|
+
chain_interface.web3.eth.send_raw_transaction.side_effect = [
|
|
75
|
+
exceptions.Web3RPCError("intrinsic gas too low"),
|
|
76
|
+
b"tx_hash",
|
|
77
|
+
]
|
|
78
|
+
chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
|
|
79
|
+
|
|
80
|
+
# Call
|
|
81
|
+
with patch("time.sleep"):
|
|
82
|
+
success, receipt = transaction_service.sign_and_send(tx, "signer")
|
|
83
|
+
|
|
84
|
+
# Assert
|
|
85
|
+
assert success is True
|
|
86
|
+
assert chain_interface.web3.eth.send_raw_transaction.call_count == 2
|
|
87
|
+
# Verify gas increase
|
|
88
|
+
# arguments passed to sign_transaction should reflect updated gas
|
|
89
|
+
args, _ = mock_key_storage.sign_transaction.call_args_list[1]
|
|
90
|
+
assert args[0]["gas"] == 15000 # 10000 * 1.5
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- Negative Tests ---
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_sign_and_send_max_retries_exhausted(
|
|
97
|
+
transaction_service, mock_key_storage, mock_chain_interfaces
|
|
98
|
+
):
|
|
99
|
+
"""Test sign_and_send fails after max gas retries."""
|
|
100
|
+
tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 10000}
|
|
101
|
+
mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
|
|
102
|
+
|
|
103
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
104
|
+
|
|
105
|
+
# Always fail with low gas error
|
|
106
|
+
from web3 import exceptions
|
|
107
|
+
|
|
108
|
+
chain_interface.web3.eth.send_raw_transaction.side_effect = exceptions.Web3RPCError(
|
|
109
|
+
"intrinsic gas too low"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
with patch("time.sleep"):
|
|
113
|
+
success, receipt = transaction_service.sign_and_send(tx, "signer")
|
|
114
|
+
|
|
115
|
+
# Should fail after max retries
|
|
116
|
+
assert success is False
|
|
117
|
+
assert receipt == {} # Returns empty dict on failure
|
|
118
|
+
# Should have tried 3 times (max_retries)
|
|
119
|
+
assert chain_interface.web3.eth.send_raw_transaction.call_count == 3
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_sign_and_send_transaction_reverted(
|
|
123
|
+
transaction_service, mock_key_storage, mock_chain_interfaces
|
|
124
|
+
):
|
|
125
|
+
"""Test sign_and_send handles reverted transaction."""
|
|
126
|
+
tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 21000}
|
|
127
|
+
mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
|
|
128
|
+
|
|
129
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
130
|
+
chain_interface.web3.eth.send_raw_transaction.return_value = b"tx_hash"
|
|
131
|
+
# Transaction mined but reverted (status=0)
|
|
132
|
+
chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=0)
|
|
133
|
+
|
|
134
|
+
success, receipt = transaction_service.sign_and_send(tx, "signer")
|
|
135
|
+
|
|
136
|
+
assert success is False
|
|
137
|
+
assert receipt == {} # Returns empty dict on reverted tx
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_sign_and_send_rpc_error_triggers_rotation(
|
|
141
|
+
transaction_service, mock_key_storage, mock_chain_interfaces
|
|
142
|
+
):
|
|
143
|
+
"""Test sign_and_send rotates RPC on connection error."""
|
|
144
|
+
tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 21000}
|
|
145
|
+
mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
|
|
146
|
+
|
|
147
|
+
chain_interface = mock_chain_interfaces.get.return_value
|
|
148
|
+
|
|
149
|
+
# First call fails with connection error, second succeeds
|
|
150
|
+
chain_interface.web3.eth.send_raw_transaction.side_effect = [
|
|
151
|
+
ConnectionError("Connection refused"),
|
|
152
|
+
b"tx_hash",
|
|
153
|
+
]
|
|
154
|
+
chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
|
|
155
|
+
chain_interface.rotate_rpc.return_value = True
|
|
156
|
+
|
|
157
|
+
with patch("time.sleep"):
|
|
158
|
+
success, receipt = transaction_service.sign_and_send(tx, "signer")
|
|
159
|
+
|
|
160
|
+
assert success is True
|
|
161
|
+
chain_interface.rotate_rpc.assert_called()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_sign_and_send_signer_not_found(
|
|
165
|
+
transaction_service, mock_key_storage, mock_chain_interfaces, mock_account_service
|
|
166
|
+
):
|
|
167
|
+
"""Test sign_and_send fails when signer account not found."""
|
|
168
|
+
tx = {"to": "0x123", "value": 100, "nonce": 5}
|
|
169
|
+
|
|
170
|
+
# Signing raises ValueError for unknown account
|
|
171
|
+
mock_key_storage.sign_transaction.side_effect = ValueError("Account not found")
|
|
172
|
+
|
|
173
|
+
success, receipt = transaction_service.sign_and_send(tx, "unknown_signer")
|
|
174
|
+
|
|
175
|
+
assert success is False
|
|
176
|
+
assert receipt == {} # Returns empty dict on failure
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tests for staking.py router coverage."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_check_availability_exception():
|
|
7
|
+
"""Test _check_availability handles contract call failures."""
|
|
8
|
+
from iwa.web.routers.olas.staking import _check_availability
|
|
9
|
+
|
|
10
|
+
mock_w3 = MagicMock()
|
|
11
|
+
mock_w3.eth.contract.side_effect = Exception("Contract error")
|
|
12
|
+
|
|
13
|
+
result = _check_availability("Test", "0xAddr", mock_w3, [])
|
|
14
|
+
assert result["name"] == "Test"
|
|
15
|
+
assert result["usage"] is None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_filter_contracts_no_availability():
|
|
19
|
+
"""Test _filter_contracts excludes unavailable contracts."""
|
|
20
|
+
from iwa.web.routers.olas.staking import _filter_contracts
|
|
21
|
+
|
|
22
|
+
results = [
|
|
23
|
+
{"name": "A", "usage": {"available": False}, "min_staking_deposit": 100},
|
|
24
|
+
{"name": "B", "usage": {"available": True}, "min_staking_deposit": 100},
|
|
25
|
+
]
|
|
26
|
+
filtered = _filter_contracts(results, None, None)
|
|
27
|
+
assert len(filtered) == 1
|
|
28
|
+
assert filtered[0]["name"] == "B"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_filter_contracts_bond_too_low():
|
|
32
|
+
"""Test _filter_contracts excludes contracts where bond is too low."""
|
|
33
|
+
from iwa.web.routers.olas.staking import _filter_contracts
|
|
34
|
+
|
|
35
|
+
results = [
|
|
36
|
+
{"name": "A", "usage": {"available": True}, "min_staking_deposit": 1000},
|
|
37
|
+
]
|
|
38
|
+
filtered = _filter_contracts(results, service_bond=500, service_token=None)
|
|
39
|
+
assert len(filtered) == 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_filter_contracts_token_mismatch():
|
|
43
|
+
"""Test _filter_contracts excludes contracts with wrong token."""
|
|
44
|
+
from iwa.web.routers.olas.staking import _filter_contracts
|
|
45
|
+
|
|
46
|
+
results = [
|
|
47
|
+
{
|
|
48
|
+
"name": "A",
|
|
49
|
+
"usage": {"available": True},
|
|
50
|
+
"min_staking_deposit": 100,
|
|
51
|
+
"staking_token": "0xOtherToken",
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
filtered = _filter_contracts(results, service_bond=500, service_token="0xmytoken")
|
|
55
|
+
assert len(filtered) == 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_filter_contracts_compatible():
|
|
59
|
+
"""Test _filter_contracts includes compatible contracts."""
|
|
60
|
+
from iwa.web.routers.olas.staking import _filter_contracts
|
|
61
|
+
|
|
62
|
+
results = [
|
|
63
|
+
{
|
|
64
|
+
"name": "A",
|
|
65
|
+
"usage": {"available": True},
|
|
66
|
+
"min_staking_deposit": 100,
|
|
67
|
+
"staking_token": "0xMyToken",
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
filtered = _filter_contracts(results, service_bond=500, service_token="0xmytoken")
|
|
71
|
+
assert len(filtered) == 1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, mock_open, patch
|
|
2
|
+
|
|
3
|
+
from iwa.plugins.olas.contracts.staking import StakingContract
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_staking_contract_coverage():
|
|
7
|
+
with (
|
|
8
|
+
patch("iwa.core.contracts.contract.ChainInterfaces") as mock_chains,
|
|
9
|
+
patch("iwa.plugins.olas.contracts.staking.ActivityCheckerContract"),
|
|
10
|
+
patch("builtins.open", mock_open(read_data="[]")),
|
|
11
|
+
):
|
|
12
|
+
# Setup mocks
|
|
13
|
+
mock_interface = MagicMock()
|
|
14
|
+
mock_chains.return_value.get.return_value = mock_interface
|
|
15
|
+
|
|
16
|
+
# Mock contract calls in __init__
|
|
17
|
+
mock_interface.call_contract.side_effect = lambda method, *args: {
|
|
18
|
+
"activityChecker": "0xChecker",
|
|
19
|
+
"availableRewards": 100,
|
|
20
|
+
"balance": 1000,
|
|
21
|
+
"livenessPeriod": 3600,
|
|
22
|
+
"rewardsPerSecond": 1,
|
|
23
|
+
"maxNumServices": 10,
|
|
24
|
+
"minStakingDeposit": 100,
|
|
25
|
+
"minStakingDuration": 86400,
|
|
26
|
+
"stakingToken": "0xToken",
|
|
27
|
+
}.get(method, 0)
|
|
28
|
+
|
|
29
|
+
# Instantiate (This covers __init__ logic)
|
|
30
|
+
contract = StakingContract(address="0x123")
|
|
31
|
+
assert contract.address == "0x123"
|
tests/test_tables.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from iwa.core.models import StoredAccount, StoredSafeAccount
|
|
6
|
+
from iwa.core.tables import list_accounts
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_console():
|
|
11
|
+
with patch("iwa.core.tables.Console") as mock:
|
|
12
|
+
yield mock.return_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def mock_chain_interface():
|
|
17
|
+
mock = MagicMock()
|
|
18
|
+
mock.chain.native_currency = "ETH"
|
|
19
|
+
return mock
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_list_accounts_empty(mock_console, mock_chain_interface):
|
|
23
|
+
list_accounts(None, mock_chain_interface, None, None)
|
|
24
|
+
mock_console.print.assert_called_once()
|
|
25
|
+
# Could verify table content but that's harder with mocks.
|
|
26
|
+
# Just ensuring it runs without error and prints something is good for coverage.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_list_accounts_eoa(mock_console, mock_chain_interface):
|
|
30
|
+
accounts = {
|
|
31
|
+
"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4": StoredAccount(
|
|
32
|
+
address="0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", tag="tag1"
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
list_accounts(accounts, mock_chain_interface, None, None)
|
|
36
|
+
mock_console.print.assert_called_once()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_list_accounts_safe(mock_console, mock_chain_interface):
|
|
40
|
+
accounts = {
|
|
41
|
+
"0x61a4f49e9dD1f90EB312889632FA956a21353720": StoredSafeAccount(
|
|
42
|
+
address="0x61a4f49e9dD1f90EB312889632FA956a21353720",
|
|
43
|
+
tag="tag2",
|
|
44
|
+
signers=["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"],
|
|
45
|
+
threshold=1,
|
|
46
|
+
chains=["gnosis"],
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
list_accounts(accounts, mock_chain_interface, None, None)
|
|
50
|
+
mock_console.print.assert_called_once()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_list_accounts_with_tokens(mock_console, mock_chain_interface):
|
|
54
|
+
accounts = {
|
|
55
|
+
"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4": StoredAccount(
|
|
56
|
+
address="0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", tag="tag1"
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
token_names = ["native", "OLAS"]
|
|
60
|
+
token_balances = {"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4": {"native": 1.5, "OLAS": 100.0}}
|
|
61
|
+
list_accounts(accounts, mock_chain_interface, token_names, token_balances)
|
|
62
|
+
mock_console.print.assert_called_once()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_list_accounts_empty_with_tokens(mock_console, mock_chain_interface):
|
|
66
|
+
token_names = ["native"]
|
|
67
|
+
token_balances = {} # Should be empty if no accounts
|
|
68
|
+
list_accounts(None, mock_chain_interface, token_names, token_balances)
|
|
69
|
+
mock_console.print.assert_called_once()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_list_accounts_no_accounts_but_tokens(mock_console, mock_chain_interface):
|
|
73
|
+
token_names = ["native"]
|
|
74
|
+
token_balances = {"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4": {"native": 1.5}}
|
|
75
|
+
list_accounts(None, mock_chain_interface, token_names, token_balances)
|
|
76
|
+
mock_console.print.assert_called_once()
|