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.
Files changed (191) hide show
  1. conftest.py +22 -0
  2. iwa/__init__.py +1 -0
  3. iwa/__main__.py +6 -0
  4. iwa/core/__init__.py +1 -0
  5. iwa/core/chain/__init__.py +68 -0
  6. iwa/core/chain/errors.py +47 -0
  7. iwa/core/chain/interface.py +514 -0
  8. iwa/core/chain/manager.py +38 -0
  9. iwa/core/chain/models.py +128 -0
  10. iwa/core/chain/rate_limiter.py +193 -0
  11. iwa/core/cli.py +210 -0
  12. iwa/core/constants.py +28 -0
  13. iwa/core/contracts/__init__.py +1 -0
  14. iwa/core/contracts/contract.py +297 -0
  15. iwa/core/contracts/erc20.py +79 -0
  16. iwa/core/contracts/multisend.py +71 -0
  17. iwa/core/db.py +317 -0
  18. iwa/core/keys.py +361 -0
  19. iwa/core/mnemonic.py +385 -0
  20. iwa/core/models.py +344 -0
  21. iwa/core/monitor.py +209 -0
  22. iwa/core/plugins.py +45 -0
  23. iwa/core/pricing.py +91 -0
  24. iwa/core/services/__init__.py +17 -0
  25. iwa/core/services/account.py +57 -0
  26. iwa/core/services/balance.py +113 -0
  27. iwa/core/services/plugin.py +88 -0
  28. iwa/core/services/safe.py +392 -0
  29. iwa/core/services/transaction.py +172 -0
  30. iwa/core/services/transfer/__init__.py +166 -0
  31. iwa/core/services/transfer/base.py +260 -0
  32. iwa/core/services/transfer/erc20.py +247 -0
  33. iwa/core/services/transfer/multisend.py +386 -0
  34. iwa/core/services/transfer/native.py +262 -0
  35. iwa/core/services/transfer/swap.py +326 -0
  36. iwa/core/settings.py +95 -0
  37. iwa/core/tables.py +60 -0
  38. iwa/core/test.py +27 -0
  39. iwa/core/tests/test_wallet.py +255 -0
  40. iwa/core/types.py +59 -0
  41. iwa/core/ui.py +99 -0
  42. iwa/core/utils.py +59 -0
  43. iwa/core/wallet.py +380 -0
  44. iwa/plugins/__init__.py +1 -0
  45. iwa/plugins/gnosis/__init__.py +5 -0
  46. iwa/plugins/gnosis/cow/__init__.py +6 -0
  47. iwa/plugins/gnosis/cow/quotes.py +148 -0
  48. iwa/plugins/gnosis/cow/swap.py +403 -0
  49. iwa/plugins/gnosis/cow/types.py +20 -0
  50. iwa/plugins/gnosis/cow_utils.py +44 -0
  51. iwa/plugins/gnosis/plugin.py +68 -0
  52. iwa/plugins/gnosis/safe.py +157 -0
  53. iwa/plugins/gnosis/tests/test_cow.py +227 -0
  54. iwa/plugins/gnosis/tests/test_safe.py +100 -0
  55. iwa/plugins/olas/__init__.py +5 -0
  56. iwa/plugins/olas/constants.py +106 -0
  57. iwa/plugins/olas/contracts/activity_checker.py +93 -0
  58. iwa/plugins/olas/contracts/base.py +10 -0
  59. iwa/plugins/olas/contracts/mech.py +49 -0
  60. iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
  61. iwa/plugins/olas/contracts/service.py +215 -0
  62. iwa/plugins/olas/contracts/staking.py +403 -0
  63. iwa/plugins/olas/importer.py +736 -0
  64. iwa/plugins/olas/mech_reference.py +135 -0
  65. iwa/plugins/olas/models.py +110 -0
  66. iwa/plugins/olas/plugin.py +243 -0
  67. iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
  68. iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
  69. iwa/plugins/olas/service_manager/__init__.py +60 -0
  70. iwa/plugins/olas/service_manager/base.py +113 -0
  71. iwa/plugins/olas/service_manager/drain.py +336 -0
  72. iwa/plugins/olas/service_manager/lifecycle.py +839 -0
  73. iwa/plugins/olas/service_manager/mech.py +322 -0
  74. iwa/plugins/olas/service_manager/staking.py +530 -0
  75. iwa/plugins/olas/tests/conftest.py +30 -0
  76. iwa/plugins/olas/tests/test_importer.py +128 -0
  77. iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
  78. iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
  79. iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
  80. iwa/plugins/olas/tests/test_olas_integration.py +561 -0
  81. iwa/plugins/olas/tests/test_olas_models.py +144 -0
  82. iwa/plugins/olas/tests/test_olas_view.py +258 -0
  83. iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
  84. iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
  85. iwa/plugins/olas/tests/test_plugin.py +70 -0
  86. iwa/plugins/olas/tests/test_plugin_full.py +212 -0
  87. iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
  88. iwa/plugins/olas/tests/test_service_manager.py +1065 -0
  89. iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
  90. iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
  91. iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
  92. iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
  93. iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
  94. iwa/plugins/olas/tests/test_service_staking.py +342 -0
  95. iwa/plugins/olas/tests/test_staking_integration.py +269 -0
  96. iwa/plugins/olas/tests/test_staking_validation.py +109 -0
  97. iwa/plugins/olas/tui/__init__.py +1 -0
  98. iwa/plugins/olas/tui/olas_view.py +952 -0
  99. iwa/tools/check_profile.py +67 -0
  100. iwa/tools/release.py +111 -0
  101. iwa/tools/reset_env.py +111 -0
  102. iwa/tools/reset_tenderly.py +362 -0
  103. iwa/tools/restore_backup.py +82 -0
  104. iwa/tui/__init__.py +1 -0
  105. iwa/tui/app.py +174 -0
  106. iwa/tui/modals/__init__.py +5 -0
  107. iwa/tui/modals/base.py +406 -0
  108. iwa/tui/rpc.py +63 -0
  109. iwa/tui/screens/__init__.py +1 -0
  110. iwa/tui/screens/wallets.py +749 -0
  111. iwa/tui/tests/test_app.py +125 -0
  112. iwa/tui/tests/test_rpc.py +139 -0
  113. iwa/tui/tests/test_wallets_refactor.py +30 -0
  114. iwa/tui/tests/test_widgets.py +123 -0
  115. iwa/tui/widgets/__init__.py +5 -0
  116. iwa/tui/widgets/base.py +100 -0
  117. iwa/tui/workers.py +42 -0
  118. iwa/web/dependencies.py +76 -0
  119. iwa/web/models.py +76 -0
  120. iwa/web/routers/accounts.py +115 -0
  121. iwa/web/routers/olas/__init__.py +24 -0
  122. iwa/web/routers/olas/admin.py +169 -0
  123. iwa/web/routers/olas/funding.py +135 -0
  124. iwa/web/routers/olas/general.py +29 -0
  125. iwa/web/routers/olas/services.py +378 -0
  126. iwa/web/routers/olas/staking.py +341 -0
  127. iwa/web/routers/state.py +65 -0
  128. iwa/web/routers/swap.py +617 -0
  129. iwa/web/routers/transactions.py +153 -0
  130. iwa/web/server.py +155 -0
  131. iwa/web/tests/test_web_endpoints.py +713 -0
  132. iwa/web/tests/test_web_olas.py +430 -0
  133. iwa/web/tests/test_web_swap.py +103 -0
  134. iwa-0.0.1a2.dist-info/METADATA +234 -0
  135. iwa-0.0.1a2.dist-info/RECORD +186 -0
  136. iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
  137. iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
  138. iwa-0.0.1a2.dist-info/top_level.txt +4 -0
  139. tests/legacy_cow.py +248 -0
  140. tests/legacy_safe.py +93 -0
  141. tests/legacy_transaction_retry_logic.py +51 -0
  142. tests/legacy_tui.py +440 -0
  143. tests/legacy_wallets_screen.py +554 -0
  144. tests/legacy_web.py +243 -0
  145. tests/test_account_service.py +120 -0
  146. tests/test_balance_service.py +186 -0
  147. tests/test_chain.py +490 -0
  148. tests/test_chain_interface.py +210 -0
  149. tests/test_cli.py +139 -0
  150. tests/test_contract.py +195 -0
  151. tests/test_db.py +180 -0
  152. tests/test_drain_coverage.py +174 -0
  153. tests/test_erc20.py +95 -0
  154. tests/test_gnosis_plugin.py +111 -0
  155. tests/test_keys.py +449 -0
  156. tests/test_legacy_wallet.py +1285 -0
  157. tests/test_main.py +13 -0
  158. tests/test_mnemonic.py +217 -0
  159. tests/test_modals.py +109 -0
  160. tests/test_models.py +213 -0
  161. tests/test_monitor.py +202 -0
  162. tests/test_multisend.py +84 -0
  163. tests/test_plugin_service.py +119 -0
  164. tests/test_pricing.py +143 -0
  165. tests/test_rate_limiter.py +199 -0
  166. tests/test_reset_tenderly.py +202 -0
  167. tests/test_rpc_view.py +73 -0
  168. tests/test_safe_coverage.py +139 -0
  169. tests/test_safe_service.py +168 -0
  170. tests/test_service_manager_integration.py +61 -0
  171. tests/test_service_manager_structure.py +31 -0
  172. tests/test_service_transaction.py +176 -0
  173. tests/test_staking_router.py +71 -0
  174. tests/test_staking_simple.py +31 -0
  175. tests/test_tables.py +76 -0
  176. tests/test_transaction_service.py +161 -0
  177. tests/test_transfer_multisend.py +179 -0
  178. tests/test_transfer_native.py +220 -0
  179. tests/test_transfer_security.py +93 -0
  180. tests/test_transfer_structure.py +37 -0
  181. tests/test_transfer_swap_unit.py +155 -0
  182. tests/test_ui_coverage.py +66 -0
  183. tests/test_utils.py +53 -0
  184. tests/test_workers.py +91 -0
  185. tools/verify_drain.py +183 -0
  186. __init__.py +0 -2
  187. hello.py +0 -6
  188. iwa-0.0.0.dist-info/METADATA +0 -10
  189. iwa-0.0.0.dist-info/RECORD +0 -6
  190. iwa-0.0.0.dist-info/top_level.txt +0 -2
  191. {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