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_cli.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def iwa_cli_module():
|
|
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
|
+
if "iwa.core.cli" in sys.modules:
|
|
31
|
+
del sys.modules["iwa.core.cli"]
|
|
32
|
+
if "iwa.core.wallet" in sys.modules:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
with patch("iwa.core.wallet.Wallet"):
|
|
36
|
+
import iwa.core.cli
|
|
37
|
+
|
|
38
|
+
yield iwa.core.cli.iwa_cli
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
runner = CliRunner()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def cli(iwa_cli_module):
|
|
46
|
+
return iwa_cli_module
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def mock_key_storage():
|
|
51
|
+
with patch("iwa.core.cli.KeyStorage") as mock:
|
|
52
|
+
yield mock.return_value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def mock_wallet():
|
|
57
|
+
with patch("iwa.core.cli.Wallet") as mock:
|
|
58
|
+
yield mock.return_value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_account_create(cli, mock_key_storage):
|
|
62
|
+
result = runner.invoke(cli, ["wallet", "create", "--tag", "test"])
|
|
63
|
+
assert result.exit_code == 0
|
|
64
|
+
mock_key_storage.create_account.assert_called_with("test")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_account_create_error(cli, mock_key_storage):
|
|
68
|
+
mock_key_storage.create_account.side_effect = ValueError("Error creating account")
|
|
69
|
+
result = runner.invoke(cli, ["wallet", "create", "--tag", "test"])
|
|
70
|
+
assert result.exit_code == 1
|
|
71
|
+
assert "Error: Error creating account" in result.stdout
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_account_list(cli, mock_wallet):
|
|
75
|
+
mock_wallet.get_accounts_balances.return_value = ({}, None)
|
|
76
|
+
with (
|
|
77
|
+
patch("iwa.core.cli.list_accounts") as mock_list_accounts,
|
|
78
|
+
patch("iwa.core.cli.ChainInterfaces"),
|
|
79
|
+
):
|
|
80
|
+
result = runner.invoke(cli, ["wallet", "list", "--chain", "gnosis", "--balances", "native"])
|
|
81
|
+
assert result.exit_code == 0
|
|
82
|
+
mock_wallet.get_accounts_balances.assert_called_with("gnosis", ["native"])
|
|
83
|
+
mock_list_accounts.assert_called_once()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_account_send(cli, mock_wallet):
|
|
87
|
+
result = runner.invoke(
|
|
88
|
+
cli, ["wallet", "send", "--from", "sender", "--to", "receiver", "--amount", "1.0"]
|
|
89
|
+
)
|
|
90
|
+
assert result.exit_code == 0
|
|
91
|
+
mock_wallet.send.assert_called()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_erc20_transfer_from(cli, mock_wallet):
|
|
95
|
+
result = runner.invoke(
|
|
96
|
+
cli,
|
|
97
|
+
[
|
|
98
|
+
"wallet",
|
|
99
|
+
"transfer-from",
|
|
100
|
+
"--from",
|
|
101
|
+
"from",
|
|
102
|
+
"--sender",
|
|
103
|
+
"sender",
|
|
104
|
+
"--recipient",
|
|
105
|
+
"recipient",
|
|
106
|
+
"--token",
|
|
107
|
+
"token",
|
|
108
|
+
"--amount",
|
|
109
|
+
"1.0",
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
assert result.exit_code == 0
|
|
113
|
+
mock_wallet.transfer_from_erc20.assert_called()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_erc20_approve(cli, mock_wallet):
|
|
117
|
+
result = runner.invoke(
|
|
118
|
+
cli,
|
|
119
|
+
[
|
|
120
|
+
"wallet",
|
|
121
|
+
"approve",
|
|
122
|
+
"--owner",
|
|
123
|
+
"owner",
|
|
124
|
+
"--spender",
|
|
125
|
+
"spender",
|
|
126
|
+
"--token",
|
|
127
|
+
"token",
|
|
128
|
+
"--amount",
|
|
129
|
+
"1.0",
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
assert result.exit_code == 0
|
|
133
|
+
mock_wallet.approve_erc20.assert_called()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_drain_wallet(cli, mock_wallet):
|
|
137
|
+
result = runner.invoke(cli, ["wallet", "drain", "--from", "from", "--to", "to"])
|
|
138
|
+
assert result.exit_code == 0
|
|
139
|
+
mock_wallet.drain.assert_called()
|
tests/test_contract.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from unittest.mock import MagicMock, mock_open, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from web3.exceptions import ContractCustomError
|
|
6
|
+
|
|
7
|
+
from iwa.core.contracts.contract import ContractInstance
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_chain_interface():
|
|
12
|
+
with patch("iwa.core.contracts.contract.ChainInterfaces") as mock:
|
|
13
|
+
mock_ci = mock.return_value.get.return_value
|
|
14
|
+
mock_ci.web3.eth.contract.return_value = MagicMock()
|
|
15
|
+
yield mock_ci
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_abi_file():
|
|
20
|
+
abi_content = '[{"type": "function", "name": "testFunc", "inputs": []}, {"type": "error", "name": "CustomError", "inputs": [{"type": "uint256", "name": "code"}]}, {"type": "event", "name": "TestEvent", "inputs": []}]'
|
|
21
|
+
with patch("builtins.open", mock_open(read_data=abi_content)):
|
|
22
|
+
yield
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MockContract(ContractInstance):
|
|
26
|
+
name = "test_contract"
|
|
27
|
+
abi_path = Path("test.json")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_init(mock_chain_interface, mock_abi_file):
|
|
31
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
32
|
+
assert contract.address == "0xAddress"
|
|
33
|
+
assert contract.abi is not None
|
|
34
|
+
assert "0x" in str(contract.error_selectors.keys()) # Check if selector generated
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_init_abi_dict(mock_chain_interface):
|
|
38
|
+
abi_content = '{"abi": [{"type": "function", "name": "testFunc"}]}'
|
|
39
|
+
with patch("builtins.open", mock_open(read_data=abi_content)):
|
|
40
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
41
|
+
assert contract.abi == [{"type": "function", "name": "testFunc"}]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_call(mock_chain_interface, mock_abi_file):
|
|
45
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
46
|
+
contract.contract.functions.testFunc.return_value.call.return_value = "result"
|
|
47
|
+
assert contract.call("testFunc") == "result"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_prepare_transaction_success(mock_chain_interface, mock_abi_file):
|
|
51
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
52
|
+
mock_chain_interface.calculate_transaction_params.return_value = {"gas": 100}
|
|
53
|
+
contract.contract.functions.testFunc.return_value.build_transaction.return_value = {
|
|
54
|
+
"data": "0x"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
tx = contract.prepare_transaction("testFunc", {}, {})
|
|
58
|
+
assert tx == {"data": "0x"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_prepare_transaction_custom_error_known(mock_chain_interface, mock_abi_file):
|
|
62
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
63
|
+
# Selector for CustomError(uint256)
|
|
64
|
+
# We need to calculate it or capture what load_error_selectors produced
|
|
65
|
+
selector = list(contract.error_selectors.keys())[0] # 0x...
|
|
66
|
+
# Encode args: uint256(123)
|
|
67
|
+
encoded_args = "0" * 62 + "7b" # 123 hex
|
|
68
|
+
error_data = f"{selector}{encoded_args}"
|
|
69
|
+
|
|
70
|
+
contract.contract.functions.testFunc.return_value.build_transaction.side_effect = (
|
|
71
|
+
ContractCustomError(error_data)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Now the function returns None and logs the error instead of raising
|
|
75
|
+
with patch("iwa.core.contracts.contract.logger") as mock_logger:
|
|
76
|
+
result = contract.prepare_transaction("testFunc", {}, {})
|
|
77
|
+
assert result is None
|
|
78
|
+
# Verify error was logged
|
|
79
|
+
mock_logger.error.assert_called()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_prepare_transaction_custom_error_unknown(mock_chain_interface, mock_abi_file):
|
|
83
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
84
|
+
error_data = "0x12345678" # Unknown selector
|
|
85
|
+
|
|
86
|
+
contract.contract.functions.testFunc.return_value.build_transaction.side_effect = (
|
|
87
|
+
ContractCustomError(error_data)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Now the function returns None and logs the error instead of raising
|
|
91
|
+
with patch("iwa.core.contracts.contract.logger") as mock_logger:
|
|
92
|
+
result = contract.prepare_transaction("testFunc", {}, {})
|
|
93
|
+
assert result is None
|
|
94
|
+
# Verify error was logged
|
|
95
|
+
mock_logger.error.assert_called()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_prepare_transaction_revert_string(mock_chain_interface, mock_abi_file):
|
|
99
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
100
|
+
# Encoded Error(string) with "Error" as the message
|
|
101
|
+
encoded_error = "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000054572726f72000000000000000000000000000000000000000000000000000000"
|
|
102
|
+
e = Exception("msg", encoded_error)
|
|
103
|
+
|
|
104
|
+
contract.contract.functions.testFunc.return_value.build_transaction.side_effect = e
|
|
105
|
+
|
|
106
|
+
with patch("iwa.core.contracts.contract.logger") as mock_logger:
|
|
107
|
+
tx = contract.prepare_transaction("testFunc", {}, {})
|
|
108
|
+
assert tx is None
|
|
109
|
+
# Should log the decoded error
|
|
110
|
+
mock_logger.error.assert_called()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_prepare_transaction_other_exception(mock_chain_interface, mock_abi_file):
|
|
114
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
115
|
+
# The code expects e.args[1] to exist, so we must provide it
|
|
116
|
+
e = Exception("Generic Error", "Some Data")
|
|
117
|
+
contract.contract.functions.testFunc.return_value.build_transaction.side_effect = e
|
|
118
|
+
|
|
119
|
+
with patch("iwa.core.contracts.contract.logger") as mock_logger:
|
|
120
|
+
tx = contract.prepare_transaction("testFunc", {}, {})
|
|
121
|
+
assert tx is None
|
|
122
|
+
mock_logger.error.assert_called()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_extract_events(mock_chain_interface, mock_abi_file):
|
|
126
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
127
|
+
receipt = MagicMock()
|
|
128
|
+
|
|
129
|
+
# Mock event class and its process_receipt method
|
|
130
|
+
mock_event_instance = MagicMock()
|
|
131
|
+
|
|
132
|
+
# Create a log object that supports both ["event"] and .args
|
|
133
|
+
mock_log = MagicMock()
|
|
134
|
+
mock_log.__getitem__.side_effect = lambda key: "TestEvent" if key == "event" else None
|
|
135
|
+
mock_log.args = {"arg1": 1}
|
|
136
|
+
|
|
137
|
+
mock_event_instance.process_receipt.return_value = [mock_log]
|
|
138
|
+
mock_event_class = MagicMock(return_value=mock_event_instance)
|
|
139
|
+
|
|
140
|
+
# Mock contract.events dictionary-like access
|
|
141
|
+
contract.contract.events = MagicMock()
|
|
142
|
+
|
|
143
|
+
def get_event(name):
|
|
144
|
+
if name == "TestEvent":
|
|
145
|
+
return mock_event_class
|
|
146
|
+
raise KeyError(name)
|
|
147
|
+
|
|
148
|
+
contract.contract.events.__getitem__.side_effect = get_event
|
|
149
|
+
|
|
150
|
+
# Explicitly set abi on the mock contract object
|
|
151
|
+
contract.contract.abi = contract.abi
|
|
152
|
+
|
|
153
|
+
events = contract.extract_events(receipt)
|
|
154
|
+
assert len(events) == 1
|
|
155
|
+
assert events[0]["name"] == "TestEvent"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_extract_events_edge_cases(mock_chain_interface):
|
|
159
|
+
# Custom ABI with multiple event types to test different paths
|
|
160
|
+
abi_content = '[{"type": "event", "name": "MissingEvent", "inputs": []}, {"type": "event", "name": "EmptyLogsEvent", "inputs": []}, {"type": "event", "name": "ErrorEvent", "inputs": []}, {"type": "function", "name": "NotAnEvent", "inputs": []}]'
|
|
161
|
+
|
|
162
|
+
with patch("builtins.open", mock_open(read_data=abi_content)):
|
|
163
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
164
|
+
|
|
165
|
+
receipt = MagicMock()
|
|
166
|
+
|
|
167
|
+
# Mock contract.events
|
|
168
|
+
contract.contract.events = MagicMock()
|
|
169
|
+
|
|
170
|
+
# 1. MissingEvent: raises KeyError when accessed
|
|
171
|
+
# 2. EmptyLogsEvent: returns empty list from process_receipt
|
|
172
|
+
# 3. ErrorEvent: raises Exception from process_receipt
|
|
173
|
+
|
|
174
|
+
mock_empty_logs_event = MagicMock()
|
|
175
|
+
mock_empty_logs_event.return_value.process_receipt.return_value = []
|
|
176
|
+
|
|
177
|
+
mock_error_event = MagicMock()
|
|
178
|
+
mock_error_event.return_value.process_receipt.side_effect = Exception("Processing error")
|
|
179
|
+
|
|
180
|
+
def get_event(name):
|
|
181
|
+
if name == "MissingEvent":
|
|
182
|
+
raise KeyError(name)
|
|
183
|
+
if name == "EmptyLogsEvent":
|
|
184
|
+
return mock_empty_logs_event
|
|
185
|
+
if name == "ErrorEvent":
|
|
186
|
+
return mock_error_event
|
|
187
|
+
return MagicMock()
|
|
188
|
+
|
|
189
|
+
contract.contract.events.__getitem__.side_effect = get_event
|
|
190
|
+
|
|
191
|
+
# Explicitly set abi on the mock contract object
|
|
192
|
+
contract.contract.abi = contract.abi
|
|
193
|
+
|
|
194
|
+
events = contract.extract_events(receipt)
|
|
195
|
+
assert len(events) == 0
|
tests/test_db.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Tests for database operations."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from iwa.core.db import init_db, log_transaction
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_log_transaction_upsert():
|
|
9
|
+
"""Test log_transaction creates new records."""
|
|
10
|
+
with patch("iwa.core.db.SentTransaction") as mock_model:
|
|
11
|
+
mock_model.get_or_none.return_value = None
|
|
12
|
+
mock_insert = mock_model.insert.return_value
|
|
13
|
+
mock_upsert = mock_insert.on_conflict_replace.return_value
|
|
14
|
+
|
|
15
|
+
log_transaction("0x123", "0xFrom", "0xTo", "DAI", 100, "gnosis")
|
|
16
|
+
|
|
17
|
+
mock_model.insert.assert_called_once()
|
|
18
|
+
_, kwargs = mock_model.insert.call_args
|
|
19
|
+
assert kwargs["tx_hash"] == "0x123"
|
|
20
|
+
assert kwargs["chain"] == "gnosis"
|
|
21
|
+
|
|
22
|
+
mock_upsert.execute.assert_called_once()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_log_transaction_update_preserve_fields():
|
|
26
|
+
"""Test log_transaction preserves existing non-null fields."""
|
|
27
|
+
with patch("iwa.core.db.SentTransaction") as mock_model:
|
|
28
|
+
mock_instance = MagicMock()
|
|
29
|
+
mock_instance.token = "DAI"
|
|
30
|
+
mock_instance.value_eur = 10.0
|
|
31
|
+
mock_instance.amount_wei = "100"
|
|
32
|
+
mock_model.get_or_none.return_value = mock_instance
|
|
33
|
+
|
|
34
|
+
# Update with token="NATIVE" which should be ignored if existing is better
|
|
35
|
+
log_transaction("0x123", "0xFrom", "0xTo", "NATIVE", 0, "gnosis")
|
|
36
|
+
|
|
37
|
+
mock_model.insert.assert_called_once()
|
|
38
|
+
_, kwargs = mock_model.insert.call_args
|
|
39
|
+
# Should preserve DAI and 100
|
|
40
|
+
assert kwargs["token"] == "DAI"
|
|
41
|
+
assert kwargs["amount_wei"] == "100"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_log_transaction_error():
|
|
45
|
+
"""Test log_transaction handles errors gracefully."""
|
|
46
|
+
with (
|
|
47
|
+
patch("iwa.core.db.SentTransaction") as mock_model,
|
|
48
|
+
patch("iwa.core.db.logger") as mock_logger,
|
|
49
|
+
):
|
|
50
|
+
mock_model.get_or_none.side_effect = Exception("DB Error")
|
|
51
|
+
|
|
52
|
+
log_transaction("0x123", "0xFrom", "0xTo", "DAI", 100, "gnosis")
|
|
53
|
+
|
|
54
|
+
mock_logger.error.assert_called()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_init_db():
|
|
58
|
+
"""Test init_db creates tables and runs migrations."""
|
|
59
|
+
with (
|
|
60
|
+
patch("iwa.core.db.db") as mock_db,
|
|
61
|
+
patch("iwa.core.db.SentTransaction") as mock_model,
|
|
62
|
+
patch("iwa.core.db.migrate") as mock_migrate,
|
|
63
|
+
patch("iwa.core.db.SqliteMigrator"),
|
|
64
|
+
):
|
|
65
|
+
mock_db.get_columns.return_value = []
|
|
66
|
+
|
|
67
|
+
init_db()
|
|
68
|
+
|
|
69
|
+
mock_db.connect.assert_called_once()
|
|
70
|
+
mock_db.create_tables.assert_called_with([mock_model], safe=True)
|
|
71
|
+
assert mock_migrate.call_count >= 1
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_init_db_closed_at_end():
|
|
75
|
+
"""Test init_db closes connection at end."""
|
|
76
|
+
with (
|
|
77
|
+
patch("iwa.core.db.db") as mock_db,
|
|
78
|
+
patch("iwa.core.db.SentTransaction"),
|
|
79
|
+
patch("iwa.core.db.migrate"),
|
|
80
|
+
patch("iwa.core.db.SqliteMigrator"),
|
|
81
|
+
):
|
|
82
|
+
mock_db.is_closed.side_effect = [True, False] # closed initially, then open
|
|
83
|
+
mock_db.get_columns.return_value = []
|
|
84
|
+
|
|
85
|
+
init_db()
|
|
86
|
+
|
|
87
|
+
mock_db.close.assert_called_once()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_init_db_get_columns_error():
|
|
91
|
+
"""Test init_db handles get_columns error."""
|
|
92
|
+
with (
|
|
93
|
+
patch("iwa.core.db.db") as mock_db,
|
|
94
|
+
patch("iwa.core.db.SentTransaction"),
|
|
95
|
+
):
|
|
96
|
+
mock_db.get_columns.side_effect = Exception("Table not found")
|
|
97
|
+
|
|
98
|
+
# Should not raise
|
|
99
|
+
init_db()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_run_migrations_drop_token_symbol():
|
|
103
|
+
"""Test run_migrations drops deprecated token_symbol column."""
|
|
104
|
+
from iwa.core.db import run_migrations
|
|
105
|
+
|
|
106
|
+
with patch("iwa.core.db.SqliteMigrator"), patch("iwa.core.db.migrate") as mock_migrate:
|
|
107
|
+
columns = ["token_symbol", "from_tag", "price_eur", "tags"]
|
|
108
|
+
|
|
109
|
+
run_migrations(columns)
|
|
110
|
+
|
|
111
|
+
# Should have called migrate to drop token_symbol
|
|
112
|
+
assert mock_migrate.called
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_run_migrations_drop_token_symbol_error():
|
|
116
|
+
"""Test run_migrations handles drop_column error."""
|
|
117
|
+
from iwa.core.db import run_migrations
|
|
118
|
+
|
|
119
|
+
with (
|
|
120
|
+
patch("iwa.core.db.SqliteMigrator"),
|
|
121
|
+
patch("iwa.core.db.migrate", side_effect=Exception("Drop failed")),
|
|
122
|
+
patch("iwa.core.db.logger") as mock_logger,
|
|
123
|
+
):
|
|
124
|
+
columns = ["token_symbol", "from_tag", "price_eur", "tags"]
|
|
125
|
+
|
|
126
|
+
run_migrations(columns)
|
|
127
|
+
|
|
128
|
+
mock_logger.warning.assert_called()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_run_migrations_add_from_tag():
|
|
132
|
+
"""Test run_migrations adds from_tag columns."""
|
|
133
|
+
from iwa.core.db import run_migrations
|
|
134
|
+
|
|
135
|
+
with patch("iwa.core.db.SqliteMigrator"), patch("iwa.core.db.migrate") as mock_migrate:
|
|
136
|
+
columns = ["price_eur", "tags"] # No from_tag
|
|
137
|
+
|
|
138
|
+
run_migrations(columns)
|
|
139
|
+
|
|
140
|
+
assert mock_migrate.called
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_run_migrations_add_from_tag_error():
|
|
144
|
+
"""Test run_migrations handles add_column error."""
|
|
145
|
+
from iwa.core.db import run_migrations
|
|
146
|
+
|
|
147
|
+
with (
|
|
148
|
+
patch("iwa.core.db.SqliteMigrator"),
|
|
149
|
+
patch("iwa.core.db.migrate", side_effect=Exception("Add failed")),
|
|
150
|
+
patch("iwa.core.db.logger") as mock_logger,
|
|
151
|
+
):
|
|
152
|
+
columns = [] # No columns - triggers add
|
|
153
|
+
|
|
154
|
+
run_migrations(columns)
|
|
155
|
+
|
|
156
|
+
mock_logger.warning.assert_called()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_run_migrations_add_price_eur():
|
|
160
|
+
"""Test run_migrations adds price columns."""
|
|
161
|
+
from iwa.core.db import run_migrations
|
|
162
|
+
|
|
163
|
+
with patch("iwa.core.db.SqliteMigrator"), patch("iwa.core.db.migrate") as mock_migrate:
|
|
164
|
+
columns = ["from_tag", "tags"] # No price_eur
|
|
165
|
+
|
|
166
|
+
run_migrations(columns)
|
|
167
|
+
|
|
168
|
+
assert mock_migrate.called
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_run_migrations_add_tags():
|
|
172
|
+
"""Test run_migrations adds tags column."""
|
|
173
|
+
from iwa.core.db import run_migrations
|
|
174
|
+
|
|
175
|
+
with patch("iwa.core.db.SqliteMigrator"), patch("iwa.core.db.migrate") as mock_migrate:
|
|
176
|
+
columns = ["from_tag", "price_eur"] # No tags
|
|
177
|
+
|
|
178
|
+
run_migrations(columns)
|
|
179
|
+
|
|
180
|
+
assert mock_migrate.called
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Tests for DrainManagerMixin coverage."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.plugins.olas.contracts.staking import StakingState
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_drain_manager():
|
|
12
|
+
"""Create a mock DrainManagerMixin instance."""
|
|
13
|
+
from iwa.plugins.olas.service_manager.drain import DrainManagerMixin
|
|
14
|
+
|
|
15
|
+
class MockManager(DrainManagerMixin):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.wallet = MagicMock()
|
|
18
|
+
self.service = MagicMock()
|
|
19
|
+
self.chain_name = "gnosis"
|
|
20
|
+
self.olas_config = MagicMock()
|
|
21
|
+
|
|
22
|
+
return MockManager()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_claim_rewards_no_service(mock_drain_manager):
|
|
26
|
+
"""Test claim_rewards with no active service."""
|
|
27
|
+
mock_drain_manager.service = None
|
|
28
|
+
success, amount = mock_drain_manager.claim_rewards()
|
|
29
|
+
assert not success
|
|
30
|
+
assert amount == 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_claim_rewards_not_staked(mock_drain_manager):
|
|
34
|
+
"""Test claim_rewards when service is not staked."""
|
|
35
|
+
mock_drain_manager.service.staking_contract_address = None
|
|
36
|
+
success, amount = mock_drain_manager.claim_rewards()
|
|
37
|
+
assert not success
|
|
38
|
+
assert amount == 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_claim_rewards_claim_tx_fails(mock_drain_manager):
|
|
42
|
+
"""Test claim_rewards when prepare_claim_tx fails."""
|
|
43
|
+
mock_drain_manager.service.staking_contract_address = "0xStaking"
|
|
44
|
+
mock_drain_manager.service.service_id = 1
|
|
45
|
+
|
|
46
|
+
with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
|
|
47
|
+
mock_staking = mock_staking_cls.return_value
|
|
48
|
+
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
49
|
+
mock_staking.get_accrued_rewards.return_value = 1000000000000000000
|
|
50
|
+
mock_staking.prepare_claim_tx.return_value = None # Failed to prepare
|
|
51
|
+
|
|
52
|
+
success, amount = mock_drain_manager.claim_rewards()
|
|
53
|
+
assert not success
|
|
54
|
+
assert amount == 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_claim_rewards_send_fails(mock_drain_manager):
|
|
58
|
+
"""Test claim_rewards when transaction send fails."""
|
|
59
|
+
mock_drain_manager.service.staking_contract_address = "0xStaking"
|
|
60
|
+
mock_drain_manager.service.service_id = 1
|
|
61
|
+
mock_drain_manager.wallet.master_account.address = "0xMaster"
|
|
62
|
+
|
|
63
|
+
with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
|
|
64
|
+
mock_staking = mock_staking_cls.return_value
|
|
65
|
+
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
66
|
+
mock_staking.get_accrued_rewards.return_value = 1000000000000000000
|
|
67
|
+
mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
|
|
68
|
+
mock_drain_manager.wallet.sign_and_send_transaction.return_value = (False, None)
|
|
69
|
+
|
|
70
|
+
success, amount = mock_drain_manager.claim_rewards()
|
|
71
|
+
assert not success
|
|
72
|
+
assert amount == 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_claim_rewards_success_no_event(mock_drain_manager):
|
|
76
|
+
"""Test claim_rewards success but no RewardClaimed event."""
|
|
77
|
+
mock_drain_manager.service.staking_contract_address = "0xStaking"
|
|
78
|
+
mock_drain_manager.service.service_id = 1
|
|
79
|
+
mock_drain_manager.wallet.master_account.address = "0xMaster"
|
|
80
|
+
|
|
81
|
+
with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
|
|
82
|
+
mock_staking = mock_staking_cls.return_value
|
|
83
|
+
mock_staking.get_staking_state.return_value = StakingState.STAKED
|
|
84
|
+
mock_staking.get_accrued_rewards.return_value = 1000000000000000000
|
|
85
|
+
mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
|
|
86
|
+
mock_staking.extract_events.return_value = [] # No RewardClaimed event
|
|
87
|
+
mock_drain_manager.wallet.sign_and_send_transaction.return_value = (
|
|
88
|
+
True,
|
|
89
|
+
{"transactionHash": "0xHash"},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
success, amount = mock_drain_manager.claim_rewards()
|
|
93
|
+
assert success
|
|
94
|
+
assert amount == 1000000000000000000
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_withdraw_rewards_no_withdrawal_address(mock_drain_manager):
|
|
98
|
+
"""Test withdraw_rewards with no withdrawal address configured."""
|
|
99
|
+
mock_drain_manager.service.multisig_address = "0xSafe"
|
|
100
|
+
mock_drain_manager.olas_config.withdrawal_address = None
|
|
101
|
+
|
|
102
|
+
success, amount = mock_drain_manager.withdraw_rewards()
|
|
103
|
+
assert not success
|
|
104
|
+
assert amount == 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_drain_service_no_service(mock_drain_manager):
|
|
108
|
+
"""Test drain_service with no active service."""
|
|
109
|
+
mock_drain_manager.service = None
|
|
110
|
+
result = mock_drain_manager.drain_service()
|
|
111
|
+
assert result == {}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_claim_rewards_if_needed_exception(mock_drain_manager):
|
|
115
|
+
"""Test _claim_rewards_if_needed handles exceptions."""
|
|
116
|
+
mock_drain_manager.service.staking_contract_address = "0xStaking"
|
|
117
|
+
|
|
118
|
+
# Mock claim_rewards to raise
|
|
119
|
+
mock_drain_manager.claim_rewards = MagicMock(side_effect=Exception("Test Error"))
|
|
120
|
+
|
|
121
|
+
result = mock_drain_manager._claim_rewards_if_needed(claim_rewards=True)
|
|
122
|
+
assert result == 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_drain_agent_account_exception(mock_drain_manager):
|
|
126
|
+
"""Test _drain_agent_account handles drain exceptions."""
|
|
127
|
+
mock_drain_manager.service.agent_address = "0xAgent"
|
|
128
|
+
mock_drain_manager.wallet.drain.side_effect = Exception("Drain failed")
|
|
129
|
+
|
|
130
|
+
result = mock_drain_manager._drain_agent_account("0xTarget", "gnosis")
|
|
131
|
+
assert result is None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_drain_owner_account_exception(mock_drain_manager):
|
|
135
|
+
"""Test _drain_owner_account handles drain exceptions."""
|
|
136
|
+
mock_drain_manager.service.service_owner_address = "0xOwner"
|
|
137
|
+
mock_drain_manager.wallet.drain.side_effect = Exception("Drain failed")
|
|
138
|
+
|
|
139
|
+
result = mock_drain_manager._drain_owner_account("0xTarget", "gnosis")
|
|
140
|
+
assert result is None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_normalize_drain_result_tuple(mock_drain_manager):
|
|
144
|
+
"""Test _normalize_drain_result with tuple input."""
|
|
145
|
+
|
|
146
|
+
# Success tuple with HexBytes-like object
|
|
147
|
+
class FakeHexBytes:
|
|
148
|
+
def hex(self):
|
|
149
|
+
return "0xABCDEF"
|
|
150
|
+
|
|
151
|
+
result = mock_drain_manager._normalize_drain_result((True, {"transactionHash": FakeHexBytes()}))
|
|
152
|
+
assert result == "0xABCDEF"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_normalize_drain_result_failure_tuple(mock_drain_manager):
|
|
156
|
+
"""Test _normalize_drain_result with failure tuple."""
|
|
157
|
+
result = mock_drain_manager._normalize_drain_result((False, {}))
|
|
158
|
+
assert result is None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_normalize_drain_result_none(mock_drain_manager):
|
|
162
|
+
"""Test _normalize_drain_result with None input."""
|
|
163
|
+
result = mock_drain_manager._normalize_drain_result(None)
|
|
164
|
+
assert result is None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_drain_owner_skipped_when_equals_target(mock_drain_manager):
|
|
168
|
+
"""Test _drain_owner_account is skipped when owner == target."""
|
|
169
|
+
mock_drain_manager.service.service_owner_address = "0xOwner123"
|
|
170
|
+
# Target is the same as owner (case-insensitive)
|
|
171
|
+
result = mock_drain_manager._drain_owner_account("0xowner123", "gnosis")
|
|
172
|
+
# Should skip and return None without calling drain
|
|
173
|
+
assert result is None
|
|
174
|
+
mock_drain_manager.wallet.drain.assert_not_called()
|