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/test_cli.py ADDED
@@ -0,0 +1,139 @@
1
+ import sys
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ import pytest
5
+ from typer.testing import CliRunner
6
+
7
+
8
+ @pytest.fixture
9
+ def iwa_cli_module():
10
+ mock_cowpy = MagicMock()
11
+ modules_to_patch = {
12
+ "cowdao_cowpy": mock_cowpy,
13
+ "cowdao_cowpy.common": MagicMock(),
14
+ "cowdao_cowpy.common.chains": MagicMock(),
15
+ "cowdao_cowpy.app_data": MagicMock(),
16
+ "cowdao_cowpy.app_data.utils": MagicMock(),
17
+ "cowdao_cowpy.contracts": MagicMock(),
18
+ "cowdao_cowpy.contracts.order": MagicMock(),
19
+ "cowdao_cowpy.contracts.sign": MagicMock(),
20
+ "cowdao_cowpy.cow": MagicMock(),
21
+ "cowdao_cowpy.cow.swap": MagicMock(),
22
+ "cowdao_cowpy.order_book": MagicMock(),
23
+ "cowdao_cowpy.order_book.api": MagicMock(),
24
+ "cowdao_cowpy.order_book.config": MagicMock(),
25
+ "cowdao_cowpy.order_book.generated": MagicMock(),
26
+ "cowdao_cowpy.order_book.generated.model": MagicMock(),
27
+ }
28
+
29
+ with patch.dict(sys.modules, modules_to_patch):
30
+ if "iwa.core.cli" in sys.modules:
31
+ del sys.modules["iwa.core.cli"]
32
+ if "iwa.core.wallet" in sys.modules:
33
+ pass
34
+
35
+ with patch("iwa.core.wallet.Wallet"):
36
+ import iwa.core.cli
37
+
38
+ yield iwa.core.cli.iwa_cli
39
+
40
+
41
+ runner = CliRunner()
42
+
43
+
44
+ @pytest.fixture
45
+ def cli(iwa_cli_module):
46
+ return iwa_cli_module
47
+
48
+
49
+ @pytest.fixture
50
+ def mock_key_storage():
51
+ with patch("iwa.core.cli.KeyStorage") as mock:
52
+ yield mock.return_value
53
+
54
+
55
+ @pytest.fixture
56
+ def mock_wallet():
57
+ with patch("iwa.core.cli.Wallet") as mock:
58
+ yield mock.return_value
59
+
60
+
61
+ def test_account_create(cli, mock_key_storage):
62
+ result = runner.invoke(cli, ["wallet", "create", "--tag", "test"])
63
+ assert result.exit_code == 0
64
+ mock_key_storage.create_account.assert_called_with("test")
65
+
66
+
67
+ def test_account_create_error(cli, mock_key_storage):
68
+ mock_key_storage.create_account.side_effect = ValueError("Error creating account")
69
+ result = runner.invoke(cli, ["wallet", "create", "--tag", "test"])
70
+ assert result.exit_code == 1
71
+ assert "Error: Error creating account" in result.stdout
72
+
73
+
74
+ def test_account_list(cli, mock_wallet):
75
+ mock_wallet.get_accounts_balances.return_value = ({}, None)
76
+ with (
77
+ patch("iwa.core.cli.list_accounts") as mock_list_accounts,
78
+ patch("iwa.core.cli.ChainInterfaces"),
79
+ ):
80
+ result = runner.invoke(cli, ["wallet", "list", "--chain", "gnosis", "--balances", "native"])
81
+ assert result.exit_code == 0
82
+ mock_wallet.get_accounts_balances.assert_called_with("gnosis", ["native"])
83
+ mock_list_accounts.assert_called_once()
84
+
85
+
86
+ def test_account_send(cli, mock_wallet):
87
+ result = runner.invoke(
88
+ cli, ["wallet", "send", "--from", "sender", "--to", "receiver", "--amount", "1.0"]
89
+ )
90
+ assert result.exit_code == 0
91
+ mock_wallet.send.assert_called()
92
+
93
+
94
+ def test_erc20_transfer_from(cli, mock_wallet):
95
+ result = runner.invoke(
96
+ cli,
97
+ [
98
+ "wallet",
99
+ "transfer-from",
100
+ "--from",
101
+ "from",
102
+ "--sender",
103
+ "sender",
104
+ "--recipient",
105
+ "recipient",
106
+ "--token",
107
+ "token",
108
+ "--amount",
109
+ "1.0",
110
+ ],
111
+ )
112
+ assert result.exit_code == 0
113
+ mock_wallet.transfer_from_erc20.assert_called()
114
+
115
+
116
+ def test_erc20_approve(cli, mock_wallet):
117
+ result = runner.invoke(
118
+ cli,
119
+ [
120
+ "wallet",
121
+ "approve",
122
+ "--owner",
123
+ "owner",
124
+ "--spender",
125
+ "spender",
126
+ "--token",
127
+ "token",
128
+ "--amount",
129
+ "1.0",
130
+ ],
131
+ )
132
+ assert result.exit_code == 0
133
+ mock_wallet.approve_erc20.assert_called()
134
+
135
+
136
+ def test_drain_wallet(cli, mock_wallet):
137
+ result = runner.invoke(cli, ["wallet", "drain", "--from", "from", "--to", "to"])
138
+ assert result.exit_code == 0
139
+ mock_wallet.drain.assert_called()
tests/test_contract.py ADDED
@@ -0,0 +1,195 @@
1
+ from pathlib import Path
2
+ from unittest.mock import MagicMock, mock_open, patch
3
+
4
+ import pytest
5
+ from web3.exceptions import ContractCustomError
6
+
7
+ from iwa.core.contracts.contract import ContractInstance
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_chain_interface():
12
+ with patch("iwa.core.contracts.contract.ChainInterfaces") as mock:
13
+ mock_ci = mock.return_value.get.return_value
14
+ mock_ci.web3.eth.contract.return_value = MagicMock()
15
+ yield mock_ci
16
+
17
+
18
+ @pytest.fixture
19
+ def mock_abi_file():
20
+ abi_content = '[{"type": "function", "name": "testFunc", "inputs": []}, {"type": "error", "name": "CustomError", "inputs": [{"type": "uint256", "name": "code"}]}, {"type": "event", "name": "TestEvent", "inputs": []}]'
21
+ with patch("builtins.open", mock_open(read_data=abi_content)):
22
+ yield
23
+
24
+
25
+ class MockContract(ContractInstance):
26
+ name = "test_contract"
27
+ abi_path = Path("test.json")
28
+
29
+
30
+ def test_init(mock_chain_interface, mock_abi_file):
31
+ contract = MockContract("0xAddress", "gnosis")
32
+ assert contract.address == "0xAddress"
33
+ assert contract.abi is not None
34
+ assert "0x" in str(contract.error_selectors.keys()) # Check if selector generated
35
+
36
+
37
+ def test_init_abi_dict(mock_chain_interface):
38
+ abi_content = '{"abi": [{"type": "function", "name": "testFunc"}]}'
39
+ with patch("builtins.open", mock_open(read_data=abi_content)):
40
+ contract = MockContract("0xAddress", "gnosis")
41
+ assert contract.abi == [{"type": "function", "name": "testFunc"}]
42
+
43
+
44
+ def test_call(mock_chain_interface, mock_abi_file):
45
+ contract = MockContract("0xAddress", "gnosis")
46
+ contract.contract.functions.testFunc.return_value.call.return_value = "result"
47
+ assert contract.call("testFunc") == "result"
48
+
49
+
50
+ def test_prepare_transaction_success(mock_chain_interface, mock_abi_file):
51
+ contract = MockContract("0xAddress", "gnosis")
52
+ mock_chain_interface.calculate_transaction_params.return_value = {"gas": 100}
53
+ contract.contract.functions.testFunc.return_value.build_transaction.return_value = {
54
+ "data": "0x"
55
+ }
56
+
57
+ tx = contract.prepare_transaction("testFunc", {}, {})
58
+ assert tx == {"data": "0x"}
59
+
60
+
61
+ def test_prepare_transaction_custom_error_known(mock_chain_interface, mock_abi_file):
62
+ contract = MockContract("0xAddress", "gnosis")
63
+ # Selector for CustomError(uint256)
64
+ # We need to calculate it or capture what load_error_selectors produced
65
+ selector = list(contract.error_selectors.keys())[0] # 0x...
66
+ # Encode args: uint256(123)
67
+ encoded_args = "0" * 62 + "7b" # 123 hex
68
+ error_data = f"{selector}{encoded_args}"
69
+
70
+ contract.contract.functions.testFunc.return_value.build_transaction.side_effect = (
71
+ ContractCustomError(error_data)
72
+ )
73
+
74
+ # Now the function returns None and logs the error instead of raising
75
+ with patch("iwa.core.contracts.contract.logger") as mock_logger:
76
+ result = contract.prepare_transaction("testFunc", {}, {})
77
+ assert result is None
78
+ # Verify error was logged
79
+ mock_logger.error.assert_called()
80
+
81
+
82
+ def test_prepare_transaction_custom_error_unknown(mock_chain_interface, mock_abi_file):
83
+ contract = MockContract("0xAddress", "gnosis")
84
+ error_data = "0x12345678" # Unknown selector
85
+
86
+ contract.contract.functions.testFunc.return_value.build_transaction.side_effect = (
87
+ ContractCustomError(error_data)
88
+ )
89
+
90
+ # Now the function returns None and logs the error instead of raising
91
+ with patch("iwa.core.contracts.contract.logger") as mock_logger:
92
+ result = contract.prepare_transaction("testFunc", {}, {})
93
+ assert result is None
94
+ # Verify error was logged
95
+ mock_logger.error.assert_called()
96
+
97
+
98
+ def test_prepare_transaction_revert_string(mock_chain_interface, mock_abi_file):
99
+ contract = MockContract("0xAddress", "gnosis")
100
+ # Encoded Error(string) with "Error" as the message
101
+ encoded_error = "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000054572726f72000000000000000000000000000000000000000000000000000000"
102
+ e = Exception("msg", encoded_error)
103
+
104
+ contract.contract.functions.testFunc.return_value.build_transaction.side_effect = e
105
+
106
+ with patch("iwa.core.contracts.contract.logger") as mock_logger:
107
+ tx = contract.prepare_transaction("testFunc", {}, {})
108
+ assert tx is None
109
+ # Should log the decoded error
110
+ mock_logger.error.assert_called()
111
+
112
+
113
+ def test_prepare_transaction_other_exception(mock_chain_interface, mock_abi_file):
114
+ contract = MockContract("0xAddress", "gnosis")
115
+ # The code expects e.args[1] to exist, so we must provide it
116
+ e = Exception("Generic Error", "Some Data")
117
+ contract.contract.functions.testFunc.return_value.build_transaction.side_effect = e
118
+
119
+ with patch("iwa.core.contracts.contract.logger") as mock_logger:
120
+ tx = contract.prepare_transaction("testFunc", {}, {})
121
+ assert tx is None
122
+ mock_logger.error.assert_called()
123
+
124
+
125
+ def test_extract_events(mock_chain_interface, mock_abi_file):
126
+ contract = MockContract("0xAddress", "gnosis")
127
+ receipt = MagicMock()
128
+
129
+ # Mock event class and its process_receipt method
130
+ mock_event_instance = MagicMock()
131
+
132
+ # Create a log object that supports both ["event"] and .args
133
+ mock_log = MagicMock()
134
+ mock_log.__getitem__.side_effect = lambda key: "TestEvent" if key == "event" else None
135
+ mock_log.args = {"arg1": 1}
136
+
137
+ mock_event_instance.process_receipt.return_value = [mock_log]
138
+ mock_event_class = MagicMock(return_value=mock_event_instance)
139
+
140
+ # Mock contract.events dictionary-like access
141
+ contract.contract.events = MagicMock()
142
+
143
+ def get_event(name):
144
+ if name == "TestEvent":
145
+ return mock_event_class
146
+ raise KeyError(name)
147
+
148
+ contract.contract.events.__getitem__.side_effect = get_event
149
+
150
+ # Explicitly set abi on the mock contract object
151
+ contract.contract.abi = contract.abi
152
+
153
+ events = contract.extract_events(receipt)
154
+ assert len(events) == 1
155
+ assert events[0]["name"] == "TestEvent"
156
+
157
+
158
+ def test_extract_events_edge_cases(mock_chain_interface):
159
+ # Custom ABI with multiple event types to test different paths
160
+ abi_content = '[{"type": "event", "name": "MissingEvent", "inputs": []}, {"type": "event", "name": "EmptyLogsEvent", "inputs": []}, {"type": "event", "name": "ErrorEvent", "inputs": []}, {"type": "function", "name": "NotAnEvent", "inputs": []}]'
161
+
162
+ with patch("builtins.open", mock_open(read_data=abi_content)):
163
+ contract = MockContract("0xAddress", "gnosis")
164
+
165
+ receipt = MagicMock()
166
+
167
+ # Mock contract.events
168
+ contract.contract.events = MagicMock()
169
+
170
+ # 1. MissingEvent: raises KeyError when accessed
171
+ # 2. EmptyLogsEvent: returns empty list from process_receipt
172
+ # 3. ErrorEvent: raises Exception from process_receipt
173
+
174
+ mock_empty_logs_event = MagicMock()
175
+ mock_empty_logs_event.return_value.process_receipt.return_value = []
176
+
177
+ mock_error_event = MagicMock()
178
+ mock_error_event.return_value.process_receipt.side_effect = Exception("Processing error")
179
+
180
+ def get_event(name):
181
+ if name == "MissingEvent":
182
+ raise KeyError(name)
183
+ if name == "EmptyLogsEvent":
184
+ return mock_empty_logs_event
185
+ if name == "ErrorEvent":
186
+ return mock_error_event
187
+ return MagicMock()
188
+
189
+ contract.contract.events.__getitem__.side_effect = get_event
190
+
191
+ # Explicitly set abi on the mock contract object
192
+ contract.contract.abi = contract.abi
193
+
194
+ events = contract.extract_events(receipt)
195
+ assert len(events) == 0
tests/test_db.py ADDED
@@ -0,0 +1,180 @@
1
+ """Tests for database operations."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from iwa.core.db import init_db, log_transaction
6
+
7
+
8
+ def test_log_transaction_upsert():
9
+ """Test log_transaction creates new records."""
10
+ with patch("iwa.core.db.SentTransaction") as mock_model:
11
+ mock_model.get_or_none.return_value = None
12
+ mock_insert = mock_model.insert.return_value
13
+ mock_upsert = mock_insert.on_conflict_replace.return_value
14
+
15
+ log_transaction("0x123", "0xFrom", "0xTo", "DAI", 100, "gnosis")
16
+
17
+ mock_model.insert.assert_called_once()
18
+ _, kwargs = mock_model.insert.call_args
19
+ assert kwargs["tx_hash"] == "0x123"
20
+ assert kwargs["chain"] == "gnosis"
21
+
22
+ mock_upsert.execute.assert_called_once()
23
+
24
+
25
+ def test_log_transaction_update_preserve_fields():
26
+ """Test log_transaction preserves existing non-null fields."""
27
+ with patch("iwa.core.db.SentTransaction") as mock_model:
28
+ mock_instance = MagicMock()
29
+ mock_instance.token = "DAI"
30
+ mock_instance.value_eur = 10.0
31
+ mock_instance.amount_wei = "100"
32
+ mock_model.get_or_none.return_value = mock_instance
33
+
34
+ # Update with token="NATIVE" which should be ignored if existing is better
35
+ log_transaction("0x123", "0xFrom", "0xTo", "NATIVE", 0, "gnosis")
36
+
37
+ mock_model.insert.assert_called_once()
38
+ _, kwargs = mock_model.insert.call_args
39
+ # Should preserve DAI and 100
40
+ assert kwargs["token"] == "DAI"
41
+ assert kwargs["amount_wei"] == "100"
42
+
43
+
44
+ def test_log_transaction_error():
45
+ """Test log_transaction handles errors gracefully."""
46
+ with (
47
+ patch("iwa.core.db.SentTransaction") as mock_model,
48
+ patch("iwa.core.db.logger") as mock_logger,
49
+ ):
50
+ mock_model.get_or_none.side_effect = Exception("DB Error")
51
+
52
+ log_transaction("0x123", "0xFrom", "0xTo", "DAI", 100, "gnosis")
53
+
54
+ mock_logger.error.assert_called()
55
+
56
+
57
+ def test_init_db():
58
+ """Test init_db creates tables and runs migrations."""
59
+ with (
60
+ patch("iwa.core.db.db") as mock_db,
61
+ patch("iwa.core.db.SentTransaction") as mock_model,
62
+ patch("iwa.core.db.migrate") as mock_migrate,
63
+ patch("iwa.core.db.SqliteMigrator"),
64
+ ):
65
+ mock_db.get_columns.return_value = []
66
+
67
+ init_db()
68
+
69
+ mock_db.connect.assert_called_once()
70
+ mock_db.create_tables.assert_called_with([mock_model], safe=True)
71
+ assert mock_migrate.call_count >= 1
72
+
73
+
74
+ def test_init_db_closed_at_end():
75
+ """Test init_db closes connection at end."""
76
+ with (
77
+ patch("iwa.core.db.db") as mock_db,
78
+ patch("iwa.core.db.SentTransaction"),
79
+ patch("iwa.core.db.migrate"),
80
+ patch("iwa.core.db.SqliteMigrator"),
81
+ ):
82
+ mock_db.is_closed.side_effect = [True, False] # closed initially, then open
83
+ mock_db.get_columns.return_value = []
84
+
85
+ init_db()
86
+
87
+ mock_db.close.assert_called_once()
88
+
89
+
90
+ def test_init_db_get_columns_error():
91
+ """Test init_db handles get_columns error."""
92
+ with (
93
+ patch("iwa.core.db.db") as mock_db,
94
+ patch("iwa.core.db.SentTransaction"),
95
+ ):
96
+ mock_db.get_columns.side_effect = Exception("Table not found")
97
+
98
+ # Should not raise
99
+ init_db()
100
+
101
+
102
+ def test_run_migrations_drop_token_symbol():
103
+ """Test run_migrations drops deprecated token_symbol column."""
104
+ from iwa.core.db import run_migrations
105
+
106
+ with patch("iwa.core.db.SqliteMigrator"), patch("iwa.core.db.migrate") as mock_migrate:
107
+ columns = ["token_symbol", "from_tag", "price_eur", "tags"]
108
+
109
+ run_migrations(columns)
110
+
111
+ # Should have called migrate to drop token_symbol
112
+ assert mock_migrate.called
113
+
114
+
115
+ def test_run_migrations_drop_token_symbol_error():
116
+ """Test run_migrations handles drop_column error."""
117
+ from iwa.core.db import run_migrations
118
+
119
+ with (
120
+ patch("iwa.core.db.SqliteMigrator"),
121
+ patch("iwa.core.db.migrate", side_effect=Exception("Drop failed")),
122
+ patch("iwa.core.db.logger") as mock_logger,
123
+ ):
124
+ columns = ["token_symbol", "from_tag", "price_eur", "tags"]
125
+
126
+ run_migrations(columns)
127
+
128
+ mock_logger.warning.assert_called()
129
+
130
+
131
+ def test_run_migrations_add_from_tag():
132
+ """Test run_migrations adds from_tag columns."""
133
+ from iwa.core.db import run_migrations
134
+
135
+ with patch("iwa.core.db.SqliteMigrator"), patch("iwa.core.db.migrate") as mock_migrate:
136
+ columns = ["price_eur", "tags"] # No from_tag
137
+
138
+ run_migrations(columns)
139
+
140
+ assert mock_migrate.called
141
+
142
+
143
+ def test_run_migrations_add_from_tag_error():
144
+ """Test run_migrations handles add_column error."""
145
+ from iwa.core.db import run_migrations
146
+
147
+ with (
148
+ patch("iwa.core.db.SqliteMigrator"),
149
+ patch("iwa.core.db.migrate", side_effect=Exception("Add failed")),
150
+ patch("iwa.core.db.logger") as mock_logger,
151
+ ):
152
+ columns = [] # No columns - triggers add
153
+
154
+ run_migrations(columns)
155
+
156
+ mock_logger.warning.assert_called()
157
+
158
+
159
+ def test_run_migrations_add_price_eur():
160
+ """Test run_migrations adds price columns."""
161
+ from iwa.core.db import run_migrations
162
+
163
+ with patch("iwa.core.db.SqliteMigrator"), patch("iwa.core.db.migrate") as mock_migrate:
164
+ columns = ["from_tag", "tags"] # No price_eur
165
+
166
+ run_migrations(columns)
167
+
168
+ assert mock_migrate.called
169
+
170
+
171
+ def test_run_migrations_add_tags():
172
+ """Test run_migrations adds tags column."""
173
+ from iwa.core.db import run_migrations
174
+
175
+ with patch("iwa.core.db.SqliteMigrator"), patch("iwa.core.db.migrate") as mock_migrate:
176
+ columns = ["from_tag", "price_eur"] # No tags
177
+
178
+ run_migrations(columns)
179
+
180
+ assert mock_migrate.called
@@ -0,0 +1,174 @@
1
+ """Tests for DrainManagerMixin coverage."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.plugins.olas.contracts.staking import StakingState
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_drain_manager():
12
+ """Create a mock DrainManagerMixin instance."""
13
+ from iwa.plugins.olas.service_manager.drain import DrainManagerMixin
14
+
15
+ class MockManager(DrainManagerMixin):
16
+ def __init__(self):
17
+ self.wallet = MagicMock()
18
+ self.service = MagicMock()
19
+ self.chain_name = "gnosis"
20
+ self.olas_config = MagicMock()
21
+
22
+ return MockManager()
23
+
24
+
25
+ def test_claim_rewards_no_service(mock_drain_manager):
26
+ """Test claim_rewards with no active service."""
27
+ mock_drain_manager.service = None
28
+ success, amount = mock_drain_manager.claim_rewards()
29
+ assert not success
30
+ assert amount == 0
31
+
32
+
33
+ def test_claim_rewards_not_staked(mock_drain_manager):
34
+ """Test claim_rewards when service is not staked."""
35
+ mock_drain_manager.service.staking_contract_address = None
36
+ success, amount = mock_drain_manager.claim_rewards()
37
+ assert not success
38
+ assert amount == 0
39
+
40
+
41
+ def test_claim_rewards_claim_tx_fails(mock_drain_manager):
42
+ """Test claim_rewards when prepare_claim_tx fails."""
43
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
44
+ mock_drain_manager.service.service_id = 1
45
+
46
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
47
+ mock_staking = mock_staking_cls.return_value
48
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
49
+ mock_staking.get_accrued_rewards.return_value = 1000000000000000000
50
+ mock_staking.prepare_claim_tx.return_value = None # Failed to prepare
51
+
52
+ success, amount = mock_drain_manager.claim_rewards()
53
+ assert not success
54
+ assert amount == 0
55
+
56
+
57
+ def test_claim_rewards_send_fails(mock_drain_manager):
58
+ """Test claim_rewards when transaction send fails."""
59
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
60
+ mock_drain_manager.service.service_id = 1
61
+ mock_drain_manager.wallet.master_account.address = "0xMaster"
62
+
63
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
64
+ mock_staking = mock_staking_cls.return_value
65
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
66
+ mock_staking.get_accrued_rewards.return_value = 1000000000000000000
67
+ mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
68
+ mock_drain_manager.wallet.sign_and_send_transaction.return_value = (False, None)
69
+
70
+ success, amount = mock_drain_manager.claim_rewards()
71
+ assert not success
72
+ assert amount == 0
73
+
74
+
75
+ def test_claim_rewards_success_no_event(mock_drain_manager):
76
+ """Test claim_rewards success but no RewardClaimed event."""
77
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
78
+ mock_drain_manager.service.service_id = 1
79
+ mock_drain_manager.wallet.master_account.address = "0xMaster"
80
+
81
+ with patch("iwa.plugins.olas.service_manager.drain.StakingContract") as mock_staking_cls:
82
+ mock_staking = mock_staking_cls.return_value
83
+ mock_staking.get_staking_state.return_value = StakingState.STAKED
84
+ mock_staking.get_accrued_rewards.return_value = 1000000000000000000
85
+ mock_staking.prepare_claim_tx.return_value = {"to": "0x", "data": "0x"}
86
+ mock_staking.extract_events.return_value = [] # No RewardClaimed event
87
+ mock_drain_manager.wallet.sign_and_send_transaction.return_value = (
88
+ True,
89
+ {"transactionHash": "0xHash"},
90
+ )
91
+
92
+ success, amount = mock_drain_manager.claim_rewards()
93
+ assert success
94
+ assert amount == 1000000000000000000
95
+
96
+
97
+ def test_withdraw_rewards_no_withdrawal_address(mock_drain_manager):
98
+ """Test withdraw_rewards with no withdrawal address configured."""
99
+ mock_drain_manager.service.multisig_address = "0xSafe"
100
+ mock_drain_manager.olas_config.withdrawal_address = None
101
+
102
+ success, amount = mock_drain_manager.withdraw_rewards()
103
+ assert not success
104
+ assert amount == 0
105
+
106
+
107
+ def test_drain_service_no_service(mock_drain_manager):
108
+ """Test drain_service with no active service."""
109
+ mock_drain_manager.service = None
110
+ result = mock_drain_manager.drain_service()
111
+ assert result == {}
112
+
113
+
114
+ def test_claim_rewards_if_needed_exception(mock_drain_manager):
115
+ """Test _claim_rewards_if_needed handles exceptions."""
116
+ mock_drain_manager.service.staking_contract_address = "0xStaking"
117
+
118
+ # Mock claim_rewards to raise
119
+ mock_drain_manager.claim_rewards = MagicMock(side_effect=Exception("Test Error"))
120
+
121
+ result = mock_drain_manager._claim_rewards_if_needed(claim_rewards=True)
122
+ assert result == 0
123
+
124
+
125
+ def test_drain_agent_account_exception(mock_drain_manager):
126
+ """Test _drain_agent_account handles drain exceptions."""
127
+ mock_drain_manager.service.agent_address = "0xAgent"
128
+ mock_drain_manager.wallet.drain.side_effect = Exception("Drain failed")
129
+
130
+ result = mock_drain_manager._drain_agent_account("0xTarget", "gnosis")
131
+ assert result is None
132
+
133
+
134
+ def test_drain_owner_account_exception(mock_drain_manager):
135
+ """Test _drain_owner_account handles drain exceptions."""
136
+ mock_drain_manager.service.service_owner_address = "0xOwner"
137
+ mock_drain_manager.wallet.drain.side_effect = Exception("Drain failed")
138
+
139
+ result = mock_drain_manager._drain_owner_account("0xTarget", "gnosis")
140
+ assert result is None
141
+
142
+
143
+ def test_normalize_drain_result_tuple(mock_drain_manager):
144
+ """Test _normalize_drain_result with tuple input."""
145
+
146
+ # Success tuple with HexBytes-like object
147
+ class FakeHexBytes:
148
+ def hex(self):
149
+ return "0xABCDEF"
150
+
151
+ result = mock_drain_manager._normalize_drain_result((True, {"transactionHash": FakeHexBytes()}))
152
+ assert result == "0xABCDEF"
153
+
154
+
155
+ def test_normalize_drain_result_failure_tuple(mock_drain_manager):
156
+ """Test _normalize_drain_result with failure tuple."""
157
+ result = mock_drain_manager._normalize_drain_result((False, {}))
158
+ assert result is None
159
+
160
+
161
+ def test_normalize_drain_result_none(mock_drain_manager):
162
+ """Test _normalize_drain_result with None input."""
163
+ result = mock_drain_manager._normalize_drain_result(None)
164
+ assert result is None
165
+
166
+
167
+ def test_drain_owner_skipped_when_equals_target(mock_drain_manager):
168
+ """Test _drain_owner_account is skipped when owner == target."""
169
+ mock_drain_manager.service.service_owner_address = "0xOwner123"
170
+ # Target is the same as owner (case-insensitive)
171
+ result = mock_drain_manager._drain_owner_account("0xowner123", "gnosis")
172
+ # Should skip and return None without calling drain
173
+ assert result is None
174
+ mock_drain_manager.wallet.drain.assert_not_called()