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,554 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from textual.widgets import Input, Select
|
|
5
|
+
|
|
6
|
+
from iwa.tui.app import IwaApp
|
|
7
|
+
from iwa.tui.screens.wallets import WalletsScreen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_wallet():
|
|
12
|
+
with patch("iwa.tui.app.Wallet") as mock_wallet_cls:
|
|
13
|
+
wallet = mock_wallet_cls.return_value
|
|
14
|
+
wallet.key_storage.accounts = {
|
|
15
|
+
"0x1": MagicMock(address="0x1", tag="Acc1"),
|
|
16
|
+
"0x2": MagicMock(address="0x2", tag="Acc2"),
|
|
17
|
+
}
|
|
18
|
+
wallet.account_service = MagicMock()
|
|
19
|
+
wallet.account_service.accounts = wallet.key_storage.accounts
|
|
20
|
+
wallet.account_service.get_account_data.side_effect = (
|
|
21
|
+
lambda: wallet.account_service.accounts
|
|
22
|
+
)
|
|
23
|
+
wallet.balance_service = MagicMock()
|
|
24
|
+
|
|
25
|
+
# Helper to make resolve_account work with key_storage for consistency
|
|
26
|
+
wallet.account_service.resolve_account.side_effect = (
|
|
27
|
+
lambda tag: wallet.key_storage.get_account(tag)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
wallet.get_native_balance_eth.return_value = 0.0
|
|
31
|
+
wallet.get_erc20_balance_eth.return_value = 0.0
|
|
32
|
+
wallet.send.return_value = "0xHash"
|
|
33
|
+
|
|
34
|
+
# Mock PluginService
|
|
35
|
+
wallet.plugin_service = MagicMock()
|
|
36
|
+
wallet.plugin_service.get_all_plugins.return_value = {}
|
|
37
|
+
yield wallet
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture(autouse=True)
|
|
41
|
+
def mock_deps():
|
|
42
|
+
with (
|
|
43
|
+
patch("iwa.tui.screens.wallets.EventMonitor"),
|
|
44
|
+
patch("iwa.tui.screens.wallets.PriceService") as mock_price,
|
|
45
|
+
patch("iwa.core.db.SentTransaction"),
|
|
46
|
+
patch("iwa.core.db.log_transaction"),
|
|
47
|
+
patch("iwa.tui.screens.wallets.ChainInterfaces") as mock_chains,
|
|
48
|
+
):
|
|
49
|
+
mock_price.return_value.get_token_price.return_value = 10.0
|
|
50
|
+
|
|
51
|
+
# Setup Chain Interface Mock
|
|
52
|
+
mock_interface = MagicMock()
|
|
53
|
+
mock_interface.tokens = {"TOKEN": "0xToken"}
|
|
54
|
+
mock_interface.chain.native_currency = "ETH"
|
|
55
|
+
mock_chains.return_value.get.return_value = mock_interface
|
|
56
|
+
mock_chains.return_value.items.return_value = [("gnosis", mock_interface)]
|
|
57
|
+
|
|
58
|
+
yield {"chains": mock_chains}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_fetch_balances_flow(mock_wallet, mock_deps):
|
|
63
|
+
app = IwaApp()
|
|
64
|
+
# Patch call_from_thread
|
|
65
|
+
app.call_from_thread = lambda cb, *args, **kwargs: cb(*args, **kwargs)
|
|
66
|
+
|
|
67
|
+
async with app.run_test(size=(160, 80)):
|
|
68
|
+
view = app.query_one(WalletsScreen)
|
|
69
|
+
|
|
70
|
+
# Configure returns on both legacy and service
|
|
71
|
+
mock_wallet.get_native_balance_eth.return_value = 1.2345
|
|
72
|
+
mock_wallet.balance_service.get_native_balance_eth.return_value = 1.2345
|
|
73
|
+
mock_wallet.balance_service.get_erc20_balance_with_retry.return_value = 500.0
|
|
74
|
+
|
|
75
|
+
# Trigger fetch directly
|
|
76
|
+
view.balance_cache = {}
|
|
77
|
+
|
|
78
|
+
# Trigger (call impl directly to avoid threading issues)
|
|
79
|
+
view.chain_token_states["gnosis"].add("TOKEN")
|
|
80
|
+
# In the refactored view, we call fetch_all_balances
|
|
81
|
+
# We'll wait for the worker to finish
|
|
82
|
+
worker = view.fetch_all_balances(view.active_chain, ["TOKEN"])
|
|
83
|
+
await worker.wait()
|
|
84
|
+
|
|
85
|
+
# Verify calls made (can check either legacy or service depending on how it's implemented)
|
|
86
|
+
mock_wallet.balance_service.get_native_balance_eth.assert_called()
|
|
87
|
+
mock_wallet.balance_service.get_erc20_balance_with_retry.assert_called()
|
|
88
|
+
|
|
89
|
+
# Verify cache state
|
|
90
|
+
assert view.balance_cache["gnosis"]["0x1"]["NATIVE"] == "1.2345"
|
|
91
|
+
assert view.balance_cache["gnosis"]["0x1"]["TOKEN"] == "500.0000"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.asyncio
|
|
95
|
+
async def test_chain_changed(mock_wallet, mock_deps):
|
|
96
|
+
app = IwaApp()
|
|
97
|
+
# Patch call_from_thread
|
|
98
|
+
app.call_from_thread = lambda cb, *args, **kwargs: cb(*args, **kwargs)
|
|
99
|
+
|
|
100
|
+
async with app.run_test(size=(160, 80)) as pilot:
|
|
101
|
+
view = app.query_one(WalletsScreen)
|
|
102
|
+
|
|
103
|
+
# Select different chain
|
|
104
|
+
select = app.query_one("#chain_select")
|
|
105
|
+
select.value = "ethereum"
|
|
106
|
+
await pilot.pause(1.0)
|
|
107
|
+
|
|
108
|
+
assert view.active_chain == "ethereum"
|
|
109
|
+
|
|
110
|
+
# Test Invalid chain (no RPC)
|
|
111
|
+
chains = mock_deps["chains"]
|
|
112
|
+
chains.return_value.get.return_value.chain.rpc = ""
|
|
113
|
+
select.value = "base"
|
|
114
|
+
await pilot.pause()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_send_transaction_coverage(mock_wallet, mock_deps):
|
|
119
|
+
app = IwaApp()
|
|
120
|
+
# Patch call_from_thread
|
|
121
|
+
app.call_from_thread = lambda cb, *args, **kwargs: cb(*args, **kwargs)
|
|
122
|
+
|
|
123
|
+
async with app.run_test(size=(200, 200)) as pilot:
|
|
124
|
+
view = app.query_one(WalletsScreen)
|
|
125
|
+
# Force table height
|
|
126
|
+
app.query_one("#accounts_table").styles.height = 10
|
|
127
|
+
# Wait for workers to populate
|
|
128
|
+
# In the new design, monitor_workers are created in start_monitor
|
|
129
|
+
if not view.monitor_workers:
|
|
130
|
+
view.start_monitor()
|
|
131
|
+
assert len(view.monitor_workers) > 0
|
|
132
|
+
await pilot.pause()
|
|
133
|
+
|
|
134
|
+
# Test validation failures
|
|
135
|
+
# 1. No from
|
|
136
|
+
app.query_one("#from_addr", Select).value = Select.BLANK
|
|
137
|
+
mock_wallet.send.reset_mock()
|
|
138
|
+
btn = app.query_one("#send_btn")
|
|
139
|
+
btn.focus()
|
|
140
|
+
await pilot.press("enter")
|
|
141
|
+
# Assert not sent
|
|
142
|
+
mock_wallet.send.assert_not_called()
|
|
143
|
+
|
|
144
|
+
# 2. No to
|
|
145
|
+
app.query_one("#from_addr", Select).value = "0x1"
|
|
146
|
+
app.query_one("#to_addr", Select).value = Select.BLANK
|
|
147
|
+
btn = app.query_one("#send_btn")
|
|
148
|
+
btn.focus()
|
|
149
|
+
await pilot.press("enter")
|
|
150
|
+
mock_wallet.send.assert_not_called()
|
|
151
|
+
|
|
152
|
+
# 3. No amount
|
|
153
|
+
app.query_one("#to_addr", Select).value = "0x2"
|
|
154
|
+
app.query_one("#amount", Input).value = ""
|
|
155
|
+
btn = app.query_one("#send_btn")
|
|
156
|
+
btn.focus()
|
|
157
|
+
await pilot.press("enter")
|
|
158
|
+
mock_wallet.send.assert_not_called()
|
|
159
|
+
|
|
160
|
+
# 4. Valid Send NATIVE
|
|
161
|
+
app.query_one("#amount", Input).value = "1.0"
|
|
162
|
+
app.query_one("#token", Select).value = "native"
|
|
163
|
+
mock_wallet.send.return_value = "0xTxHash"
|
|
164
|
+
|
|
165
|
+
# Call worker directly
|
|
166
|
+
view.send_tx_worker("0x1", "0x2", "native", 1.0)
|
|
167
|
+
await pilot.pause()
|
|
168
|
+
|
|
169
|
+
# Verify wallet.send called
|
|
170
|
+
mock_wallet.send.assert_called()
|
|
171
|
+
|
|
172
|
+
# 5. Valid Send ERC20
|
|
173
|
+
mock_wallet.send.reset_mock()
|
|
174
|
+
view.send_tx_worker("0x1", "0x2", "TOKEN", 1.0)
|
|
175
|
+
await pilot.pause()
|
|
176
|
+
mock_wallet.send.assert_called()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_watchdog_logic(mock_wallet, mock_deps):
|
|
181
|
+
app = IwaApp()
|
|
182
|
+
app.call_from_thread = lambda cb, *args, **kwargs: cb(*args, **kwargs)
|
|
183
|
+
|
|
184
|
+
async with app.run_test(size=(160, 80)):
|
|
185
|
+
view = app.query_one(WalletsScreen)
|
|
186
|
+
|
|
187
|
+
# 1. Test "Everything Loaded" -> No Retry
|
|
188
|
+
view.balance_cache = {
|
|
189
|
+
"gnosis": {
|
|
190
|
+
"0x1": {"NATIVE": "1.0", "TOKEN": "10.0"},
|
|
191
|
+
"0x2": {"NATIVE": "2.0", "TOKEN": "20.0"},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
view.chain_token_states["gnosis"] = {"TOKEN"}
|
|
195
|
+
|
|
196
|
+
# Should NOT trigger fetch
|
|
197
|
+
with patch.object(view, "fetch_all_balances") as mock_fetch:
|
|
198
|
+
view.check_balance_loading_status("gnosis")
|
|
199
|
+
mock_fetch.assert_not_called()
|
|
200
|
+
|
|
201
|
+
# 2. Test "Missing Native" -> Retry
|
|
202
|
+
view.balance_cache["gnosis"]["0x1"]["NATIVE"] = "Loading..."
|
|
203
|
+
with patch.object(view, "fetch_all_balances") as mock_fetch:
|
|
204
|
+
view.check_balance_loading_status("gnosis")
|
|
205
|
+
mock_fetch.assert_called_with("gnosis", ["TOKEN"])
|
|
206
|
+
|
|
207
|
+
# 3. Test "Missing Chain in Cache" -> Retry
|
|
208
|
+
del view.balance_cache["gnosis"]
|
|
209
|
+
with patch.object(view, "fetch_all_balances") as mock_fetch:
|
|
210
|
+
view.check_balance_loading_status("gnosis")
|
|
211
|
+
mock_fetch.assert_called_with("gnosis", ["TOKEN"])
|
|
212
|
+
|
|
213
|
+
# Restore for next
|
|
214
|
+
view.balance_cache = {"gnosis": {}}
|
|
215
|
+
|
|
216
|
+
# 4. Test "Missing Address in Cache" -> Retry
|
|
217
|
+
view.balance_cache["gnosis"] = {} # Empty
|
|
218
|
+
with patch.object(view, "fetch_all_balances") as mock_fetch:
|
|
219
|
+
view.check_balance_loading_status("gnosis")
|
|
220
|
+
mock_fetch.assert_called_with("gnosis", ["TOKEN"])
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.mark.asyncio
|
|
224
|
+
async def test_monitor_handler(mock_wallet, mock_deps):
|
|
225
|
+
app = IwaApp()
|
|
226
|
+
app.call_from_thread = lambda cb, *args, **kwargs: cb(*args, **kwargs)
|
|
227
|
+
|
|
228
|
+
async with app.run_test(size=(160, 80)):
|
|
229
|
+
view = app.query_one(WalletsScreen)
|
|
230
|
+
|
|
231
|
+
# Simulate txs
|
|
232
|
+
txs = [
|
|
233
|
+
{
|
|
234
|
+
"hash": "0xHash1",
|
|
235
|
+
"timestamp": 1700000000,
|
|
236
|
+
"from": "0x1",
|
|
237
|
+
"to": "0x2",
|
|
238
|
+
"token": "NATIVE",
|
|
239
|
+
"value": 10**18,
|
|
240
|
+
"chain": "gnosis",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"hash": "0xHash2",
|
|
244
|
+
"timestamp": None,
|
|
245
|
+
"from": "0x3",
|
|
246
|
+
"to": "0x4",
|
|
247
|
+
"token": "DAI",
|
|
248
|
+
"value": 500,
|
|
249
|
+
"chain": "gnosis",
|
|
250
|
+
},
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
view.handle_new_txs(txs)
|
|
254
|
+
|
|
255
|
+
# Verify table rows added
|
|
256
|
+
table = app.query_one("#tx_table")
|
|
257
|
+
assert table.row_count >= 2
|
|
258
|
+
# Verify first row details
|
|
259
|
+
assert "Detected" in str(table.get_row("0xHash1"))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@pytest.mark.asyncio
|
|
263
|
+
async def test_token_fetch_retry_and_failure(mock_wallet, mock_deps):
|
|
264
|
+
app = IwaApp()
|
|
265
|
+
app.call_from_thread = lambda cb, *args, **kwargs: cb(*args, **kwargs)
|
|
266
|
+
|
|
267
|
+
async with app.run_test(size=(160, 80)):
|
|
268
|
+
view = app.query_one(WalletsScreen)
|
|
269
|
+
view.balance_cache = {}
|
|
270
|
+
view.chain_token_states["gnosis"] = {"TOKEN"}
|
|
271
|
+
|
|
272
|
+
# Check worker logic
|
|
273
|
+
worker = view.monitor_workers[0]
|
|
274
|
+
# We can't easily check internal state of the worker loop without complex mocking
|
|
275
|
+
# assert worker.monitor.chain_name in ["gnosis", "ethereum", "base"]
|
|
276
|
+
|
|
277
|
+
# Limit to 1 account to control call count
|
|
278
|
+
mock_wallet.key_storage.accounts = {"0x1": MagicMock(address="0x1", tag="Acc1")}
|
|
279
|
+
|
|
280
|
+
# Patch time.sleep to avoid wait
|
|
281
|
+
with patch("time.sleep"):
|
|
282
|
+
# Case 1: Success
|
|
283
|
+
mock_wallet.balance_service.get_erc20_balance_with_retry.return_value = 100.0
|
|
284
|
+
|
|
285
|
+
worker = view.fetch_all_balances("gnosis", ["TOKEN"])
|
|
286
|
+
await worker.wait()
|
|
287
|
+
# verify call made
|
|
288
|
+
mock_wallet.balance_service.get_erc20_balance_with_retry.assert_called()
|
|
289
|
+
# Should have updated cache (4 decimals)
|
|
290
|
+
assert view.balance_cache["gnosis"]["0x1"]["TOKEN"] == "100.0000"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@pytest.mark.asyncio
|
|
294
|
+
async def test_send_transaction_failure(mock_wallet, mock_deps):
|
|
295
|
+
app = IwaApp()
|
|
296
|
+
app.call_from_thread = lambda cb, *args, **kwargs: cb(*args, **kwargs)
|
|
297
|
+
|
|
298
|
+
async with app.run_test(size=(160, 80)):
|
|
299
|
+
view = app.query_one(WalletsScreen)
|
|
300
|
+
mock_wallet.send.side_effect = Exception("Tx Failed")
|
|
301
|
+
|
|
302
|
+
# Just ensure it doesn't crash
|
|
303
|
+
view.send_tx_worker("0x1", "0x2", "native", 1.0)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# --- Tests migrated from test_tui_clipboard_chain.py ---
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@pytest.mark.asyncio
|
|
310
|
+
async def test_wallets_view_clipboard(mock_wallet, mock_deps):
|
|
311
|
+
"""Test clipboard functionality for account and transaction cells."""
|
|
312
|
+
app = IwaApp()
|
|
313
|
+
async with app.run_test() as _:
|
|
314
|
+
view = app.query_one(WalletsScreen)
|
|
315
|
+
|
|
316
|
+
# Test on_account_cell_selected
|
|
317
|
+
mock_event = MagicMock()
|
|
318
|
+
mock_event.coordinate.column = 1
|
|
319
|
+
mock_event.value = "0xAddress"
|
|
320
|
+
|
|
321
|
+
with patch.dict("sys.modules", {"pyperclip": MagicMock()}):
|
|
322
|
+
view.on_account_cell_selected(mock_event)
|
|
323
|
+
|
|
324
|
+
# Test on_tx_cell_selected
|
|
325
|
+
mock_event_tx = MagicMock()
|
|
326
|
+
mock_event_tx.coordinate.column = 0
|
|
327
|
+
mock_event_tx.data_table.columns.values.return_value = [
|
|
328
|
+
MagicMock(label="Hash"),
|
|
329
|
+
MagicMock(label="Status"),
|
|
330
|
+
]
|
|
331
|
+
mock_event_tx.cell_key.row_key.value = "0xFullHash"
|
|
332
|
+
|
|
333
|
+
with patch.dict("sys.modules", {"pyperclip": MagicMock()}):
|
|
334
|
+
view.on_tx_cell_selected(mock_event_tx)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
from textual.widget import Widget
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class DummySelect(Widget):
|
|
341
|
+
"""Dummy Select widget for testing chain change."""
|
|
342
|
+
|
|
343
|
+
def __init__(self, *args, **kwargs):
|
|
344
|
+
super().__init__(id=kwargs.get("id"))
|
|
345
|
+
|
|
346
|
+
def set_options(self, options):
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@pytest.mark.asyncio
|
|
351
|
+
async def test_wallets_view_chain_change_detailed(mock_wallet, mock_deps):
|
|
352
|
+
"""Test chain change with detailed mocking."""
|
|
353
|
+
app = IwaApp()
|
|
354
|
+
async with app.run_test() as _:
|
|
355
|
+
view = app.query_one(WalletsScreen)
|
|
356
|
+
|
|
357
|
+
# Mock ChainInterfaces
|
|
358
|
+
with patch("iwa.tui.screens.wallets.ChainInterfaces") as mock_chains:
|
|
359
|
+
mock_interface = MagicMock()
|
|
360
|
+
mock_interface.chain.rpc = "http://rpc"
|
|
361
|
+
mock_interface.chain.native_currency = "ETH"
|
|
362
|
+
mock_interface.tokens = {"TOKEN": "0xToken"}
|
|
363
|
+
mock_chains.return_value.get.return_value = mock_interface
|
|
364
|
+
|
|
365
|
+
# Patch Select with DummyWidget to satisfy isinstance(w, Widget)
|
|
366
|
+
with patch("iwa.tui.screens.wallets.Select", side_effect=DummySelect):
|
|
367
|
+
# Chain changed event
|
|
368
|
+
mock_event = MagicMock()
|
|
369
|
+
mock_event.value = "gnosis"
|
|
370
|
+
mock_event.control.value = "gnosis"
|
|
371
|
+
view.active_chain = "ethereum"
|
|
372
|
+
|
|
373
|
+
# Trigger
|
|
374
|
+
await view.on_chain_changed(mock_event)
|
|
375
|
+
|
|
376
|
+
assert view.active_chain == "gnosis"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# --- Tests migrated from test_keystorage_edge_cases.py ---
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@pytest.mark.asyncio
|
|
383
|
+
async def test_wallets_view_actions():
|
|
384
|
+
"""Test WalletsScreen action methods."""
|
|
385
|
+
with patch("iwa.core.db.db") as mock_db:
|
|
386
|
+
mock_db.is_closed.return_value = True
|
|
387
|
+
|
|
388
|
+
app = IwaApp()
|
|
389
|
+
async with app.run_test() as _:
|
|
390
|
+
view = app.query_one(WalletsScreen)
|
|
391
|
+
|
|
392
|
+
# Test action_refresh
|
|
393
|
+
with patch.object(view, "refresh_accounts") as mock_refresh:
|
|
394
|
+
view.action_refresh()
|
|
395
|
+
mock_refresh.assert_called_with(force=True)
|
|
396
|
+
|
|
397
|
+
# Test on_unmount
|
|
398
|
+
view.start_monitor()
|
|
399
|
+
with patch.object(view, "stop_monitor") as mock_stop:
|
|
400
|
+
view.on_unmount()
|
|
401
|
+
mock_stop.assert_called()
|
|
402
|
+
|
|
403
|
+
# Test monitor_callback
|
|
404
|
+
with patch.object(view, "handle_new_txs") as _:
|
|
405
|
+
with patch.object(app, "call_from_thread") as mock_call:
|
|
406
|
+
view.monitor_callback([])
|
|
407
|
+
mock_call.assert_called_with(view.handle_new_txs, [])
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@pytest.mark.asyncio
|
|
411
|
+
async def test_wallets_view_resolve_tag(mock_wallet, mock_deps):
|
|
412
|
+
"""Test resolve_tag method."""
|
|
413
|
+
view = WalletsScreen(mock_wallet)
|
|
414
|
+
|
|
415
|
+
mock_acc = MagicMock()
|
|
416
|
+
mock_acc.address = "0xAddress"
|
|
417
|
+
mock_acc.tag = "MyTag"
|
|
418
|
+
|
|
419
|
+
mock_accounts = MagicMock()
|
|
420
|
+
mock_accounts.values.return_value = [mock_acc]
|
|
421
|
+
|
|
422
|
+
mock_wallet.key_storage.accounts = mock_accounts
|
|
423
|
+
mock_wallet.account_service.accounts = mock_accounts
|
|
424
|
+
|
|
425
|
+
tag = view.resolve_tag("0xAddress")
|
|
426
|
+
assert tag == "MyTag"
|
|
427
|
+
|
|
428
|
+
# Test fallback
|
|
429
|
+
mock_accounts.values.return_value = []
|
|
430
|
+
tag = view.resolve_tag("0xAddress")
|
|
431
|
+
assert tag == "0xAddr...ress"
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@pytest.mark.asyncio
|
|
435
|
+
async def test_create_safe_modal_cancel():
|
|
436
|
+
"""Test CreateSafeModal cancel button."""
|
|
437
|
+
from iwa.tui.modals import CreateSafeModal
|
|
438
|
+
|
|
439
|
+
modal = CreateSafeModal([])
|
|
440
|
+
|
|
441
|
+
mock_event = MagicMock()
|
|
442
|
+
mock_event.button.id = "cancel"
|
|
443
|
+
with patch.object(modal, "dismiss") as mock_dismiss:
|
|
444
|
+
modal.on_button_pressed(mock_event)
|
|
445
|
+
mock_dismiss.assert_called()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# --- Tests migrated from test_core_db_integration.py ---
|
|
449
|
+
|
|
450
|
+
from unittest.mock import PropertyMock
|
|
451
|
+
|
|
452
|
+
from textual.widgets import DataTable
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@pytest.mark.asyncio
|
|
456
|
+
async def test_create_safe_worker_no_rpc(mock_wallet, mock_deps):
|
|
457
|
+
"""Test create_safe_worker when no RPC is configured."""
|
|
458
|
+
view = WalletsScreen(mock_wallet)
|
|
459
|
+
|
|
460
|
+
with patch.object(WalletsScreen, "app", new_callable=PropertyMock) as mock_app_prop:
|
|
461
|
+
mock_app = MagicMock()
|
|
462
|
+
mock_app_prop.return_value = mock_app
|
|
463
|
+
view.notify = MagicMock()
|
|
464
|
+
|
|
465
|
+
with patch("iwa.tui.screens.wallets.ChainInterfaces") as mock_chains:
|
|
466
|
+
mock_interface = MagicMock()
|
|
467
|
+
mock_interface.chain.rpc = None
|
|
468
|
+
mock_chains.return_value.get.return_value = mock_interface
|
|
469
|
+
|
|
470
|
+
view.create_safe_worker("Tag", 1, ["0x1"], ["gnosis"])
|
|
471
|
+
|
|
472
|
+
assert mock_app.call_from_thread.call_count >= 1
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@pytest.mark.asyncio
|
|
476
|
+
async def test_create_safe_worker_exception(mock_wallet, mock_deps):
|
|
477
|
+
"""Test create_safe_worker handles exceptions."""
|
|
478
|
+
view = WalletsScreen(mock_wallet)
|
|
479
|
+
|
|
480
|
+
with patch.object(WalletsScreen, "app", new_callable=PropertyMock) as mock_app_prop:
|
|
481
|
+
mock_app = MagicMock()
|
|
482
|
+
mock_app_prop.return_value = mock_app
|
|
483
|
+
view.notify = MagicMock()
|
|
484
|
+
|
|
485
|
+
with patch("iwa.tui.screens.wallets.ChainInterfaces") as mock_chains:
|
|
486
|
+
mock_interface = MagicMock()
|
|
487
|
+
mock_interface.chain.rpc = "http://rpc"
|
|
488
|
+
mock_chains.return_value.get.return_value = mock_interface
|
|
489
|
+
|
|
490
|
+
mock_wallet.key_storage.create_safe.side_effect = Exception("Create Failed")
|
|
491
|
+
|
|
492
|
+
view.create_safe_worker("Tag", 1, ["0x1"], ["gnosis"])
|
|
493
|
+
|
|
494
|
+
assert mock_app.call_from_thread.call_count >= 1
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@pytest.mark.asyncio
|
|
498
|
+
async def test_action_quit():
|
|
499
|
+
"""Test IwaApp action_quit."""
|
|
500
|
+
app = IwaApp()
|
|
501
|
+
with patch.object(app, "exit") as mock_exit:
|
|
502
|
+
await app.action_quit()
|
|
503
|
+
mock_exit.assert_called_once()
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@pytest.mark.asyncio
|
|
507
|
+
async def test_wallets_view_lifecycle(mock_wallet, mock_deps):
|
|
508
|
+
"""Test WalletsScreen lifecycle (on_mount/compose)."""
|
|
509
|
+
app = IwaApp()
|
|
510
|
+
async with app.run_test() as _:
|
|
511
|
+
view = app.query_one(WalletsScreen)
|
|
512
|
+
assert view is not None
|
|
513
|
+
table = view.query_one("#accounts_table", DataTable)
|
|
514
|
+
assert len(table.columns) > 0
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@pytest.mark.asyncio
|
|
518
|
+
async def test_wallets_view_copy_address_fallback(mock_wallet, mock_deps):
|
|
519
|
+
"""Test clipboard fallback when pyperclip fails."""
|
|
520
|
+
app = IwaApp()
|
|
521
|
+
async with app.run_test() as _:
|
|
522
|
+
view = app.query_one(WalletsScreen)
|
|
523
|
+
|
|
524
|
+
mock_event = MagicMock()
|
|
525
|
+
mock_event.coordinate.column = 1
|
|
526
|
+
mock_event.value = "0xAddr"
|
|
527
|
+
|
|
528
|
+
mock_pyperclip = MagicMock()
|
|
529
|
+
mock_pyperclip.copy.side_effect = Exception("No clipboard")
|
|
530
|
+
|
|
531
|
+
with patch.dict("sys.modules", {"pyperclip": mock_pyperclip}):
|
|
532
|
+
with patch("iwa.tui.app.IwaApp.copy_to_clipboard") as mock_copy:
|
|
533
|
+
view.on_account_cell_selected(mock_event)
|
|
534
|
+
mock_copy.assert_called_with("0xAddr")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@pytest.mark.asyncio
|
|
538
|
+
async def test_enrich_logs_api_failure(mock_wallet, mock_deps):
|
|
539
|
+
"""Test enrich_and_log_txs handles API failures."""
|
|
540
|
+
app = IwaApp()
|
|
541
|
+
async with app.run_test():
|
|
542
|
+
view = app.query_one(WalletsScreen)
|
|
543
|
+
|
|
544
|
+
txs = [{"hash": "0x1", "token": "TOKEN", "chain": "gnosis"}]
|
|
545
|
+
|
|
546
|
+
with patch("iwa.tui.screens.wallets.PriceService") as mock_price:
|
|
547
|
+
mock_price.return_value.get_token_price.return_value = None
|
|
548
|
+
|
|
549
|
+
with patch("iwa.core.db.log_transaction") as _:
|
|
550
|
+
with patch("iwa.tui.screens.wallets.ChainInterfaces"):
|
|
551
|
+
if not view.monitor_workers:
|
|
552
|
+
view.start_monitor()
|
|
553
|
+
view.enrich_and_log_txs(txs)
|
|
554
|
+
# Just verify it doesn't crash
|