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_erc20.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from iwa.core.contracts.erc20 import ERC20Contract
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def mock_contract_instance():
|
|
10
|
+
with (
|
|
11
|
+
patch("iwa.core.contracts.contract.ContractInstance.__init__", return_value=None),
|
|
12
|
+
patch("iwa.core.contracts.contract.ContractInstance.call") as mock_call,
|
|
13
|
+
patch("iwa.core.contracts.contract.ContractInstance.prepare_transaction") as mock_prep,
|
|
14
|
+
):
|
|
15
|
+
mock_call.side_effect = lambda method, *args: {
|
|
16
|
+
"decimals": 18,
|
|
17
|
+
"symbol": "TEST",
|
|
18
|
+
"name": "Test Token",
|
|
19
|
+
"totalSupply": 1000000,
|
|
20
|
+
"allowance": 500,
|
|
21
|
+
"balanceOf": 1000,
|
|
22
|
+
}.get(method)
|
|
23
|
+
|
|
24
|
+
yield mock_call, mock_prep
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_init(mock_contract_instance):
|
|
28
|
+
erc20 = ERC20Contract("0xToken", "gnosis")
|
|
29
|
+
assert erc20.decimals == 18
|
|
30
|
+
assert erc20.symbol == "TEST"
|
|
31
|
+
assert erc20.name == "Test Token"
|
|
32
|
+
assert erc20.total_supply == 1000000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_allowance_wei(mock_contract_instance):
|
|
36
|
+
erc20 = ERC20Contract("0xToken", "gnosis")
|
|
37
|
+
assert erc20.allowance_wei("0xOwner", "0xSpender") == 500
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_allowance_eth(mock_contract_instance):
|
|
41
|
+
erc20 = ERC20Contract("0xToken", "gnosis")
|
|
42
|
+
# 500 wei / 10^18 is tiny
|
|
43
|
+
assert erc20.allowance_eth("0xOwner", "0xSpender") == 500 / 10**18
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_balance_of_wei(mock_contract_instance):
|
|
47
|
+
erc20 = ERC20Contract("0xToken", "gnosis")
|
|
48
|
+
assert erc20.balance_of_wei("0xAccount") == 1000
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_balance_of_eth(mock_contract_instance):
|
|
52
|
+
erc20 = ERC20Contract("0xToken", "gnosis")
|
|
53
|
+
assert erc20.balance_of_eth("0xAccount") == 1000 / 10**18
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_prepare_transfer_tx(mock_contract_instance):
|
|
57
|
+
mock_call, mock_prep = mock_contract_instance
|
|
58
|
+
mock_prep.return_value = {"data": "0x"}
|
|
59
|
+
|
|
60
|
+
erc20 = ERC20Contract("0xToken", "gnosis")
|
|
61
|
+
tx = erc20.prepare_transfer_tx("0xFrom", "0xTo", 100)
|
|
62
|
+
assert tx == {"data": "0x"}
|
|
63
|
+
mock_prep.assert_called_with(
|
|
64
|
+
method_name="transfer",
|
|
65
|
+
method_kwargs={"to": "0xTo", "amount": 100},
|
|
66
|
+
tx_params={"from": "0xFrom"},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_prepare_transfer_from_tx(mock_contract_instance):
|
|
71
|
+
mock_call, mock_prep = mock_contract_instance
|
|
72
|
+
mock_prep.return_value = {"data": "0x"}
|
|
73
|
+
|
|
74
|
+
erc20 = ERC20Contract("0xToken", "gnosis")
|
|
75
|
+
tx = erc20.prepare_transfer_from_tx("0xFrom", "0xSender", "0xRecipient", 100)
|
|
76
|
+
assert tx == {"data": "0x"}
|
|
77
|
+
mock_prep.assert_called_with(
|
|
78
|
+
method_name="transferFrom",
|
|
79
|
+
method_kwargs={"_sender": "0xSender", "_recipient": "0xRecipient", "_amount": 100},
|
|
80
|
+
tx_params={"from": "0xFrom"},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_prepare_approve_tx(mock_contract_instance):
|
|
85
|
+
mock_call, mock_prep = mock_contract_instance
|
|
86
|
+
mock_prep.return_value = {"data": "0x"}
|
|
87
|
+
|
|
88
|
+
erc20 = ERC20Contract("0xToken", "gnosis")
|
|
89
|
+
tx = erc20.prepare_approve_tx("0xFrom", "0xSpender", 100)
|
|
90
|
+
assert tx == {"data": "0x"}
|
|
91
|
+
mock_prep.assert_called_with(
|
|
92
|
+
method_name="approve",
|
|
93
|
+
method_kwargs={"spender": "0xSpender", "amount": 100},
|
|
94
|
+
tx_params={"from": "0xFrom"},
|
|
95
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Tests for Gnosis Plugin."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from iwa.plugins.gnosis.plugin import GnosisPlugin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def plugin():
|
|
13
|
+
return GnosisPlugin()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_plugin_name(plugin):
|
|
17
|
+
"""Test plugin name property."""
|
|
18
|
+
assert plugin.name == "gnosis"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_get_cli_commands(plugin):
|
|
22
|
+
"""Test get_cli_commands returns correct commands."""
|
|
23
|
+
commands = plugin.get_cli_commands()
|
|
24
|
+
|
|
25
|
+
assert "create-safe" in commands
|
|
26
|
+
assert callable(commands["create-safe"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_create_safe_command_success(plugin):
|
|
30
|
+
"""Test create_safe_command with successful creation."""
|
|
31
|
+
with (
|
|
32
|
+
patch("iwa.plugins.gnosis.plugin.KeyStorage"),
|
|
33
|
+
patch("iwa.core.services.AccountService"),
|
|
34
|
+
patch("iwa.core.services.SafeService") as mock_safe_service,
|
|
35
|
+
):
|
|
36
|
+
mock_safe_service.return_value.create_safe.return_value = "0xSafeAddress"
|
|
37
|
+
|
|
38
|
+
# Call the command directly
|
|
39
|
+
plugin.create_safe_command(
|
|
40
|
+
tag="my_safe",
|
|
41
|
+
owners="owner1,owner2",
|
|
42
|
+
threshold=2,
|
|
43
|
+
chain_name="gnosis",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
mock_safe_service.return_value.create_safe.assert_called_once_with(
|
|
47
|
+
deployer_tag_or_address="master",
|
|
48
|
+
owner_tags_or_addresses=["owner1", "owner2"],
|
|
49
|
+
threshold=2,
|
|
50
|
+
chain_name="gnosis",
|
|
51
|
+
tag="my_safe",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_create_safe_command_error(plugin):
|
|
56
|
+
"""Test create_safe_command handles ValueError."""
|
|
57
|
+
with (
|
|
58
|
+
patch("iwa.plugins.gnosis.plugin.KeyStorage"),
|
|
59
|
+
patch("iwa.core.services.AccountService"),
|
|
60
|
+
patch("iwa.core.services.SafeService") as mock_safe_service,
|
|
61
|
+
):
|
|
62
|
+
mock_safe_service.return_value.create_safe.side_effect = ValueError("Owner not found")
|
|
63
|
+
|
|
64
|
+
with pytest.raises(typer.Exit) as exc_info:
|
|
65
|
+
plugin.create_safe_command(
|
|
66
|
+
tag="my_safe",
|
|
67
|
+
owners="unknown_owner",
|
|
68
|
+
threshold=1,
|
|
69
|
+
chain_name="gnosis",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
assert exc_info.value.exit_code == 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_create_safe_command_no_tag(plugin):
|
|
76
|
+
"""Test create_safe_command without tag."""
|
|
77
|
+
with (
|
|
78
|
+
patch("iwa.plugins.gnosis.plugin.KeyStorage"),
|
|
79
|
+
patch("iwa.core.services.AccountService"),
|
|
80
|
+
patch("iwa.core.services.SafeService") as mock_safe_service,
|
|
81
|
+
):
|
|
82
|
+
mock_safe_service.return_value.create_safe.return_value = "0xSafeAddress"
|
|
83
|
+
|
|
84
|
+
plugin.create_safe_command(
|
|
85
|
+
tag=None,
|
|
86
|
+
owners="owner1",
|
|
87
|
+
threshold=1,
|
|
88
|
+
chain_name="gnosis",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
mock_safe_service.return_value.create_safe.assert_called_once()
|
|
92
|
+
call_kwargs = mock_safe_service.return_value.create_safe.call_args[1]
|
|
93
|
+
assert call_kwargs["tag"] is None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_create_safe_command_multiple_owners(plugin):
|
|
97
|
+
"""Test create_safe_command with multiple owners."""
|
|
98
|
+
with (
|
|
99
|
+
patch("iwa.plugins.gnosis.plugin.KeyStorage"),
|
|
100
|
+
patch("iwa.core.services.AccountService"),
|
|
101
|
+
patch("iwa.core.services.SafeService") as mock_safe_service,
|
|
102
|
+
):
|
|
103
|
+
plugin.create_safe_command(
|
|
104
|
+
tag="multi_safe",
|
|
105
|
+
owners="owner1, owner2, owner3", # With spaces
|
|
106
|
+
threshold=2,
|
|
107
|
+
chain_name="gnosis",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
call_kwargs = mock_safe_service.return_value.create_safe.call_args[1]
|
|
111
|
+
assert call_kwargs["owner_tags_or_addresses"] == ["owner1", "owner2", "owner3"]
|
tests/test_keys.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""Tests for KeyStorage - all tests use tmp_path to avoid touching real wallet.json."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from iwa.core.keys import EncryptedAccount, KeyStorage, StoredSafeAccount
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_secrets():
|
|
14
|
+
"""Mock settings to provide test password."""
|
|
15
|
+
with patch("iwa.core.keys.settings") as mock:
|
|
16
|
+
mock.wallet_password.get_secret_value.return_value = "test_password"
|
|
17
|
+
mock.gnosis_rpc.get_secret_value.return_value = "http://rpc"
|
|
18
|
+
yield mock
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_aesgcm():
|
|
23
|
+
"""Mock AESGCM for predictable encryption/decryption."""
|
|
24
|
+
with patch("iwa.core.keys.AESGCM") as mock:
|
|
25
|
+
mock.return_value.encrypt.return_value = b"ciphertext"
|
|
26
|
+
mock.return_value.decrypt.return_value = b"private_key"
|
|
27
|
+
yield mock
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def mock_scrypt():
|
|
32
|
+
"""Mock Scrypt for predictable key derivation."""
|
|
33
|
+
with patch("iwa.core.keys.Scrypt") as mock:
|
|
34
|
+
mock.return_value.derive.return_value = b"key" * 11 # 32 bytes
|
|
35
|
+
yield mock
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def mock_account():
|
|
40
|
+
"""Mock eth_account.Account for predictable account creation."""
|
|
41
|
+
with patch("iwa.core.keys.Account") as mock:
|
|
42
|
+
from itertools import cycle
|
|
43
|
+
|
|
44
|
+
addresses = cycle(
|
|
45
|
+
[
|
|
46
|
+
"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
|
|
47
|
+
"0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
|
|
48
|
+
"0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db",
|
|
49
|
+
"0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB",
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def create_side_effect():
|
|
54
|
+
addr = next(addresses)
|
|
55
|
+
m = MagicMock()
|
|
56
|
+
m.key.hex.return_value = f"0xPrivateKey{addr}"
|
|
57
|
+
m.address = addr
|
|
58
|
+
return m
|
|
59
|
+
|
|
60
|
+
mock.create.side_effect = create_side_effect
|
|
61
|
+
|
|
62
|
+
def from_key_side_effect(private_key):
|
|
63
|
+
if isinstance(private_key, str) and "0xPrivateKey" in private_key:
|
|
64
|
+
addr = private_key.replace("0xPrivateKey", "")
|
|
65
|
+
else:
|
|
66
|
+
addr = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
|
|
67
|
+
m = MagicMock()
|
|
68
|
+
m.address = addr
|
|
69
|
+
m.key = private_key.encode() if isinstance(private_key, str) else private_key
|
|
70
|
+
return m
|
|
71
|
+
|
|
72
|
+
mock.from_key.side_effect = from_key_side_effect
|
|
73
|
+
yield mock
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --- EncryptedAccount tests (no file I/O needed) ---
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_encrypted_account_derive_key(mock_scrypt):
|
|
80
|
+
"""Test key derivation."""
|
|
81
|
+
key = EncryptedAccount.derive_key("password", b"salt")
|
|
82
|
+
assert key == b"key" * 11
|
|
83
|
+
mock_scrypt.assert_called_once()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_encrypted_account_encrypt_private_key(mock_scrypt, mock_aesgcm, mock_account):
|
|
87
|
+
"""Test private key encryption."""
|
|
88
|
+
enc_account = EncryptedAccount.encrypt_private_key(
|
|
89
|
+
"0xPrivateKey0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "password", "tag"
|
|
90
|
+
)
|
|
91
|
+
assert enc_account.address == "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
|
|
92
|
+
assert enc_account.tag == "tag"
|
|
93
|
+
assert enc_account.ciphertext == base64.b64encode(b"ciphertext").decode()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_encrypted_account_decrypt_private_key(mock_scrypt, mock_aesgcm, mock_secrets):
|
|
97
|
+
"""Test private key decryption."""
|
|
98
|
+
enc_account = EncryptedAccount(
|
|
99
|
+
address="0x1111111111111111111111111111111111111111",
|
|
100
|
+
salt=base64.b64encode(b"salt").decode(),
|
|
101
|
+
nonce=base64.b64encode(b"nonce").decode(),
|
|
102
|
+
ciphertext=base64.b64encode(b"ciphertext").decode(),
|
|
103
|
+
tag="tag",
|
|
104
|
+
)
|
|
105
|
+
pkey = enc_account.decrypt_private_key()
|
|
106
|
+
assert pkey == "private_key"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --- KeyStorage tests using tmp_path ---
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_keystorage_init_new(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
113
|
+
"""Test initialization of new KeyStorage creates master account."""
|
|
114
|
+
wallet_path = tmp_path / "wallet.json"
|
|
115
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
116
|
+
|
|
117
|
+
# Master account should be created automatically
|
|
118
|
+
assert len(storage.accounts) == 1
|
|
119
|
+
assert storage.get_account("master") is not None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_keystorage_init_existing(tmp_path, mock_secrets):
|
|
123
|
+
"""Test loading existing wallet file."""
|
|
124
|
+
wallet_path = tmp_path / "wallet.json"
|
|
125
|
+
data = {
|
|
126
|
+
"accounts": {
|
|
127
|
+
"0x1111111111111111111111111111111111111111": {
|
|
128
|
+
"address": "0x1111111111111111111111111111111111111111",
|
|
129
|
+
"salt": base64.b64encode(b"salt").decode(),
|
|
130
|
+
"nonce": base64.b64encode(b"nonce").decode(),
|
|
131
|
+
"ciphertext": base64.b64encode(b"ciphertext").decode(),
|
|
132
|
+
"tag": "master",
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
wallet_path.write_text(json.dumps(data))
|
|
137
|
+
|
|
138
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
139
|
+
assert "0x1111111111111111111111111111111111111111" in storage.accounts
|
|
140
|
+
assert storage.get_account("master") is not None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_keystorage_init_corrupted(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
144
|
+
"""Test handling of corrupted wallet file."""
|
|
145
|
+
wallet_path = tmp_path / "wallet.json"
|
|
146
|
+
wallet_path.write_text("{invalid json")
|
|
147
|
+
|
|
148
|
+
with patch("iwa.core.keys.logger") as mock_logger:
|
|
149
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
150
|
+
# Corrupted file -> empty accounts -> auto create master
|
|
151
|
+
assert len(storage.accounts) == 1
|
|
152
|
+
assert storage.get_account("master") is not None
|
|
153
|
+
mock_logger.error.assert_called()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_keystorage_save(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
157
|
+
"""Test saving wallet to file."""
|
|
158
|
+
wallet_path = tmp_path / "wallet.json"
|
|
159
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
160
|
+
storage.save()
|
|
161
|
+
|
|
162
|
+
# Verify file was created
|
|
163
|
+
assert wallet_path.exists()
|
|
164
|
+
data = json.loads(wallet_path.read_text())
|
|
165
|
+
assert "accounts" in data
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_keystorage_create_account(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
169
|
+
"""Test creating additional accounts."""
|
|
170
|
+
wallet_path = tmp_path / "wallet.json"
|
|
171
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
172
|
+
|
|
173
|
+
# Master created in init
|
|
174
|
+
enc_account = storage.create_account("tag")
|
|
175
|
+
assert enc_account.tag == "tag"
|
|
176
|
+
assert len(storage.accounts) == 2 # master + tag
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_keystorage_create_account_duplicate_tag(
|
|
180
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
181
|
+
):
|
|
182
|
+
"""Test creating account with duplicate tag raises error."""
|
|
183
|
+
wallet_path = tmp_path / "wallet.json"
|
|
184
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
185
|
+
storage.create_account("tag")
|
|
186
|
+
|
|
187
|
+
with pytest.raises(ValueError, match="already exists"):
|
|
188
|
+
storage.create_account("tag")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_keystorage_get_private_key(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
192
|
+
"""Test internal private key retrieval."""
|
|
193
|
+
wallet_path = tmp_path / "wallet.json"
|
|
194
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
195
|
+
|
|
196
|
+
master = storage.get_account("master")
|
|
197
|
+
pkey = storage._get_private_key(master.address)
|
|
198
|
+
assert pkey == "private_key"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_keystorage_sign_message(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
202
|
+
"""Test message signing."""
|
|
203
|
+
wallet_path = tmp_path / "wallet.json"
|
|
204
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
205
|
+
storage.create_account("tag")
|
|
206
|
+
|
|
207
|
+
mock_signed_msg = MagicMock()
|
|
208
|
+
mock_signed_msg.signature = b"signature"
|
|
209
|
+
mock_account.sign_message.return_value = mock_signed_msg
|
|
210
|
+
|
|
211
|
+
result = storage.sign_message(b"test message", "tag")
|
|
212
|
+
assert result == b"signature"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_keystorage_sign_transaction(
|
|
216
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
217
|
+
):
|
|
218
|
+
"""Test transaction signing."""
|
|
219
|
+
wallet_path = tmp_path / "wallet.json"
|
|
220
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
221
|
+
storage.create_account("tag")
|
|
222
|
+
|
|
223
|
+
tx = {
|
|
224
|
+
"to": "0x0000000000000000000000000000000000000000",
|
|
225
|
+
"value": 0,
|
|
226
|
+
"gas": 21000,
|
|
227
|
+
"gasPrice": 1,
|
|
228
|
+
"nonce": 0,
|
|
229
|
+
"chainId": 1,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
mock_signed_tx = MagicMock()
|
|
233
|
+
mock_account.sign_transaction.return_value = mock_signed_tx
|
|
234
|
+
|
|
235
|
+
result = storage.sign_transaction(tx, "tag")
|
|
236
|
+
assert result == mock_signed_tx
|
|
237
|
+
mock_account.sign_transaction.assert_called_once()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_keystorage_get_private_key_not_found(
|
|
241
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
242
|
+
):
|
|
243
|
+
"""Test private key retrieval for non-existent account."""
|
|
244
|
+
wallet_path = tmp_path / "wallet.json"
|
|
245
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
246
|
+
|
|
247
|
+
assert storage._get_private_key("0x0000000000000000000000000000000000000000") is None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_keystorage_get_account(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
251
|
+
"""Test getting account by address or tag."""
|
|
252
|
+
wallet_path = tmp_path / "wallet.json"
|
|
253
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
254
|
+
acc1 = storage.create_account("tag")
|
|
255
|
+
|
|
256
|
+
# Get by address
|
|
257
|
+
acct = storage.get_account(acc1.address)
|
|
258
|
+
assert acct.address == acc1.address
|
|
259
|
+
|
|
260
|
+
# Get by tag
|
|
261
|
+
acct = storage.get_account("tag")
|
|
262
|
+
assert acct.address == acc1.address
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_keystorage_get_tag_by_address(
|
|
266
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
267
|
+
):
|
|
268
|
+
"""Test getting tag by address."""
|
|
269
|
+
wallet_path = tmp_path / "wallet.json"
|
|
270
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
271
|
+
acc = storage.create_account("tag")
|
|
272
|
+
|
|
273
|
+
assert storage.get_tag_by_address(acc.address) == "tag"
|
|
274
|
+
master = storage.get_account("master")
|
|
275
|
+
assert storage.get_tag_by_address(master.address) == "master"
|
|
276
|
+
assert storage.get_tag_by_address("0x3333333333333333333333333333333333333333") is None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_keystorage_get_address_by_tag(
|
|
280
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
281
|
+
):
|
|
282
|
+
"""Test getting address by tag."""
|
|
283
|
+
wallet_path = tmp_path / "wallet.json"
|
|
284
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
285
|
+
acc = storage.create_account("tag")
|
|
286
|
+
|
|
287
|
+
assert storage.get_address_by_tag("tag") == acc.address
|
|
288
|
+
assert storage.get_address_by_tag("unknown") is None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_keystorage_master_account_fallback(tmp_path, mock_secrets):
|
|
292
|
+
"""Test master_account property fallback when no master tag."""
|
|
293
|
+
wallet_path = tmp_path / "wallet.json"
|
|
294
|
+
|
|
295
|
+
# Create a wallet with an account that doesn't have "master" tag
|
|
296
|
+
enc_account = EncryptedAccount(
|
|
297
|
+
address="0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
|
|
298
|
+
salt=base64.b64encode(b"salt").decode(),
|
|
299
|
+
nonce=base64.b64encode(b"nonce").decode(),
|
|
300
|
+
ciphertext=base64.b64encode(b"ciphertext").decode(),
|
|
301
|
+
tag="other",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
data = {"accounts": {enc_account.address: enc_account.model_dump()}}
|
|
305
|
+
wallet_path.write_text(json.dumps(data))
|
|
306
|
+
|
|
307
|
+
# Patch create_account to prevent auto-creation of master
|
|
308
|
+
with patch.object(KeyStorage, "create_account"):
|
|
309
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
310
|
+
# Manually add the account since create_account is mocked
|
|
311
|
+
storage.accounts[enc_account.address] = enc_account
|
|
312
|
+
|
|
313
|
+
# Should return the first account if master not found
|
|
314
|
+
assert storage.master_account.tag == "other"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_keystorage_master_account_success(
|
|
318
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
319
|
+
):
|
|
320
|
+
"""Test master_account property returns master."""
|
|
321
|
+
wallet_path = tmp_path / "wallet.json"
|
|
322
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
323
|
+
|
|
324
|
+
assert storage.master_account.tag == "master"
|
|
325
|
+
assert storage.master_account.address is not None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_keystorage_create_account_default_tag(
|
|
329
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
330
|
+
):
|
|
331
|
+
"""Test creating account with custom tag."""
|
|
332
|
+
wallet_path = tmp_path / "wallet.json"
|
|
333
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
334
|
+
|
|
335
|
+
acc = storage.create_account("foo")
|
|
336
|
+
assert acc.tag == "foo"
|
|
337
|
+
assert len(storage.accounts) == 2
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_keystorage_remove_account_not_found(
|
|
341
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
342
|
+
):
|
|
343
|
+
"""Test removing non-existent account doesn't raise."""
|
|
344
|
+
wallet_path = tmp_path / "wallet.json"
|
|
345
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
346
|
+
|
|
347
|
+
# Should not raise
|
|
348
|
+
storage.remove_account("0x0000000000000000000000000000000000000000")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_keystorage_get_account_auto_load_safe(
|
|
352
|
+
tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt
|
|
353
|
+
):
|
|
354
|
+
"""Test getting StoredSafeAccount."""
|
|
355
|
+
wallet_path = tmp_path / "wallet.json"
|
|
356
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
357
|
+
|
|
358
|
+
safe_addr = "0x61a4f49e9dD1f90EB312889632FA956a21353720"
|
|
359
|
+
safe = StoredSafeAccount(
|
|
360
|
+
tag="safe", address=safe_addr, chains=["gnosis"], threshold=1, signers=[]
|
|
361
|
+
)
|
|
362
|
+
storage.accounts[safe_addr] = safe
|
|
363
|
+
|
|
364
|
+
acc = storage.get_account(safe_addr)
|
|
365
|
+
assert isinstance(acc, StoredSafeAccount)
|
|
366
|
+
assert acc.tag == "safe"
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def test_keystorage_get_account_none(tmp_path, mock_secrets):
|
|
370
|
+
"""Test getting non-existent account returns None."""
|
|
371
|
+
wallet_path = tmp_path / "wallet.json"
|
|
372
|
+
|
|
373
|
+
# Create empty wallet
|
|
374
|
+
data = {"accounts": {}}
|
|
375
|
+
wallet_path.write_text(json.dumps(data))
|
|
376
|
+
|
|
377
|
+
# Patch create_account to prevent auto-creation
|
|
378
|
+
with patch.object(KeyStorage, "create_account"):
|
|
379
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
380
|
+
assert storage.get_account("0x5B38Da6a701c568545dCfcB03FcB875f56beddC4") is None
|
|
381
|
+
assert storage.get_account("tag") is None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_get_account_info(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
385
|
+
"""Test get_account_info alias."""
|
|
386
|
+
wallet_path = tmp_path / "wallet.json"
|
|
387
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
388
|
+
storage.create_account("tag1")
|
|
389
|
+
|
|
390
|
+
info = storage.get_account_info("tag1")
|
|
391
|
+
assert info.address == storage.find_stored_account("tag1").address
|
|
392
|
+
assert info.tag == "tag1"
|
|
393
|
+
assert not hasattr(info, "key")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_get_signer(tmp_path, mock_secrets, mock_account, mock_aesgcm, mock_scrypt):
|
|
397
|
+
"""Test get_signer method."""
|
|
398
|
+
wallet_path = tmp_path / "wallet.json"
|
|
399
|
+
storage = KeyStorage(wallet_path, password="test_password")
|
|
400
|
+
storage.create_account("tag")
|
|
401
|
+
|
|
402
|
+
# Test valid signer retrieval
|
|
403
|
+
signer = storage.get_signer("tag")
|
|
404
|
+
assert signer is not None
|
|
405
|
+
mock_account.from_key.assert_called_with("private_key")
|
|
406
|
+
|
|
407
|
+
# Test non-existent account
|
|
408
|
+
assert storage.get_signer("unknown") is None
|
|
409
|
+
|
|
410
|
+
# Test safe account (should return None)
|
|
411
|
+
safe = StoredSafeAccount(
|
|
412
|
+
tag="safe",
|
|
413
|
+
address="0x61a4f49e9dD1f90EB312889632FA956a21353720",
|
|
414
|
+
chains=["gnosis"],
|
|
415
|
+
threshold=1,
|
|
416
|
+
signers=[],
|
|
417
|
+
)
|
|
418
|
+
storage.accounts["0x61a4f49e9dD1f90EB312889632FA956a21353720"] = safe
|
|
419
|
+
assert storage.get_signer("safe") is None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_keystorage_edge_cases_with_real_storage(tmp_path):
|
|
423
|
+
"""Test KeyStorage edge cases with real file storage."""
|
|
424
|
+
wallet_path = tmp_path / "wallet.json"
|
|
425
|
+
storage = KeyStorage(wallet_path, password="password")
|
|
426
|
+
|
|
427
|
+
# Create account
|
|
428
|
+
encrypted_acc = storage.create_account("acc1")
|
|
429
|
+
assert encrypted_acc is not None
|
|
430
|
+
|
|
431
|
+
# Get by address
|
|
432
|
+
acc_by_addr = storage.get_account(encrypted_acc.address)
|
|
433
|
+
assert acc_by_addr is not None
|
|
434
|
+
|
|
435
|
+
# Remove account
|
|
436
|
+
storage.remove_account(encrypted_acc.address)
|
|
437
|
+
|
|
438
|
+
# Verify removal
|
|
439
|
+
assert storage.get_account(encrypted_acc.address) is None
|
|
440
|
+
assert storage.get_account("acc1") is None
|
|
441
|
+
|
|
442
|
+
# Get private key via internal method
|
|
443
|
+
encrypted_acc2 = storage.create_account("acc2")
|
|
444
|
+
pk = storage._get_private_key(encrypted_acc2.address)
|
|
445
|
+
assert pk is not None
|
|
446
|
+
|
|
447
|
+
# Sign transaction unknown account
|
|
448
|
+
with pytest.raises(ValueError):
|
|
449
|
+
storage.sign_transaction({}, "0xUnknown")
|