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
tests/legacy_tui.py ADDED
@@ -0,0 +1,440 @@
1
+ from unittest.mock import MagicMock, PropertyMock, patch
2
+
3
+ import pytest
4
+ from textual.widgets import Button, Checkbox, DataTable, Input, Select, SelectionList
5
+
6
+ from iwa.tui.app import IwaApp
7
+ from iwa.tui.modals import CreateEOAModal, CreateSafeModal
8
+ from iwa.tui.screens.wallets import WalletsScreen
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_wallet():
13
+ with patch("iwa.tui.app.Wallet") as mock:
14
+ mock_inst = mock.return_value
15
+ mock_inst.key_storage = MagicMock()
16
+ mock_inst.account_service = MagicMock()
17
+ mock_inst.account_service.accounts = mock_inst.key_storage.accounts
18
+ mock_inst.account_service.get_account_data.side_effect = (
19
+ lambda: mock_inst.account_service.accounts
20
+ )
21
+ mock_inst.balance_service = MagicMock()
22
+ mock_inst.balance_service.get_native_balance_eth.return_value = 0.0
23
+ mock_inst.balance_service.get_erc20_balance_with_retry.return_value = 0.0
24
+
25
+ # Mock PluginService
26
+ mock_inst.plugin_service = MagicMock()
27
+ mock_inst.plugin_service.get_all_plugins.return_value = {}
28
+ yield mock_inst
29
+
30
+
31
+ @pytest.fixture(autouse=True)
32
+ def mock_deps():
33
+ with (
34
+ patch("iwa.tui.screens.wallets.EventMonitor"),
35
+ patch("iwa.tui.screens.wallets.PriceService") as mock_price,
36
+ patch("iwa.core.db.SentTransaction") as mock_sent_tx,
37
+ patch("iwa.core.db.log_transaction"),
38
+ patch("iwa.tui.screens.wallets.ChainInterfaces") as mock_chains,
39
+ ):
40
+ # Setup Price Service
41
+ mock_price.return_value.get_token_price.return_value = 10.0
42
+
43
+ # Setup Chain Interface Mock
44
+ # Setup distinct chain mocks
45
+ gnosis_mock = MagicMock()
46
+ gnosis_mock.tokens = {"TOKEN": "0xToken", "DAI": "0xDAI"}
47
+ gnosis_mock.chain.rpc = "http://gnosis"
48
+ gnosis_mock.chain.native_currency = "xDAI"
49
+
50
+ eth_mock = MagicMock()
51
+ eth_mock.tokens = {"USDC": "0xUSDC", "USDT": "0xUSDT"}
52
+ eth_mock.chain.rpc = "http://eth"
53
+ eth_mock.chain.native_currency = "ETH"
54
+
55
+ def get_chain(name):
56
+ if name == "ethereum":
57
+ return eth_mock
58
+ return gnosis_mock
59
+
60
+ mock_chains.return_value.get.side_effect = get_chain
61
+ mock_chains.return_value.items.return_value = [
62
+ ("gnosis", gnosis_mock),
63
+ ("ethereum", eth_mock),
64
+ ]
65
+
66
+ # Make yield return a dict or object to access specific mocks
67
+ yield {"chains": mock_chains, "pricing": mock_price, "sent_tx": mock_sent_tx}
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_app_startup(mock_wallet, mock_deps):
72
+ app = IwaApp()
73
+ async with app.run_test(size=(120, 60)):
74
+ assert app.title == "Iwa"
75
+ assert app.query_one(WalletsScreen)
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_create_eoa_modal_press(mock_wallet, mock_deps):
80
+ app = IwaApp()
81
+ async with app.run_test(size=(120, 60)) as pilot:
82
+ _ = app.query_one(WalletsScreen)
83
+ await pilot.click("#create_eoa_btn")
84
+ assert isinstance(app.screen, CreateEOAModal)
85
+
86
+ # Type name
87
+ await pilot.click("#tag_input")
88
+ await pilot.press(*list("TestEOA"))
89
+
90
+ # Click Create
91
+ await pilot.click("#create")
92
+ await pilot.pause(0.5)
93
+
94
+ mock_wallet.key_storage.create_account.assert_called_with("TestEOA")
95
+ assert not isinstance(app.screen, CreateEOAModal)
96
+
97
+
98
+ @pytest.mark.asyncio
99
+ async def test_create_safe_modal_compose(mock_wallet, mock_deps):
100
+ app = IwaApp()
101
+ async with app.run_test() as pilot:
102
+ # Setup accounts for owner selection
103
+ mock_wallet.key_storage.accounts = {
104
+ "0x1": MagicMock(address="0x1", tag="Owner1"),
105
+ "0x2": MagicMock(address="0x2", tag="Owner2"),
106
+ }
107
+ mock_wallet.account_service.accounts = mock_wallet.key_storage.accounts
108
+ mock_wallet.account_service.get_account_data.return_value = {}
109
+ mock_wallet.account_service.resolve_account.side_effect = (
110
+ lambda tag: mock_wallet.key_storage.get_account(tag)
111
+ )
112
+
113
+ # Unit test compose structure directly
114
+ modal = CreateSafeModal(
115
+ [(acc.tag, acc.address) for acc in mock_wallet.key_storage.accounts.values()]
116
+ )
117
+ await app.push_screen(modal)
118
+ await pilot.pause()
119
+
120
+ # Check if we have the inputs
121
+ assert modal.query_one("#tag_input", Input)
122
+ assert modal.query_one("#threshold_input", Input)
123
+ assert modal.query_one("#owners_list", SelectionList)
124
+ assert modal.query_one("#chains_list", SelectionList)
125
+ assert modal.query_one("#create", Button)
126
+ assert modal.query_one("#cancel", Button)
127
+
128
+
129
+ @pytest.mark.asyncio
130
+ async def test_create_safe_modal_handlers():
131
+ app = IwaApp()
132
+ async with app.run_test() as _:
133
+ # Unit test CreateSafeModal handlers
134
+ modal = CreateSafeModal([])
135
+ modal.dismiss = MagicMock()
136
+
137
+ # Test Cancel
138
+ cancel_event = MagicMock()
139
+ cancel_event.button.id = "cancel"
140
+ modal.on_button_pressed(cancel_event)
141
+ modal.dismiss.assert_called_once()
142
+
143
+ # Test Create (placeholder logic)
144
+ create_event = MagicMock()
145
+ create_event.button.id = "create"
146
+
147
+ # Mock query_one since modal is not mounted
148
+ modal.query_one = MagicMock()
149
+
150
+ # Mock return values for tag and threshold inputs
151
+ tag_input_mock = MagicMock()
152
+ tag_input_mock.value = "TestSafe"
153
+ threshold_input_mock = MagicMock()
154
+ threshold_input_mock.value = "1"
155
+
156
+ # Mock SelectionList
157
+ owners_list_mock = MagicMock()
158
+ owners_list_mock.selected = ["0x1", "0x2"]
159
+
160
+ # Mock SelectionList for chains
161
+ chains_list_mock = MagicMock()
162
+ chains_list_mock.selected = ["gnosis"]
163
+
164
+ def query_side_effect(selector, *args):
165
+ if selector == "#tag_input":
166
+ return tag_input_mock
167
+ if selector == "#threshold_input":
168
+ return threshold_input_mock
169
+ if selector == "#owners_list":
170
+ return owners_list_mock
171
+ if selector == "#chains_list":
172
+ return chains_list_mock
173
+ return MagicMock()
174
+
175
+ modal.query_one.side_effect = query_side_effect
176
+
177
+ modal.on_button_pressed(create_event)
178
+
179
+ # Teardown
180
+ from iwa.core.db import db
181
+
182
+ if not db.is_closed():
183
+ db.close()
184
+
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_wallets_screen_buttons(mock_wallet):
188
+ # Unit test WalletsScreen handler for Create Safe
189
+ view = WalletsScreen(mock_wallet)
190
+
191
+ # Mock 'app' property using PropertyMock since it's read-only
192
+ with patch.object(WalletsScreen, "app", new_callable=PropertyMock) as mock_app_prop:
193
+ mock_app = MagicMock()
194
+ mock_app_prop.return_value = mock_app
195
+
196
+ # Mock wallet key storage for accounts list
197
+ mock_wallet.key_storage.accounts = {}
198
+
199
+ safe_btn_event = MagicMock()
200
+ safe_btn_event.button.id = "create_safe_btn"
201
+
202
+ view.on_button_pressed(safe_btn_event)
203
+
204
+ args = mock_app.push_screen.call_args[0]
205
+ assert isinstance(args[0], CreateSafeModal)
206
+ callback = args[1]
207
+
208
+ # Test callback logic
209
+ with patch.object(view, "create_safe_worker") as mock_worker:
210
+ # Case 1: Success
211
+ callback(
212
+ {"tag": "MySafe", "threshold": 2, "owners": ["0x1", "0x2"], "chains": ["gnosis"]}
213
+ )
214
+ mock_worker.assert_called_with("MySafe", 2, ["0x1", "0x2"], ["gnosis"])
215
+
216
+
217
+ @pytest.mark.asyncio
218
+ async def test_send_transaction_ui(mock_wallet, mock_deps):
219
+ app = IwaApp()
220
+ async with app.run_test(size=(200, 200)) as pilot:
221
+ view = app.query_one(WalletsScreen)
222
+ mock_wallet.key_storage.accounts = {
223
+ "addr1": MagicMock(address="0x1", tag="Acc1"),
224
+ "addr2": MagicMock(address="0x2", tag="Acc2"),
225
+ }
226
+ await view.refresh_ui_for_chain()
227
+ await pilot.pause()
228
+
229
+ # Force table height to avoid pushing button off screen
230
+ app.query_one("#accounts_table").styles.height = 10
231
+ await pilot.pause()
232
+
233
+ app.query_one("#from_addr", Select).value = "0x1"
234
+ app.query_one("#to_addr", Select).value = "0x2"
235
+ app.query_one("#amount", Input).value = "1.0"
236
+ mock_wallet.send.return_value = "0xTxHash"
237
+
238
+ # Click by focus/enter to avoid layout/OutOfBounds issues
239
+ btn = app.query_one("#send_btn")
240
+ btn.focus()
241
+ await pilot.press("enter")
242
+ await pilot.pause()
243
+
244
+
245
+ @pytest.mark.asyncio
246
+ async def test_view_methods_direct(mock_wallet, mock_deps):
247
+ """Test methods directly for coverage."""
248
+ app = IwaApp()
249
+ async with app.run_test(size=(160, 80)) as pilot:
250
+ view = app.query_one(WalletsScreen)
251
+
252
+ mock_acc = MagicMock(address="0xABC", tag="Tag1")
253
+ mock_wallet.key_storage.get_account.side_effect = (
254
+ lambda tag: mock_acc if tag == "0xABC" else None
255
+ )
256
+ mock_wallet.account_service.accounts = {"a1": mock_acc}
257
+ assert view.resolve_tag("0xABC") == "Tag1"
258
+ assert view.resolve_tag("0xXYZ") == "0xXYZ...xXYZ"
259
+
260
+ view.refresh_accounts(force=True)
261
+
262
+ txs = [
263
+ {
264
+ "hash": "0xH",
265
+ "from": "0xF",
266
+ "to": "0xT",
267
+ "value": 10**18,
268
+ "token": "NATIVE",
269
+ "timestamp": 1234567890,
270
+ }
271
+ ]
272
+
273
+ # Enrich txs needs web3 mock
274
+ chains_mock = mock_deps["chains"]
275
+ mock_interface = chains_mock.return_value.get.return_value
276
+ mock_interface.web3.eth.get_transaction_receipt.return_value = {
277
+ "gasUsed": 21000,
278
+ "effectiveGasPrice": 10**9,
279
+ }
280
+
281
+ # Mock from_wei to return float compatible
282
+ mock_interface.web3.from_wei.return_value = 1.0
283
+
284
+ # Execute
285
+ view.enrich_and_log_txs(txs)
286
+ await pilot.pause()
287
+
288
+ view.on_checkbox_changed(MagicMock(checkbox=MagicMock(id="cb_TOKEN"), value=True))
289
+ if view.active_chain in view.chain_token_states:
290
+ assert "TOKEN" in view.chain_token_states[view.active_chain]
291
+ view.on_checkbox_changed(MagicMock(checkbox=MagicMock(id="cb_TOKEN"), value=False))
292
+ assert "TOKEN" not in view.chain_token_states[view.active_chain]
293
+
294
+
295
+ @pytest.mark.asyncio
296
+ async def test_load_recent_txs(mock_wallet, mock_deps):
297
+ # Setup Mock SentTransaction from fixture
298
+ mock_sent_tx_cls = mock_deps["sent_tx"]
299
+
300
+ mock_tx = MagicMock()
301
+ mock_tx.timestamp.strftime.return_value = "2025-01-01 12:00:00"
302
+ mock_tx.from_address = "0x1"
303
+ mock_tx.to_address = "0x2"
304
+ mock_tx.value_eur = 10.5
305
+ mock_tx.amount_wei = 10**18
306
+ mock_tx.token_symbol = "ETH"
307
+ mock_tx.tx_hash = "0xHash"
308
+ mock_tx.chain = "ethereum"
309
+ mock_tx.tags = '["tag1"]'
310
+ mock_tx.gas_cost = 1000
311
+ mock_tx.gas_value_eur = 0.1
312
+ mock_tx.from_tag = "FromTag"
313
+ mock_tx.to_tag = "ToTag"
314
+ mock_tx.token = "ETH"
315
+
316
+ # Configure mock chain
317
+ # Allow timestamp > datetime comparison
318
+ mock_ts_field = MagicMock()
319
+ mock_ts_field.__gt__ = MagicMock(return_value=True)
320
+ # Also need desc() for order_by
321
+ mock_ts_field.desc.return_value = "DESC_ORDER"
322
+ mock_sent_tx_cls.timestamp = mock_ts_field
323
+
324
+ mock_sent_tx_cls.select.return_value.where.return_value.order_by.return_value = [mock_tx]
325
+
326
+ app = IwaApp()
327
+ async with app.run_test(size=(160, 80)) as pilot:
328
+ _ = app.query_one(WalletsScreen)
329
+ # Give time for on_mount -> load_recent_txs to run
330
+ await pilot.pause(0.5)
331
+
332
+ # Verify load_recent_txs was called
333
+ table = app.query_one("#tx_table", DataTable)
334
+ assert table.row_count > 0
335
+
336
+ # Check first row presence
337
+ assert "0xHash" in table.rows
338
+
339
+ # Verify content of the row
340
+ row = table.get_row("0xHash")
341
+ # Check date format
342
+ assert "2025-01-01 12:00:00" in str(row[0])
343
+ # Check value in EUR (now at index 6: Time, Chain, From, To, Token, Amount, Value)
344
+ assert "€10.50" in str(row[6])
345
+
346
+
347
+ @pytest.mark.asyncio
348
+ async def test_chain_switching(mock_wallet, mock_deps):
349
+ """Test chain selector functionality."""
350
+ app = IwaApp()
351
+ async with app.run_test(size=(160, 80)) as pilot:
352
+ view = app.query_one(WalletsScreen)
353
+
354
+ # Initial chain is Gnosis (default)
355
+ assert view.active_chain == "gnosis"
356
+
357
+ # Change to Ethereum
358
+ chain_select = app.query_one("#chain_select", Select)
359
+
360
+ # Stop existing monitor workers before switching chain
361
+ if hasattr(view, "monitor_workers"):
362
+ for w in view.monitor_workers:
363
+ w.stop()
364
+
365
+ # But setting .value property on Select DOES trigger Changed event.
366
+ chain_select.value = "ethereum"
367
+
368
+ # Wait for event to process
369
+ await pilot.pause()
370
+ assert len(view.monitor_workers) > 0
371
+ # worker = view.monitor_workers[0]
372
+ # assert worker.monitor.chain_interface.chain.name in ["gnosis", "ethereum"]
373
+
374
+ # Verify active chain updated
375
+ assert view.active_chain == "ethereum"
376
+
377
+ # Verify columns updated
378
+ # Gnosis had TOKEN, DAI. Ethereum has USDC, USDT.
379
+ table = app.query_one("#accounts_table", DataTable)
380
+ col_labels = [c.label.plain.upper() for c in table.columns.values()]
381
+
382
+ # Should contain standard columns + ETH native + USDC + USDT
383
+ assert "TAG" in col_labels
384
+ assert "ADDRESS" in col_labels
385
+ assert "ETH" in col_labels # Native
386
+ assert "USDC" in col_labels
387
+ assert "USDT" in col_labels
388
+ assert "DAI" not in col_labels
389
+
390
+
391
+ @pytest.mark.asyncio
392
+ async def test_token_overwrite(mock_wallet, mock_deps):
393
+ """Test that enabling a second token does not overwrite the first."""
394
+ app = IwaApp()
395
+ async with app.run_test(size=(160, 80)) as pilot:
396
+ view = app.query_one(WalletsScreen)
397
+
398
+ # Setup mock account
399
+ mock_wallet.key_storage.accounts = {"0x1": MagicMock(address="0x1", tag="TestAcc")}
400
+ mock_wallet.account_service.accounts = mock_wallet.key_storage.accounts
401
+ view.refresh_accounts(force=True)
402
+
403
+ # Switch to Ethereum (has USDC, USDT)
404
+ chain_select = app.query_one("#chain_select", Select)
405
+ chain_select.value = "ethereum"
406
+ await pilot.pause()
407
+
408
+ # Configure wallet mock to return balances
409
+ mock_wallet.balance_service.get_erc20_balance_with_retry.return_value = 100.0
410
+
411
+ cb_usdc = app.query_one("#cb_USDC", Checkbox)
412
+ cb_usdc.value = True
413
+ await pilot.pause()
414
+ await pilot.pause() # Wait for workers
415
+
416
+ # Enable USDT (Second token)
417
+ cb_usdt = app.query_one("#cb_USDT", Checkbox)
418
+ cb_usdt.value = True
419
+ # Manually ensure state is updated to avoid race conditions in test
420
+ if "USDT" not in view.chain_token_states.get("ethereum", set()):
421
+ view.chain_token_states.setdefault("ethereum", set()).add("USDT")
422
+ view.refresh_accounts()
423
+ await pilot.pause()
424
+ await pilot.pause() # Wait for workers
425
+
426
+ # Check table content
427
+ table = app.query_one("#accounts_table", DataTable)
428
+
429
+ addr = list(mock_wallet.key_storage.accounts.values())[0].address
430
+ row_idx = table.get_row_index(addr)
431
+
432
+ # Tag(0), Address(1), Type(2), Native(3), USDC(4), USDT(5)
433
+ # Check USDC column (4)
434
+ usdc_cell = table.get_row_at(row_idx)[4]
435
+ # Check USDT column (5)
436
+ usdt_cell = table.get_row_at(row_idx)[5]
437
+
438
+ # Let's check that col 5 is NOT empty string
439
+ assert str(usdt_cell) != "", "USDT column should not be empty"
440
+ assert str(usdc_cell) != "", "USDC column should not be empty"