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,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
@@ -1,2 +0,0 @@
1
- def hello(name: str = "world") -> str:
2
- return f"Hello, {name}!"
hello.py DELETED
@@ -1,6 +0,0 @@
1
- def main():
2
- print("Hello!")
3
-
4
-
5
- if __name__ == "__main__":
6
- main()
@@ -1,10 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: iwa
3
- Version: 0.0.0
4
- Summary: Add your description here
5
- Requires-Python: >=3.12
6
- Description-Content-Type: text/markdown
7
-
8
- # Iwa
9
-
10
- A packaging example
@@ -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,,
@@ -1,2 +0,0 @@
1
- __init__
2
- hello
File without changes