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
tests/test_main.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import runpy
|
|
2
|
+
import sys
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_main_execution():
|
|
7
|
+
"""Test that python -m iwa executes the CLI."""
|
|
8
|
+
# We mock iwa.core.cli.iwa_cli to verify it gets called
|
|
9
|
+
with patch("iwa.core.cli.iwa_cli") as mock_cli:
|
|
10
|
+
# We also need to mock sys.argv to avoid interfering with pytest args
|
|
11
|
+
with patch.object(sys, "argv", ["iwa"]):
|
|
12
|
+
runpy.run_module("iwa", run_name="__main__")
|
|
13
|
+
mock_cli.assert_called_once()
|
tests/test_mnemonic.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import mock_open, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from cryptography.exceptions import InvalidTag
|
|
8
|
+
|
|
9
|
+
from iwa.core.mnemonic import (
|
|
10
|
+
WALLET_PATH,
|
|
11
|
+
EncryptedMnemonic,
|
|
12
|
+
MnemonicManager,
|
|
13
|
+
MnemonicStorage,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_scrypt():
|
|
19
|
+
with patch("iwa.core.mnemonic.Scrypt") as mock:
|
|
20
|
+
mock.return_value.derive.return_value = b"derived_key"
|
|
21
|
+
yield mock
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def mock_aesgcm():
|
|
26
|
+
with patch("iwa.core.mnemonic.AESGCM") as mock:
|
|
27
|
+
mock.return_value.encrypt.return_value = b"ciphertext"
|
|
28
|
+
mock.return_value.decrypt.return_value = b"plaintext"
|
|
29
|
+
yield mock
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def mock_bip39_generator():
|
|
34
|
+
with patch("iwa.core.mnemonic.Bip39MnemonicGenerator") as mock:
|
|
35
|
+
mock.return_value.FromWordsNumber.return_value.ToStr.return_value = "word1 word2 word3"
|
|
36
|
+
yield mock
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def mock_bip39_seed_generator():
|
|
41
|
+
with patch("iwa.core.mnemonic.Bip39SeedGenerator") as mock:
|
|
42
|
+
mock.return_value.Generate.return_value = b"seed"
|
|
43
|
+
yield mock
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def mock_bip44():
|
|
48
|
+
with patch("iwa.core.mnemonic.Bip44") as mock:
|
|
49
|
+
mock.FromSeed.return_value.Purpose.return_value.Coin.return_value.Account.return_value.Change.return_value.AddressIndex.return_value.PrivateKey.return_value.Raw.return_value.ToHex.return_value = "1234"
|
|
50
|
+
yield mock
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def mock_console():
|
|
55
|
+
with patch("iwa.core.mnemonic.Console") as mock:
|
|
56
|
+
yield mock.return_value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_encrypted_mnemonic_derive_key(mock_scrypt):
|
|
60
|
+
em = EncryptedMnemonic(kdf_salt="salt", nonce="nonce", ciphertext="ciphertext")
|
|
61
|
+
key = em.derive_key(b"password")
|
|
62
|
+
assert key == b"derived_key"
|
|
63
|
+
mock_scrypt.assert_called_once()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_encrypted_mnemonic_decrypt_success(mock_scrypt, mock_aesgcm):
|
|
67
|
+
em = EncryptedMnemonic(
|
|
68
|
+
kdf_salt=base64.b64encode(b"salt").decode(),
|
|
69
|
+
nonce=base64.b64encode(b"nonce").decode(),
|
|
70
|
+
ciphertext=base64.b64encode(b"ciphertext").decode(),
|
|
71
|
+
)
|
|
72
|
+
plaintext = em.decrypt("password")
|
|
73
|
+
assert plaintext == "plaintext"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_encrypted_mnemonic_decrypt_unsupported_kdf():
|
|
77
|
+
em = EncryptedMnemonic(
|
|
78
|
+
kdf="unsupported", kdf_salt="salt", nonce="nonce", ciphertext="ciphertext"
|
|
79
|
+
)
|
|
80
|
+
with pytest.raises(ValueError, match="Unsupported kdf"):
|
|
81
|
+
em.decrypt("password")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_encrypted_mnemonic_decrypt_unsupported_cipher():
|
|
85
|
+
em = EncryptedMnemonic(
|
|
86
|
+
cypher="unsupported", kdf_salt="salt", nonce="nonce", ciphertext="ciphertext"
|
|
87
|
+
)
|
|
88
|
+
with pytest.raises(ValueError, match="Unsupported cipher"):
|
|
89
|
+
em.decrypt("password")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_encrypted_mnemonic_encrypt(mock_scrypt, mock_aesgcm):
|
|
93
|
+
with patch("os.urandom", return_value=b"random"):
|
|
94
|
+
data = EncryptedMnemonic.encrypt("mnemonic", "password")
|
|
95
|
+
assert data["kdf"] == "scrypt"
|
|
96
|
+
assert data["cipher"] == "aesgcm"
|
|
97
|
+
assert data["ciphertext"] == base64.b64encode(b"ciphertext").decode()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_mnemonic_storage_load():
|
|
101
|
+
data = {
|
|
102
|
+
"encrypted_mnemonic": {"kdf_salt": "salt", "nonce": "nonce", "ciphertext": "ciphertext"},
|
|
103
|
+
"accounts": {},
|
|
104
|
+
}
|
|
105
|
+
with patch("builtins.open", mock_open(read_data=json.dumps(data))):
|
|
106
|
+
storage = MnemonicStorage.load(Path("test.json"))
|
|
107
|
+
assert isinstance(storage.encrypted_mnemonic, EncryptedMnemonic)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_mnemonic_storage_save():
|
|
111
|
+
storage = MnemonicStorage(
|
|
112
|
+
encrypted_mnemonic=EncryptedMnemonic(
|
|
113
|
+
kdf_salt="salt", nonce="nonce", ciphertext="ciphertext"
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
with patch("builtins.open", mock_open()) as mock_file, patch("os.chmod") as mock_chmod:
|
|
117
|
+
storage.save(Path("test.json"))
|
|
118
|
+
mock_file.assert_called_once()
|
|
119
|
+
mock_chmod.assert_called_once()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_mnemonic_manager_init():
|
|
123
|
+
mgr = MnemonicManager()
|
|
124
|
+
assert mgr.mnemonic_file == WALLET_PATH
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_mnemonic_manager_derive_key(mock_scrypt):
|
|
128
|
+
mgr = MnemonicManager()
|
|
129
|
+
key = mgr.derive_key(b"password", b"salt")
|
|
130
|
+
assert key == b"derived_key"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_mnemonic_manager_encrypt_mnemonic(mock_scrypt, mock_aesgcm):
|
|
134
|
+
mgr = MnemonicManager()
|
|
135
|
+
with patch("os.urandom", return_value=b"random"):
|
|
136
|
+
data = mgr.encrypt_mnemonic("mnemonic", "password")
|
|
137
|
+
assert data["kdf"] == "scrypt"
|
|
138
|
+
assert data["cipher"] == "aesgcm"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_mnemonic_manager_decrypt_mnemonic_success(mock_scrypt, mock_aesgcm):
|
|
142
|
+
mgr = MnemonicManager()
|
|
143
|
+
encobj = {
|
|
144
|
+
"kdf": "scrypt",
|
|
145
|
+
"kdf_salt": base64.b64encode(b"salt").decode(),
|
|
146
|
+
"nonce": base64.b64encode(b"nonce").decode(),
|
|
147
|
+
"ciphertext": base64.b64encode(b"ciphertext").decode(),
|
|
148
|
+
"cipher": "aesgcm",
|
|
149
|
+
}
|
|
150
|
+
plaintext = mgr.decrypt_mnemonic(encobj, "password")
|
|
151
|
+
assert plaintext == "plaintext"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_mnemonic_manager_decrypt_mnemonic_unsupported_kdf():
|
|
155
|
+
mgr = MnemonicManager()
|
|
156
|
+
encobj = {"kdf": "unsupported"}
|
|
157
|
+
with pytest.raises(ValueError, match="Unsupported kdf"):
|
|
158
|
+
mgr.decrypt_mnemonic(encobj, "password")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_mnemonic_manager_decrypt_mnemonic_unsupported_cipher():
|
|
162
|
+
mgr = MnemonicManager()
|
|
163
|
+
encobj = {"kdf": "scrypt", "cipher": "unsupported"}
|
|
164
|
+
with pytest.raises(ValueError, match="Unsupported cipher"):
|
|
165
|
+
mgr.decrypt_mnemonic(encobj, "password")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_mnemonic_manager_generate_and_store_mnemonic(
|
|
169
|
+
mock_bip39_generator, mock_scrypt, mock_aesgcm
|
|
170
|
+
):
|
|
171
|
+
mgr = MnemonicManager()
|
|
172
|
+
with patch("builtins.open", mock_open()) as mock_file, patch("os.chmod") as mock_chmod:
|
|
173
|
+
mnemonic = mgr.generate_and_store_mnemonic("password", "test.json")
|
|
174
|
+
mock_file.assert_called_once()
|
|
175
|
+
mock_chmod.assert_called_once()
|
|
176
|
+
assert mnemonic == "word1 word2 word3"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_mnemonic_manager_load_and_decrypt_mnemonic_invalid_tag(mock_scrypt, mock_aesgcm):
|
|
180
|
+
mgr = MnemonicManager()
|
|
181
|
+
mock_aesgcm.return_value.decrypt.side_effect = InvalidTag()
|
|
182
|
+
encobj = {
|
|
183
|
+
"kdf": "scrypt",
|
|
184
|
+
"kdf_salt": base64.b64encode(b"salt").decode(),
|
|
185
|
+
"nonce": base64.b64encode(b"nonce").decode(),
|
|
186
|
+
"ciphertext": base64.b64encode(b"ciphertext").decode(),
|
|
187
|
+
"cipher": "aesgcm",
|
|
188
|
+
}
|
|
189
|
+
with patch("builtins.open", mock_open(read_data=json.dumps(encobj))):
|
|
190
|
+
with pytest.raises(ValueError, match="Incorrect password"):
|
|
191
|
+
mgr.load_and_decrypt_mnemonic("password", "test.json")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_mnemonic_manager_derive_eth_accounts_from_mnemonic(
|
|
195
|
+
mock_scrypt, mock_aesgcm, mock_bip39_seed_generator, mock_bip44
|
|
196
|
+
):
|
|
197
|
+
mgr = MnemonicManager()
|
|
198
|
+
# Mock load_and_decrypt_mnemonic to return a mnemonic
|
|
199
|
+
with (
|
|
200
|
+
patch.object(mgr, "load_and_decrypt_mnemonic", return_value="mnemonic"),
|
|
201
|
+
patch("iwa.core.mnemonic.Account") as mock_account,
|
|
202
|
+
):
|
|
203
|
+
mock_account.from_key.return_value.address = "0xAddress"
|
|
204
|
+
|
|
205
|
+
accounts = mgr.derive_eth_accounts_from_mnemonic("password", n_accounts=1)
|
|
206
|
+
assert len(accounts) == 1
|
|
207
|
+
assert accounts[0]["address"] == "0xAddress"
|
|
208
|
+
assert accounts[0]["private_key_hex"] == "1234"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_mnemonic_manager_derive_eth_accounts_from_mnemonic_none(mock_scrypt, mock_aesgcm):
|
|
212
|
+
mgr = MnemonicManager()
|
|
213
|
+
with (
|
|
214
|
+
patch.object(mgr, "load_and_decrypt_mnemonic", return_value=None),
|
|
215
|
+
):
|
|
216
|
+
accounts = mgr.derive_eth_accounts_from_mnemonic("password")
|
|
217
|
+
assert accounts is None
|
tests/test_modals.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Tests for TUI modals to boost coverage."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.containers import Container
|
|
6
|
+
|
|
7
|
+
# === Modal compose tests - these test the UI rendering paths ===
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.asyncio
|
|
11
|
+
async def test_create_eoa_modal_compose():
|
|
12
|
+
"""Cover CreateEOAModal.compose (lines 56-64)."""
|
|
13
|
+
from iwa.tui.modals.base import CreateEOAModal
|
|
14
|
+
|
|
15
|
+
modal = CreateEOAModal()
|
|
16
|
+
|
|
17
|
+
class TestApp(App):
|
|
18
|
+
def compose(self) -> ComposeResult:
|
|
19
|
+
yield Container()
|
|
20
|
+
|
|
21
|
+
app = TestApp()
|
|
22
|
+
async with app.run_test() as pilot:
|
|
23
|
+
app.push_screen(modal)
|
|
24
|
+
await pilot.pause()
|
|
25
|
+
|
|
26
|
+
# Verify modal is mounted
|
|
27
|
+
assert modal.is_mounted
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_create_safe_modal_compose():
|
|
32
|
+
"""Cover CreateSafeModal.compose (lines 128-149)."""
|
|
33
|
+
from iwa.tui.modals.base import CreateSafeModal
|
|
34
|
+
|
|
35
|
+
accounts = [("wallet1", "0x123"), ("wallet2", "0x456")]
|
|
36
|
+
modal = CreateSafeModal(existing_accounts=accounts)
|
|
37
|
+
|
|
38
|
+
class TestApp(App):
|
|
39
|
+
def compose(self) -> ComposeResult:
|
|
40
|
+
yield Container()
|
|
41
|
+
|
|
42
|
+
app = TestApp()
|
|
43
|
+
async with app.run_test() as pilot:
|
|
44
|
+
app.push_screen(modal)
|
|
45
|
+
await pilot.pause()
|
|
46
|
+
|
|
47
|
+
assert modal.is_mounted
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_stake_service_modal_compose():
|
|
52
|
+
"""Cover StakeServiceModal.compose (lines 205-214)."""
|
|
53
|
+
from iwa.tui.modals.base import StakeServiceModal
|
|
54
|
+
|
|
55
|
+
contracts = [("Contract1", "0xabc"), ("Contract2", "0xdef")]
|
|
56
|
+
modal = StakeServiceModal(contracts=contracts)
|
|
57
|
+
|
|
58
|
+
class TestApp(App):
|
|
59
|
+
def compose(self) -> ComposeResult:
|
|
60
|
+
yield Container()
|
|
61
|
+
|
|
62
|
+
app = TestApp()
|
|
63
|
+
async with app.run_test() as pilot:
|
|
64
|
+
app.push_screen(modal)
|
|
65
|
+
await pilot.pause()
|
|
66
|
+
|
|
67
|
+
assert modal.is_mounted
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_create_service_modal_compose():
|
|
72
|
+
"""Cover CreateServiceModal.compose (lines 280-303)."""
|
|
73
|
+
from iwa.tui.modals.base import CreateServiceModal
|
|
74
|
+
|
|
75
|
+
chains = ["gnosis", "ethereum"]
|
|
76
|
+
staking_contracts = [("Staking1", "0xabc")]
|
|
77
|
+
modal = CreateServiceModal(
|
|
78
|
+
chains=chains, default_chain="gnosis", staking_contracts=staking_contracts
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
class TestApp(App):
|
|
82
|
+
def compose(self) -> ComposeResult:
|
|
83
|
+
yield Container()
|
|
84
|
+
|
|
85
|
+
app = TestApp()
|
|
86
|
+
async with app.run_test() as pilot:
|
|
87
|
+
app.push_screen(modal)
|
|
88
|
+
await pilot.pause()
|
|
89
|
+
|
|
90
|
+
assert modal.is_mounted
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.asyncio
|
|
94
|
+
async def test_fund_service_modal_compose():
|
|
95
|
+
"""Cover FundServiceModal.compose (lines 371-382)."""
|
|
96
|
+
from iwa.tui.modals.base import FundServiceModal
|
|
97
|
+
|
|
98
|
+
modal = FundServiceModal(service_key="gnosis:1", native_symbol="xDAI")
|
|
99
|
+
|
|
100
|
+
class TestApp(App):
|
|
101
|
+
def compose(self) -> ComposeResult:
|
|
102
|
+
yield Container()
|
|
103
|
+
|
|
104
|
+
app = TestApp()
|
|
105
|
+
async with app.run_test() as pilot:
|
|
106
|
+
app.push_screen(modal)
|
|
107
|
+
await pilot.pause()
|
|
108
|
+
|
|
109
|
+
assert modal.is_mounted
|
tests/test_models.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from unittest.mock import mock_open, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from iwa.core.models import EthereumAddress, StorableModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_ethereum_address_valid():
|
|
10
|
+
addr_str = "0x1111111111111111111111111111111111111111"
|
|
11
|
+
addr = EthereumAddress(addr_str)
|
|
12
|
+
assert addr == addr_str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_ethereum_address_invalid():
|
|
16
|
+
with pytest.raises(ValueError, match="Invalid Ethereum address"):
|
|
17
|
+
EthereumAddress("0xInvalid")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_ethereum_address_checksum():
|
|
21
|
+
# Test that it converts to checksum address
|
|
22
|
+
addr_lower = "0x1111111111111111111111111111111111111111" # All 1s is same
|
|
23
|
+
# Let's use one that changes
|
|
24
|
+
addr_lower = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"
|
|
25
|
+
addr_checksum = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
|
|
26
|
+
addr = EthereumAddress(addr_lower)
|
|
27
|
+
assert addr == addr_checksum
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MockStorableModel(StorableModel):
|
|
31
|
+
name: str
|
|
32
|
+
value: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_storable_model_save_json():
|
|
36
|
+
model = MockStorableModel(name="test", value=123)
|
|
37
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
38
|
+
model.save_json(Path("test.json"))
|
|
39
|
+
mock_file.assert_called_once()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_storable_model_save_toml():
|
|
43
|
+
model = MockStorableModel(name="test", value=123)
|
|
44
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
45
|
+
model.save_toml(Path("test.toml"))
|
|
46
|
+
mock_file.assert_called_once()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_storable_model_save_yaml():
|
|
50
|
+
model = MockStorableModel(name="test", value=123)
|
|
51
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
52
|
+
model.save_yaml(Path("test.yaml"))
|
|
53
|
+
mock_file.assert_called_once()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_storable_model_save_auto_json():
|
|
57
|
+
model = MockStorableModel(name="test", value=123)
|
|
58
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
59
|
+
model.save("test.json")
|
|
60
|
+
mock_file.assert_called_once()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_storable_model_save_auto_toml():
|
|
64
|
+
model = MockStorableModel(name="test", value=123)
|
|
65
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
66
|
+
model.save("test.toml")
|
|
67
|
+
mock_file.assert_called_once()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_storable_model_save_auto_yaml():
|
|
71
|
+
model = MockStorableModel(name="test", value=123)
|
|
72
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
73
|
+
model.save("test.yaml")
|
|
74
|
+
mock_file.assert_called_once()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_storable_model_save_no_path():
|
|
78
|
+
model = MockStorableModel(name="test", value=123)
|
|
79
|
+
with pytest.raises(ValueError, match="Save path not specified"):
|
|
80
|
+
model.save()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_storable_model_save_stored_path():
|
|
84
|
+
model = MockStorableModel(name="test", value=123)
|
|
85
|
+
model._path = Path("stored.json")
|
|
86
|
+
model._storage_format = "json"
|
|
87
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
88
|
+
model.save()
|
|
89
|
+
mock_file.assert_called_once()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_storable_model_load_json():
|
|
93
|
+
json_content = '{"name": "test", "value": 123}'
|
|
94
|
+
with patch("pathlib.Path.open", mock_open(read_data=json_content)):
|
|
95
|
+
model = MockStorableModel.load_json("test.json")
|
|
96
|
+
assert model.name == "test"
|
|
97
|
+
assert model.value == 123
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_storable_model_load_toml():
|
|
101
|
+
toml_content = b'name = "test"\nvalue = 123'
|
|
102
|
+
with patch("pathlib.Path.open", mock_open(read_data=toml_content)):
|
|
103
|
+
model = MockStorableModel.load_toml("test.toml")
|
|
104
|
+
assert model.name == "test"
|
|
105
|
+
assert model.value == 123
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_storable_model_load_yaml():
|
|
109
|
+
yaml_content = "name: test\nvalue: 123"
|
|
110
|
+
with patch("pathlib.Path.open", mock_open(read_data=yaml_content)):
|
|
111
|
+
model = MockStorableModel.load_yaml("test.yaml")
|
|
112
|
+
assert model.name == "test"
|
|
113
|
+
assert model.value == 123
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_storable_model_load_auto():
|
|
117
|
+
json_content = '{"name": "test", "value": 123}'
|
|
118
|
+
with patch("pathlib.Path.open", mock_open(read_data=json_content)):
|
|
119
|
+
model = MockStorableModel.load(Path("test.json"))
|
|
120
|
+
assert model.name == "test"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_ethereum_address_validate_method():
|
|
124
|
+
# Test the validate class method directly
|
|
125
|
+
with pytest.raises(ValueError, match="Invalid Ethereum address"):
|
|
126
|
+
EthereumAddress.validate("0xInvalid", None)
|
|
127
|
+
|
|
128
|
+
addr = EthereumAddress.validate("0x1111111111111111111111111111111111111111", None)
|
|
129
|
+
assert addr == "0x1111111111111111111111111111111111111111"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_storable_model_save_methods_no_path_error():
|
|
133
|
+
model = MockStorableModel(name="test", value=123)
|
|
134
|
+
# Ensure no _path is set
|
|
135
|
+
if hasattr(model, "_path"):
|
|
136
|
+
del model._path
|
|
137
|
+
|
|
138
|
+
with pytest.raises(ValueError, match="Save path not specified"):
|
|
139
|
+
model.save_json()
|
|
140
|
+
|
|
141
|
+
with pytest.raises(ValueError, match="Save path not specified"):
|
|
142
|
+
model.save_toml()
|
|
143
|
+
|
|
144
|
+
with pytest.raises(ValueError, match="Save path not specified"):
|
|
145
|
+
model.save_yaml()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_storable_model_save_unsupported_extension():
|
|
149
|
+
model = MockStorableModel(name="test", value=123)
|
|
150
|
+
with pytest.raises(ValueError, match="Extension not supported"):
|
|
151
|
+
model.save("test.txt")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_storable_model_save_fallback_format():
|
|
155
|
+
model = MockStorableModel(name="test", value=123)
|
|
156
|
+
model._storage_format = "json"
|
|
157
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
158
|
+
model.save("test.txt") # Unknown extension, fallback to _storage_format
|
|
159
|
+
mock_file.assert_called_once() # Should call save_json
|
|
160
|
+
|
|
161
|
+
model._storage_format = "toml"
|
|
162
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
163
|
+
model.save("test.txt")
|
|
164
|
+
mock_file.assert_called_once() # Should call save_toml
|
|
165
|
+
|
|
166
|
+
model._storage_format = "yaml"
|
|
167
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
168
|
+
model.save("test.txt")
|
|
169
|
+
mock_file.assert_called_once() # Should call save_yaml
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_storable_model_load_unsupported_extension():
|
|
173
|
+
with pytest.raises(ValueError, match="Unsupported file extension"):
|
|
174
|
+
MockStorableModel.load(Path("test.txt"))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
from iwa.core.models import Config
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_config_singleton():
|
|
181
|
+
c1 = Config()
|
|
182
|
+
c2 = Config()
|
|
183
|
+
assert c1 is c2
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_storable_model_save_with_stored_path():
|
|
187
|
+
model = MockStorableModel(name="test", value=123)
|
|
188
|
+
model._path = Path("stored.json")
|
|
189
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
190
|
+
model.save_json()
|
|
191
|
+
mock_file.assert_called_once()
|
|
192
|
+
|
|
193
|
+
model._path = Path("stored.toml")
|
|
194
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
195
|
+
model.save_toml()
|
|
196
|
+
mock_file.assert_called_once()
|
|
197
|
+
|
|
198
|
+
model._path = Path("stored.yaml")
|
|
199
|
+
with patch("pathlib.Path.open", mock_open()) as mock_file:
|
|
200
|
+
model.save_yaml()
|
|
201
|
+
mock_file.assert_called_once()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_storable_model_load_auto_toml_yaml():
|
|
205
|
+
toml_content = b'name = "test"\nvalue = 123'
|
|
206
|
+
with patch("pathlib.Path.open", mock_open(read_data=toml_content)):
|
|
207
|
+
model = MockStorableModel.load(Path("test.toml"))
|
|
208
|
+
assert model.name == "test"
|
|
209
|
+
|
|
210
|
+
yaml_content = "name: test\nvalue: 123"
|
|
211
|
+
with patch("pathlib.Path.open", mock_open(read_data=yaml_content)):
|
|
212
|
+
model = MockStorableModel.load(Path("test.yaml"))
|
|
213
|
+
assert model.name == "test"
|