iwa 0.0.0__py3-none-any.whl → 0.0.1a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Unit tests for SwapMixin.swap logic."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.services.transfer.swap import OrderType, SwapMixin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Dummy class to mixin
|
|
11
|
+
class MockTransferService(SwapMixin):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.balance_service = MagicMock()
|
|
14
|
+
self.account_service = MagicMock()
|
|
15
|
+
self.key_storage = MagicMock()
|
|
16
|
+
self.wallet = MagicMock()
|
|
17
|
+
self.get_erc20_allowance = MagicMock()
|
|
18
|
+
self.approve_erc20 = MagicMock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def transfer_service():
|
|
23
|
+
return MockTransferService()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def mock_chain_interfaces():
|
|
28
|
+
with patch("iwa.core.services.transfer.swap.ChainInterfaces") as mock:
|
|
29
|
+
yield mock
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def mock_cow_swap():
|
|
34
|
+
with patch("iwa.core.services.transfer.swap.CowSwap") as mock:
|
|
35
|
+
yield mock
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def mock_erc20_contract():
|
|
40
|
+
with patch("iwa.core.services.transfer.swap.ERC20Contract") as mock:
|
|
41
|
+
yield mock
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def mock_log_transaction():
|
|
46
|
+
with patch("iwa.core.services.transfer.swap.log_transaction") as mock:
|
|
47
|
+
yield mock
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_swap_happy_path(
|
|
52
|
+
transfer_service, mock_chain_interfaces, mock_cow_swap, mock_log_transaction
|
|
53
|
+
):
|
|
54
|
+
"""Test successful swap with sufficient allowance."""
|
|
55
|
+
# Setup
|
|
56
|
+
account_mock = MagicMock()
|
|
57
|
+
account_mock.address = "0xUser"
|
|
58
|
+
transfer_service.account_service.resolve_account.return_value = account_mock
|
|
59
|
+
transfer_service.key_storage.get_signer.return_value = "signer"
|
|
60
|
+
transfer_service.key_storage.get_signer.return_value = "signer"
|
|
61
|
+
transfer_service.get_erc20_allowance.return_value = 10**18 + 100 # Sufficient
|
|
62
|
+
|
|
63
|
+
# Mock balance for pre-swap check
|
|
64
|
+
transfer_service.balance_service.get_erc20_balance_wei.return_value = 2 * 10**18
|
|
65
|
+
transfer_service.balance_service.get_native_balance_wei.return_value = 2 * 10**18
|
|
66
|
+
|
|
67
|
+
# Mock CowSwap instance
|
|
68
|
+
cow_instance = AsyncMock()
|
|
69
|
+
mock_cow_swap.return_value = cow_instance
|
|
70
|
+
|
|
71
|
+
# Mock Swap Result
|
|
72
|
+
swap_result = {
|
|
73
|
+
"executedSellAmount": "1000000000000000000",
|
|
74
|
+
"executedBuyAmount": "2000000",
|
|
75
|
+
"quote": {"sellTokenPrice": 1.0, "buyTokenPrice": 500.0},
|
|
76
|
+
"txHash": "0xHash",
|
|
77
|
+
}
|
|
78
|
+
cow_instance.swap.return_value = swap_result
|
|
79
|
+
|
|
80
|
+
# Execute
|
|
81
|
+
result = await transfer_service.swap(
|
|
82
|
+
account_address_or_tag="user",
|
|
83
|
+
amount_eth=1.0,
|
|
84
|
+
sell_token_name="WETH",
|
|
85
|
+
buy_token_name="USDC",
|
|
86
|
+
chain_name="gnosis",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Verify
|
|
90
|
+
assert result == swap_result
|
|
91
|
+
# Check allowance was checked
|
|
92
|
+
transfer_service.get_erc20_allowance.assert_called_once()
|
|
93
|
+
# Check approval was skipped
|
|
94
|
+
transfer_service.approve_erc20.assert_not_called()
|
|
95
|
+
# Check logs
|
|
96
|
+
mock_log_transaction.assert_called_once()
|
|
97
|
+
assert result["analytics"]["value_change_pct"] != "N/A"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
async def test_swap_insufficient_allowance(transfer_service, mock_chain_interfaces, mock_cow_swap):
|
|
102
|
+
"""Test approval is called when allowance is insufficient."""
|
|
103
|
+
# Setup
|
|
104
|
+
account_mock = MagicMock()
|
|
105
|
+
account_mock.address = "0xUser"
|
|
106
|
+
transfer_service.account_service.resolve_account.return_value = account_mock
|
|
107
|
+
transfer_service.key_storage.get_signer.return_value = "signer"
|
|
108
|
+
transfer_service.key_storage.get_signer.return_value = "signer"
|
|
109
|
+
transfer_service.get_erc20_allowance.return_value = 0 # Insufficient
|
|
110
|
+
|
|
111
|
+
# Mock balance for pre-swap check
|
|
112
|
+
transfer_service.balance_service.get_erc20_balance_wei.return_value = 2 * 10**18
|
|
113
|
+
|
|
114
|
+
cow_instance = AsyncMock()
|
|
115
|
+
mock_cow_swap.return_value = cow_instance
|
|
116
|
+
cow_instance.swap.return_value = {"txHash": "0xHash"}
|
|
117
|
+
|
|
118
|
+
# Execute
|
|
119
|
+
await transfer_service.swap(
|
|
120
|
+
account_address_or_tag="user", amount_eth=1.0, sell_token_name="WETH", buy_token_name="USDC"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Verify Approval
|
|
124
|
+
transfer_service.approve_erc20.assert_called_once()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_swap_full_balance(transfer_service, mock_chain_interfaces, mock_cow_swap):
|
|
129
|
+
"""Test swapping entire balance (amount_eth=None)."""
|
|
130
|
+
# Setup
|
|
131
|
+
account_mock = MagicMock()
|
|
132
|
+
account_mock.address = "0xUser"
|
|
133
|
+
transfer_service.account_service.resolve_account.return_value = account_mock
|
|
134
|
+
transfer_service.key_storage.get_signer.return_value = "signer"
|
|
135
|
+
|
|
136
|
+
# Mock balance
|
|
137
|
+
transfer_service.balance_service.get_erc20_balance_wei.return_value = 500
|
|
138
|
+
transfer_service.get_erc20_allowance.return_value = 1000
|
|
139
|
+
|
|
140
|
+
cow_instance = AsyncMock()
|
|
141
|
+
mock_cow_swap.return_value = cow_instance
|
|
142
|
+
cow_instance.swap.return_value = {"txHash": "0xHash"}
|
|
143
|
+
|
|
144
|
+
# Execute
|
|
145
|
+
await transfer_service.swap(
|
|
146
|
+
account_address_or_tag="user",
|
|
147
|
+
amount_eth=None,
|
|
148
|
+
sell_token_name="WETH",
|
|
149
|
+
buy_token_name="USDC",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Verify correct amount passed to swap
|
|
153
|
+
cow_instance.swap.assert_called_with(
|
|
154
|
+
amount_wei=500, sell_token_name="WETH", buy_token_name="USDC", order_type=OrderType.SELL
|
|
155
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Unit tests for UI utilities to improve coverage."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.ui import display_mnemonic, prompt_and_store_mnemonic
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_display_mnemonic():
|
|
11
|
+
"""Test display_mnemonic."""
|
|
12
|
+
mnemonic = "one two three four five six seven eight nine ten eleven twelve"
|
|
13
|
+
with patch("iwa.core.ui.Console") as mock_console:
|
|
14
|
+
display_mnemonic(mnemonic)
|
|
15
|
+
assert mock_console.called
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_prompt_and_store_mnemonic_exists(tmp_path):
|
|
19
|
+
"""Test when file exists."""
|
|
20
|
+
manager = MagicMock()
|
|
21
|
+
out_file = str(tmp_path / "exists.json")
|
|
22
|
+
with open(out_file, "w") as f:
|
|
23
|
+
f.write("{}")
|
|
24
|
+
|
|
25
|
+
assert prompt_and_store_mnemonic(manager, out_file=out_file) is None
|
|
26
|
+
assert manager.generate_and_store_mnemonic.called is False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_prompt_and_store_mnemonic_success(tmp_path):
|
|
30
|
+
"""Test successful mnemonic storage."""
|
|
31
|
+
manager = MagicMock()
|
|
32
|
+
out_file = str(tmp_path / "new.json")
|
|
33
|
+
|
|
34
|
+
with patch("getpass.getpass", side_effect=["pass", "pass"]):
|
|
35
|
+
assert prompt_and_store_mnemonic(manager, out_file=out_file) is None
|
|
36
|
+
manager.generate_and_store_mnemonic.assert_called_once_with("pass", out_file)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_prompt_and_store_mnemonic_mismatch(tmp_path):
|
|
40
|
+
"""Test password mismatch then success."""
|
|
41
|
+
manager = MagicMock()
|
|
42
|
+
out_file = str(tmp_path / "mismatch.json")
|
|
43
|
+
|
|
44
|
+
with patch("getpass.getpass", side_effect=["p1", "p2", "pass", "pass"]):
|
|
45
|
+
assert prompt_and_store_mnemonic(manager, out_file=out_file) is None
|
|
46
|
+
manager.generate_and_store_mnemonic.assert_called_once_with("pass", out_file)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_prompt_and_store_mnemonic_empty(tmp_path):
|
|
50
|
+
"""Test empty password then success."""
|
|
51
|
+
manager = MagicMock()
|
|
52
|
+
out_file = str(tmp_path / "empty.json")
|
|
53
|
+
|
|
54
|
+
with patch("getpass.getpass", side_effect=["", "pass", "pass"]):
|
|
55
|
+
assert prompt_and_store_mnemonic(manager, out_file=out_file) is None
|
|
56
|
+
manager.generate_and_store_mnemonic.assert_called_once_with("pass", out_file)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_prompt_and_store_mnemonic_exhausted(tmp_path):
|
|
60
|
+
"""Test exhaustion of attempts."""
|
|
61
|
+
manager = MagicMock()
|
|
62
|
+
out_file = str(tmp_path / "fail.json")
|
|
63
|
+
|
|
64
|
+
with patch("getpass.getpass", side_effect=["p1", "p2", "p3", "p4", "p5", "p6"]):
|
|
65
|
+
with pytest.raises(ValueError, match="Maximum password attempts exceeded"):
|
|
66
|
+
prompt_and_store_mnemonic(manager, out_file=out_file)
|
tests/test_utils.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from iwa.core.utils import get_safe_master_copy_address, singleton
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_get_safe_master_copy_address_found():
|
|
9
|
+
mock_master_copies = {
|
|
10
|
+
"mainnet": [
|
|
11
|
+
("0xAddress1", "L2", "1.3.0"),
|
|
12
|
+
("0xAddress2", "L2", "1.4.1"),
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
with (
|
|
17
|
+
patch("iwa.core.utils.MASTER_COPIES", mock_master_copies),
|
|
18
|
+
patch("iwa.core.utils.EthereumNetwork") as mock_network,
|
|
19
|
+
):
|
|
20
|
+
mock_network.MAINNET = "mainnet"
|
|
21
|
+
|
|
22
|
+
address = get_safe_master_copy_address("1.4.1")
|
|
23
|
+
assert address == "0xAddress2"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_get_safe_master_copy_address_not_found():
|
|
27
|
+
mock_master_copies = {
|
|
28
|
+
"mainnet": [
|
|
29
|
+
("0xAddress1", "L2", "1.3.0"),
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
with (
|
|
34
|
+
patch("iwa.core.utils.MASTER_COPIES", mock_master_copies),
|
|
35
|
+
patch("iwa.core.utils.EthereumNetwork") as mock_network,
|
|
36
|
+
):
|
|
37
|
+
mock_network.MAINNET = "mainnet"
|
|
38
|
+
|
|
39
|
+
with pytest.raises(ValueError, match="Did not find master copy"):
|
|
40
|
+
get_safe_master_copy_address("1.0.0")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_singleton():
|
|
44
|
+
@singleton
|
|
45
|
+
class MyClass:
|
|
46
|
+
def __init__(self, val):
|
|
47
|
+
self.val = val
|
|
48
|
+
|
|
49
|
+
obj1 = MyClass(1)
|
|
50
|
+
obj2 = MyClass(2)
|
|
51
|
+
|
|
52
|
+
assert obj1 is obj2
|
|
53
|
+
assert obj1.val == 1
|
tests/test_workers.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Tests for MonitorWorker."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.tui.workers import MonitorWorker
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.asyncio
|
|
11
|
+
async def test_monitor_worker_init():
|
|
12
|
+
"""Test initialization."""
|
|
13
|
+
mock_monitor = MagicMock()
|
|
14
|
+
mock_app = MagicMock()
|
|
15
|
+
worker = MonitorWorker(mock_monitor, mock_app)
|
|
16
|
+
|
|
17
|
+
assert worker.monitor == mock_monitor
|
|
18
|
+
assert worker.app == mock_app
|
|
19
|
+
assert not worker._running
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_monitor_worker_stop():
|
|
24
|
+
"""Test stop method."""
|
|
25
|
+
mock_monitor = MagicMock()
|
|
26
|
+
mock_app = MagicMock()
|
|
27
|
+
worker = MonitorWorker(mock_monitor, mock_app)
|
|
28
|
+
worker._running = True
|
|
29
|
+
|
|
30
|
+
worker.stop()
|
|
31
|
+
|
|
32
|
+
assert not worker._running
|
|
33
|
+
mock_monitor.stop.assert_called_once()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_monitor_worker_run():
|
|
38
|
+
"""Test run loop."""
|
|
39
|
+
mock_monitor = MagicMock()
|
|
40
|
+
mock_monitor.chain_name = "test_chain"
|
|
41
|
+
mock_app = MagicMock()
|
|
42
|
+
worker = MonitorWorker(mock_monitor, mock_app)
|
|
43
|
+
|
|
44
|
+
# Side effect to stop the loop after first iteration
|
|
45
|
+
def stop_worker(*args, **kwargs):
|
|
46
|
+
worker._running = False
|
|
47
|
+
|
|
48
|
+
mock_monitor.check_activity.side_effect = stop_worker
|
|
49
|
+
|
|
50
|
+
# We need to patch asyncio.sleep to avoid waiting
|
|
51
|
+
with patch("asyncio.sleep", new_callable=AsyncMock):
|
|
52
|
+
await worker.run()
|
|
53
|
+
|
|
54
|
+
assert mock_monitor.running
|
|
55
|
+
# check_activity is called in a thread, so we verify it was called
|
|
56
|
+
# Since it's run in to_thread, the side_effect happens in the thread
|
|
57
|
+
# But since we mock check_activity, side_effect executes.
|
|
58
|
+
# Wait, check_activity is called via asyncio.to_thread(self.monitor.check_activity)
|
|
59
|
+
# So we should verify check_activity was called.
|
|
60
|
+
|
|
61
|
+
mock_monitor.check_activity.assert_called()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
async def test_monitor_worker_run_error():
|
|
66
|
+
"""Test run loop handles errors."""
|
|
67
|
+
mock_monitor = MagicMock()
|
|
68
|
+
mock_monitor.chain_name = "test_chain"
|
|
69
|
+
mock_app = MagicMock()
|
|
70
|
+
worker = MonitorWorker(mock_monitor, mock_app)
|
|
71
|
+
|
|
72
|
+
# Side effect: First call raises error, second call stops worker
|
|
73
|
+
async def side_effect(*args, **kwargs):
|
|
74
|
+
if not hasattr(side_effect, "called"):
|
|
75
|
+
side_effect.called = True
|
|
76
|
+
raise ValueError("Test Error")
|
|
77
|
+
worker._running = False
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
worker._running = True
|
|
81
|
+
|
|
82
|
+
# Patch asyncio.to_thread to use our side effect
|
|
83
|
+
# We also patch sleep to be fast
|
|
84
|
+
with patch("asyncio.to_thread", side_effect=side_effect) as mock_to_thread:
|
|
85
|
+
with patch("asyncio.sleep", new_callable=AsyncMock):
|
|
86
|
+
await worker.run()
|
|
87
|
+
|
|
88
|
+
# This verifies passing through error handling
|
|
89
|
+
assert not worker._running
|
|
90
|
+
# Should be called twice (once error, once success/stop)
|
|
91
|
+
assert mock_to_thread.call_count >= 2
|
tools/verify_drain.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Verification script for draining services."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess # nosec: B404
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
# Configure logging
|
|
10
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_command(command: List[str]): # noqa: D103
|
|
15
|
+
logger.info(f"Running: {' '.join(command)}")
|
|
16
|
+
subprocess.run(command, check=True) # nosec: B603
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def verify_drain(): # noqa: C901, D103
|
|
20
|
+
try:
|
|
21
|
+
# 1. Reset everything
|
|
22
|
+
logger.info("=== STEP 1: RESET ALL (This may take a moment) ===")
|
|
23
|
+
run_command(["just", "reset-all"])
|
|
24
|
+
|
|
25
|
+
logger.info("Waiting for reset to settle...")
|
|
26
|
+
time.sleep(5)
|
|
27
|
+
|
|
28
|
+
from iwa.core.wallet import Wallet
|
|
29
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
30
|
+
|
|
31
|
+
logger.info("Initializing Wallet & Manager...")
|
|
32
|
+
wallet = Wallet()
|
|
33
|
+
manager = ServiceManager(wallet)
|
|
34
|
+
|
|
35
|
+
# 2. Create Service
|
|
36
|
+
logger.info("=== STEP 2: CREATE & DEPLOY SERVICE ===")
|
|
37
|
+
logger.info("Creating service (default Trader)...")
|
|
38
|
+
|
|
39
|
+
# Bond 1 ETH
|
|
40
|
+
service_id = manager.create(chain_name="gnosis", bond_amount_wei=1000000000000000000)
|
|
41
|
+
|
|
42
|
+
if not service_id:
|
|
43
|
+
raise ValueError("Failed to create service")
|
|
44
|
+
|
|
45
|
+
logger.info(f"Service Created! ID: {service_id}")
|
|
46
|
+
|
|
47
|
+
logger.info(f"Spinning up service {service_id}...")
|
|
48
|
+
success = manager.spin_up(service_id=service_id)
|
|
49
|
+
if not success:
|
|
50
|
+
raise ValueError("Failed to spin up service")
|
|
51
|
+
|
|
52
|
+
# Refresh service details
|
|
53
|
+
if manager.service and manager.service.service_id == service_id:
|
|
54
|
+
service = manager.service
|
|
55
|
+
else:
|
|
56
|
+
assert manager.olas_config is not None, "Olas config not initialized"
|
|
57
|
+
service = manager.olas_config.get_service("gnosis", service_id)
|
|
58
|
+
if not service:
|
|
59
|
+
raise ValueError(f"Service {service_id} not found in config")
|
|
60
|
+
|
|
61
|
+
safe_addr = service.multisig_address
|
|
62
|
+
agent_addr = service.agent_address
|
|
63
|
+
|
|
64
|
+
logger.info(f"Safe Address: {safe_addr}")
|
|
65
|
+
logger.info(f"Agent Address: {agent_addr}")
|
|
66
|
+
|
|
67
|
+
if not safe_addr or not agent_addr:
|
|
68
|
+
# Fallback fetch if local object lagging
|
|
69
|
+
info = manager.registry.get_service(service_id)
|
|
70
|
+
logger.info(f"Registry Info: {info}")
|
|
71
|
+
assert manager.olas_config is not None, "Olas config not initialized"
|
|
72
|
+
service = manager.olas_config.get_service("gnosis", service_id)
|
|
73
|
+
if not service:
|
|
74
|
+
raise ValueError(f"Service {service_id} not found in config")
|
|
75
|
+
safe_addr = service.multisig_address
|
|
76
|
+
agent_addr = service.agent_address
|
|
77
|
+
|
|
78
|
+
if not safe_addr or not agent_addr:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Failed to get Safe or Agent address. Safe={safe_addr}, Agent={agent_addr}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# 3. Fund Accounts
|
|
84
|
+
logger.info("=== STEP 3: FUND ACCOUNT (Master -> Agent/Safe) ===")
|
|
85
|
+
amount_native_val = 1.0 # xDAI
|
|
86
|
+
amount_olas_val = 10.0 # OLAS
|
|
87
|
+
|
|
88
|
+
amount_native_wei = int(amount_native_val * 10**18)
|
|
89
|
+
amount_olas_wei = int(amount_olas_val * 10**18)
|
|
90
|
+
|
|
91
|
+
# Fund Agent
|
|
92
|
+
logger.info(f"Funding Agent {agent_addr}...")
|
|
93
|
+
wallet.transfer_service.send("master", agent_addr, amount_native_wei, "native", "gnosis")
|
|
94
|
+
wallet.transfer_service.send("master", agent_addr, amount_olas_wei, "OLAS", "gnosis")
|
|
95
|
+
|
|
96
|
+
# Fund Safe
|
|
97
|
+
logger.info(f"Funding Safe {safe_addr}...")
|
|
98
|
+
wallet.transfer_service.send("master", safe_addr, amount_native_wei, "native", "gnosis")
|
|
99
|
+
wallet.transfer_service.send("master", safe_addr, amount_olas_wei, "OLAS", "gnosis")
|
|
100
|
+
|
|
101
|
+
logger.info("Waiting 7s for indexing...")
|
|
102
|
+
time.sleep(7)
|
|
103
|
+
|
|
104
|
+
# Verify Funding
|
|
105
|
+
logger.info("Verifying balances...")
|
|
106
|
+
agent_native = wallet.balance_service.get_native_balance_eth(agent_addr, "gnosis") or 0.0
|
|
107
|
+
agent_olas = (
|
|
108
|
+
wallet.balance_service.get_erc20_balance_eth(agent_addr, "OLAS", "gnosis") or 0.0
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
safe_native = wallet.balance_service.get_native_balance_eth(safe_addr, "gnosis") or 0.0
|
|
112
|
+
safe_olas = wallet.balance_service.get_erc20_balance_eth(safe_addr, "OLAS", "gnosis") or 0.0
|
|
113
|
+
|
|
114
|
+
logger.info(f"Agent Balance: Native={agent_native}, OLAS={agent_olas}")
|
|
115
|
+
logger.info(f"Safe Balance: Native={safe_native}, OLAS={safe_olas}")
|
|
116
|
+
|
|
117
|
+
if agent_native < 0.9:
|
|
118
|
+
raise ValueError(f"Agent funding failed: {agent_native}")
|
|
119
|
+
if safe_native < 0.9:
|
|
120
|
+
raise ValueError(f"Safe funding failed: {safe_native}")
|
|
121
|
+
|
|
122
|
+
# 4. Drain Service
|
|
123
|
+
logger.info("=== STEP 4: DRAIN SERVICE ===")
|
|
124
|
+
manager_drain = ServiceManager(wallet, service_key=f"gnosis:{service_id}")
|
|
125
|
+
|
|
126
|
+
logger.info("Executing drain_service()...")
|
|
127
|
+
drained = manager_drain.drain_service()
|
|
128
|
+
logger.info(f"Drain result: {drained}")
|
|
129
|
+
|
|
130
|
+
logger.info("Waiting 7s for indexing after drain...")
|
|
131
|
+
time.sleep(7)
|
|
132
|
+
|
|
133
|
+
# 5. Verify Zero Balance
|
|
134
|
+
logger.info("=== STEP 5: VERIFY 0 BALANCE ===")
|
|
135
|
+
|
|
136
|
+
final_agent_native = (
|
|
137
|
+
wallet.balance_service.get_native_balance_eth(agent_addr, "gnosis") or 0.0
|
|
138
|
+
)
|
|
139
|
+
final_agent_olas = (
|
|
140
|
+
wallet.balance_service.get_erc20_balance_eth(agent_addr, "OLAS", "gnosis") or 0.0
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
final_safe_native = (
|
|
144
|
+
wallet.balance_service.get_native_balance_eth(safe_addr, "gnosis") or 0.0
|
|
145
|
+
)
|
|
146
|
+
final_safe_olas = (
|
|
147
|
+
wallet.balance_service.get_erc20_balance_eth(safe_addr, "OLAS", "gnosis") or 0.0
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
logger.info(f"Final Agent Balance: Native={final_agent_native}, OLAS={final_agent_olas}")
|
|
151
|
+
logger.info(f"Final Safe Balance: Native={final_safe_native}, OLAS={final_safe_olas}")
|
|
152
|
+
|
|
153
|
+
errors = []
|
|
154
|
+
|
|
155
|
+
# Checks
|
|
156
|
+
if final_agent_native > 0.02:
|
|
157
|
+
errors.append(f"Agent still has native: {final_agent_native}")
|
|
158
|
+
|
|
159
|
+
if final_agent_olas > 0.000001:
|
|
160
|
+
errors.append(f"Agent still has OLAS: {final_agent_olas}")
|
|
161
|
+
|
|
162
|
+
if final_safe_native > 0.005:
|
|
163
|
+
# Safe drain is precise when using safe txn
|
|
164
|
+
errors.append(f"Safe still has native: {final_safe_native}")
|
|
165
|
+
|
|
166
|
+
if final_safe_olas > 0.000001:
|
|
167
|
+
errors.append(f"Safe still has OLAS: {final_safe_olas}")
|
|
168
|
+
|
|
169
|
+
if errors:
|
|
170
|
+
logger.error("❌ VERIFICATION FAILED:")
|
|
171
|
+
for e in errors:
|
|
172
|
+
logger.error(f" - {e}")
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
logger.info("✅ SUCCESS: Service drained completely!")
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.exception(f"Verification process failed with exception: {e}")
|
|
179
|
+
# sys.exit(1) # actually let log trace propagate
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
verify_drain()
|
__init__.py
DELETED
hello.py
DELETED
iwa-0.0.0.dist-info/METADATA
DELETED
iwa-0.0.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
__init__.py,sha256=sBF3fhvD4D-ITi6Q6esr1QEoq433RKO9cJSNm6yy7ac,67
|
|
2
|
-
hello.py,sha256=5eAormQmiU9ZodzSDPvTuQKokBhVFnRE6m-KrStftN8,72
|
|
3
|
-
iwa-0.0.0.dist-info/METADATA,sha256=-Z_hiTEX_-4NBXCqV1JxQuaJ_oy9SjSkWRpMGh7JxJo,174
|
|
4
|
-
iwa-0.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
iwa-0.0.0.dist-info/top_level.txt,sha256=2Qir6NE0bKuCfTi-7V_-BoA1-QYuU7aoBiQsuWXUXpw,15
|
|
6
|
-
iwa-0.0.0.dist-info/RECORD,,
|
|
File without changes
|