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,168 @@
1
+ """Tests for core SafeService."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.core.keys import EncryptedAccount, KeyStorage
8
+ from iwa.core.services.safe import SafeService
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_key_storage():
13
+ """Mock key storage."""
14
+ mock = MagicMock(spec=KeyStorage)
15
+ mock.accounts = {}
16
+
17
+ # Mock find_stored_account to return appropriate account types
18
+ def find_account(tag_or_addr):
19
+ if tag_or_addr == "deployer":
20
+ acc = MagicMock(spec=EncryptedAccount)
21
+ # Valid checksum address - Deployer
22
+ acc.address = "0xAB7C8803962c0f2F5BBBe3FA8BF0Dcd705084223"
23
+ return acc
24
+ if tag_or_addr == "owner1":
25
+ acc = MagicMock(spec=EncryptedAccount)
26
+ # Valid checksum address - Owner
27
+ acc.address = "0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c"
28
+ return acc
29
+ return None
30
+
31
+ mock.find_stored_account.side_effect = find_account
32
+
33
+ # Mock private key retrieval
34
+ mock._get_private_key.return_value = (
35
+ "0x1234567890123456789012345678901234567890123456789012345678901234"
36
+ )
37
+
38
+ return mock
39
+
40
+
41
+ @pytest.fixture
42
+ def mock_account_service():
43
+ """Mock account service."""
44
+ mock = MagicMock()
45
+ mock.get_tag_by_address.return_value = "deployer_tag"
46
+ return mock
47
+
48
+
49
+ @pytest.fixture
50
+ def mock_dependencies():
51
+ """Mock external dependencies (Safe, EthereumClient, etc)."""
52
+ with (
53
+ patch("iwa.core.services.safe.EthereumClient") as mock_client,
54
+ patch("iwa.core.services.safe.Safe") as mock_safe,
55
+ patch("iwa.core.services.safe.ProxyFactory") as mock_proxy_factory,
56
+ patch("iwa.core.services.safe.settings") as mock_settings,
57
+ patch("iwa.core.services.safe.log_transaction") as mock_log,
58
+ patch("iwa.core.services.safe.get_safe_master_copy_address") as mock_master,
59
+ patch("iwa.core.services.safe.get_safe_proxy_factory_address") as mock_factory,
60
+ ):
61
+ mock_settings.gnosis_rpc.get_secret_value.return_value = "http://rpc"
62
+
63
+ # Setup Safe creation return
64
+ mock_create_tx = MagicMock()
65
+ # Valid Checksum Address - New Safe (Matches Pydantic output)
66
+ mock_create_tx.contract_address = "0xbEC49fa140ACaa83533f900357DCD37866d50618"
67
+ mock_create_tx.tx_hash.hex.return_value = "0xTxHash"
68
+
69
+ mock_safe.create.return_value = mock_create_tx
70
+
71
+ # Setup ProxyFactory return
72
+ mock_deploy_tx = MagicMock()
73
+ # Valid checksum address - Salted Safe
74
+ mock_deploy_tx.contract_address = "0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"
75
+ mock_deploy_tx.tx_hash.hex.return_value = "0xTxHashSalted"
76
+
77
+ mock_proxy_factory.return_value.deploy_proxy_contract_with_nonce.return_value = (
78
+ mock_deploy_tx
79
+ )
80
+
81
+ # Fix for setup_data chaining
82
+ mock_function = MagicMock()
83
+ mock_function.build_transaction.return_value = {"data": "0x1234"}
84
+
85
+ mock_contract = MagicMock()
86
+ mock_contract.functions.setup.return_value = mock_function
87
+
88
+ mock_safe_instance = MagicMock()
89
+ mock_safe_instance.contract = mock_contract
90
+
91
+ def safe_side_effect(*args, **kwargs):
92
+ return mock_safe_instance
93
+
94
+ mock_safe.side_effect = safe_side_effect
95
+ mock_safe.create.return_value = mock_create_tx
96
+
97
+ # Mock get_transaction_receipt for gas calc
98
+ mock_client.return_value.w3.eth.get_transaction_receipt.return_value = {
99
+ "gasUsed": 50000,
100
+ "effectiveGasPrice": 20,
101
+ }
102
+
103
+ yield {
104
+ "client": mock_client,
105
+ "safe": mock_safe,
106
+ "proxy_factory": mock_proxy_factory,
107
+ "settings": mock_settings,
108
+ "log": mock_log,
109
+ "master": mock_master,
110
+ "factory": mock_factory,
111
+ }
112
+
113
+
114
+ def test_create_safe_standard(mock_key_storage, mock_account_service, mock_dependencies):
115
+ """Test standard create_safe without salt."""
116
+ service = SafeService(mock_key_storage, mock_account_service)
117
+
118
+ safe_account, tx_hash = service.create_safe(
119
+ deployer_tag_or_address="deployer",
120
+ owner_tags_or_addresses=["owner1"],
121
+ threshold=1,
122
+ chain_name="gnosis",
123
+ tag="MySafe",
124
+ )
125
+
126
+ # Checksum address matching what Pydantic/Web3 produces
127
+ assert safe_account.address == "0xbEC49fa140ACaa83533f900357DCD37866d50618"
128
+ assert safe_account.tag == "MySafe"
129
+ assert tx_hash == "0xTxHash"
130
+
131
+ mock_dependencies["safe"].create.assert_called_once()
132
+ mock_key_storage.save.assert_called_once()
133
+
134
+
135
+ def test_create_safe_with_salt(mock_key_storage, mock_account_service, mock_dependencies):
136
+ """Test create_safe with salt nonce."""
137
+ service = SafeService(mock_key_storage, mock_account_service)
138
+
139
+ mock_dependencies["client"].return_value.w3.eth.gas_price = 1000
140
+
141
+ safe_account, tx_hash = service.create_safe(
142
+ deployer_tag_or_address="deployer",
143
+ owner_tags_or_addresses=["owner1"],
144
+ threshold=1,
145
+ chain_name="gnosis",
146
+ tag="MySaltedSafe",
147
+ salt_nonce=123,
148
+ )
149
+
150
+ # 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5
151
+ assert safe_account.address == "0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"
152
+ assert tx_hash == "0xTxHashSalted"
153
+
154
+ # Check that manual ProxyFactory logic was used
155
+ mock_dependencies[
156
+ "proxy_factory"
157
+ ].return_value.deploy_proxy_contract_with_nonce.assert_called_once()
158
+ # Safe.create should NOT be called
159
+ mock_dependencies["safe"].create.assert_not_called()
160
+
161
+
162
+ def test_create_safe_invalid_deployer(mock_key_storage, mock_account_service):
163
+ """Test error when deployer invalid."""
164
+ mock_key_storage.find_stored_account.return_value = None
165
+ service = SafeService(mock_key_storage, mock_account_service)
166
+
167
+ with pytest.raises(ValueError, match="Deployer account .* not found"):
168
+ service.create_safe("invalid", [], 1, "gnosis")
@@ -0,0 +1,61 @@
1
+ import sys
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ import pytest
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_main():
9
+ # Create mocks for all required modules
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
+ # Ensure iwa.core.wallet is imported so we can patch it
31
+ # We need to make sure it's re-imported if it was already imported,
32
+ # to use the mocked cowdao_cowpy if needed, but actually we just need to patch Wallet.
33
+ # If iwa.core.wallet is already imported, patching Wallet is enough.
34
+ # But iwa.core.test might be already imported.
35
+ if "iwa.core.test" in sys.modules:
36
+ del sys.modules["iwa.core.test"]
37
+
38
+ # We need to ensure iwa.core.wallet is importable.
39
+ # If it's not in sys.modules, import it.
40
+ # But importing it triggers cowdao_cowpy import.
41
+ # Since we patched sys.modules, it should use our mocks.
42
+ if "iwa.core.wallet" not in sys.modules:
43
+ pass
44
+
45
+ with (
46
+ patch("iwa.core.wallet.Wallet"),
47
+ patch("iwa.core.test.ServiceManager") as mock_service_manager,
48
+ ):
49
+ # Import main here
50
+ from iwa.core.test import main
51
+
52
+ # Mock the instance returned by ServiceManager()
53
+ mock_sm_instance = mock_service_manager.return_value
54
+
55
+ await main()
56
+
57
+ # Verify ServiceManager was initialized with wallet
58
+ mock_service_manager.assert_called()
59
+
60
+ # Verify create was called
61
+ mock_sm_instance.create.assert_called_once()
@@ -0,0 +1,31 @@
1
+ from unittest.mock import MagicMock
2
+
3
+ from iwa.plugins.olas.service_manager import ServiceManager
4
+
5
+
6
+ def test_service_manager_structure():
7
+ """Verify that ServiceManager has all expected methods from mixins."""
8
+ wallet_mock = MagicMock()
9
+ sm = ServiceManager(wallet=wallet_mock)
10
+
11
+ # Check Lifecycle methods
12
+ assert hasattr(sm, "create")
13
+ assert hasattr(sm, "deploy")
14
+ assert hasattr(sm, "spin_up")
15
+ assert hasattr(sm, "wind_down")
16
+
17
+ # Check Staking methods
18
+ assert hasattr(sm, "stake")
19
+ assert hasattr(sm, "unstake")
20
+ assert hasattr(sm, "get_staking_status")
21
+
22
+ # Check Drain methods
23
+ assert hasattr(sm, "drain_service")
24
+ assert hasattr(sm, "claim_rewards")
25
+
26
+ # Check Mech methods
27
+ assert hasattr(sm, "send_mech_request")
28
+
29
+ # Check Base methods
30
+ assert hasattr(sm, "get")
31
+ assert hasattr(sm, "_init_contracts")
@@ -0,0 +1,176 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+
5
+ from iwa.core.chain import Gnosis
6
+ from iwa.core.services.transaction import TransactionService
7
+
8
+
9
+ @pytest.fixture
10
+ def mock_chain_interfaces():
11
+ with patch("iwa.core.services.transaction.ChainInterfaces") as mock:
12
+ instance = mock.return_value
13
+ gnosis_interface = MagicMock()
14
+ mock_chain = MagicMock(spec=Gnosis)
15
+ mock_chain.name = "Gnosis"
16
+ mock_chain.chain_id = 100
17
+ gnosis_interface.chain = mock_chain
18
+ gnosis_interface.web3 = MagicMock()
19
+ instance.get.return_value = gnosis_interface
20
+ yield instance
21
+
22
+
23
+ @pytest.fixture
24
+ def mock_key_storage():
25
+ return MagicMock()
26
+
27
+
28
+ @pytest.fixture
29
+ def mock_account_service():
30
+ return MagicMock()
31
+
32
+
33
+ @pytest.fixture
34
+ def transaction_service(mock_key_storage, mock_account_service):
35
+ return TransactionService(mock_key_storage, mock_account_service)
36
+
37
+
38
+ def test_sign_and_send_success(
39
+ transaction_service, mock_key_storage, mock_chain_interfaces, mock_account_service
40
+ ):
41
+ # Setup
42
+ tx = {"to": "0x123", "value": 100, "nonce": 5, "chainId": 100}
43
+ mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
44
+
45
+ chain_interface = mock_chain_interfaces.get.return_value
46
+ chain_interface.web3.eth.send_raw_transaction.return_value = b"tx_hash"
47
+ chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
48
+
49
+ mock_account_service.resolve_account.return_value = MagicMock(address="0xSigner")
50
+
51
+ # Call
52
+ success, receipt = transaction_service.sign_and_send(tx, "signer")
53
+
54
+ # Assert
55
+ assert success is True
56
+ assert receipt.status == 1
57
+ mock_key_storage.sign_transaction.assert_called_with(tx, "signer")
58
+ chain_interface.web3.eth.send_raw_transaction.assert_called_with(b"raw_tx")
59
+ chain_interface.wait_for_no_pending_tx.assert_called_with("0xSigner")
60
+
61
+
62
+ def test_sign_and_send_retry_on_low_gas(
63
+ transaction_service, mock_key_storage, mock_chain_interfaces
64
+ ):
65
+ # Setup
66
+ tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 10000}
67
+ mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
68
+
69
+ chain_interface = mock_chain_interfaces.get.return_value
70
+
71
+ # Simulate low gas error then success
72
+ from web3 import exceptions
73
+
74
+ chain_interface.web3.eth.send_raw_transaction.side_effect = [
75
+ exceptions.Web3RPCError("intrinsic gas too low"),
76
+ b"tx_hash",
77
+ ]
78
+ chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
79
+
80
+ # Call
81
+ with patch("time.sleep"):
82
+ success, receipt = transaction_service.sign_and_send(tx, "signer")
83
+
84
+ # Assert
85
+ assert success is True
86
+ assert chain_interface.web3.eth.send_raw_transaction.call_count == 2
87
+ # Verify gas increase
88
+ # arguments passed to sign_transaction should reflect updated gas
89
+ args, _ = mock_key_storage.sign_transaction.call_args_list[1]
90
+ assert args[0]["gas"] == 15000 # 10000 * 1.5
91
+
92
+
93
+ # --- Negative Tests ---
94
+
95
+
96
+ def test_sign_and_send_max_retries_exhausted(
97
+ transaction_service, mock_key_storage, mock_chain_interfaces
98
+ ):
99
+ """Test sign_and_send fails after max gas retries."""
100
+ tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 10000}
101
+ mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
102
+
103
+ chain_interface = mock_chain_interfaces.get.return_value
104
+
105
+ # Always fail with low gas error
106
+ from web3 import exceptions
107
+
108
+ chain_interface.web3.eth.send_raw_transaction.side_effect = exceptions.Web3RPCError(
109
+ "intrinsic gas too low"
110
+ )
111
+
112
+ with patch("time.sleep"):
113
+ success, receipt = transaction_service.sign_and_send(tx, "signer")
114
+
115
+ # Should fail after max retries
116
+ assert success is False
117
+ assert receipt == {} # Returns empty dict on failure
118
+ # Should have tried 3 times (max_retries)
119
+ assert chain_interface.web3.eth.send_raw_transaction.call_count == 3
120
+
121
+
122
+ def test_sign_and_send_transaction_reverted(
123
+ transaction_service, mock_key_storage, mock_chain_interfaces
124
+ ):
125
+ """Test sign_and_send handles reverted transaction."""
126
+ tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 21000}
127
+ mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
128
+
129
+ chain_interface = mock_chain_interfaces.get.return_value
130
+ chain_interface.web3.eth.send_raw_transaction.return_value = b"tx_hash"
131
+ # Transaction mined but reverted (status=0)
132
+ chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=0)
133
+
134
+ success, receipt = transaction_service.sign_and_send(tx, "signer")
135
+
136
+ assert success is False
137
+ assert receipt == {} # Returns empty dict on reverted tx
138
+
139
+
140
+ def test_sign_and_send_rpc_error_triggers_rotation(
141
+ transaction_service, mock_key_storage, mock_chain_interfaces
142
+ ):
143
+ """Test sign_and_send rotates RPC on connection error."""
144
+ tx = {"to": "0x123", "value": 100, "nonce": 5, "gas": 21000}
145
+ mock_key_storage.sign_transaction.return_value = MagicMock(raw_transaction=b"raw_tx")
146
+
147
+ chain_interface = mock_chain_interfaces.get.return_value
148
+
149
+ # First call fails with connection error, second succeeds
150
+ chain_interface.web3.eth.send_raw_transaction.side_effect = [
151
+ ConnectionError("Connection refused"),
152
+ b"tx_hash",
153
+ ]
154
+ chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(status=1)
155
+ chain_interface.rotate_rpc.return_value = True
156
+
157
+ with patch("time.sleep"):
158
+ success, receipt = transaction_service.sign_and_send(tx, "signer")
159
+
160
+ assert success is True
161
+ chain_interface.rotate_rpc.assert_called()
162
+
163
+
164
+ def test_sign_and_send_signer_not_found(
165
+ transaction_service, mock_key_storage, mock_chain_interfaces, mock_account_service
166
+ ):
167
+ """Test sign_and_send fails when signer account not found."""
168
+ tx = {"to": "0x123", "value": 100, "nonce": 5}
169
+
170
+ # Signing raises ValueError for unknown account
171
+ mock_key_storage.sign_transaction.side_effect = ValueError("Account not found")
172
+
173
+ success, receipt = transaction_service.sign_and_send(tx, "unknown_signer")
174
+
175
+ assert success is False
176
+ assert receipt == {} # Returns empty dict on failure
@@ -0,0 +1,71 @@
1
+ """Tests for staking.py router coverage."""
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+
6
+ def test_check_availability_exception():
7
+ """Test _check_availability handles contract call failures."""
8
+ from iwa.web.routers.olas.staking import _check_availability
9
+
10
+ mock_w3 = MagicMock()
11
+ mock_w3.eth.contract.side_effect = Exception("Contract error")
12
+
13
+ result = _check_availability("Test", "0xAddr", mock_w3, [])
14
+ assert result["name"] == "Test"
15
+ assert result["usage"] is None
16
+
17
+
18
+ def test_filter_contracts_no_availability():
19
+ """Test _filter_contracts excludes unavailable contracts."""
20
+ from iwa.web.routers.olas.staking import _filter_contracts
21
+
22
+ results = [
23
+ {"name": "A", "usage": {"available": False}, "min_staking_deposit": 100},
24
+ {"name": "B", "usage": {"available": True}, "min_staking_deposit": 100},
25
+ ]
26
+ filtered = _filter_contracts(results, None, None)
27
+ assert len(filtered) == 1
28
+ assert filtered[0]["name"] == "B"
29
+
30
+
31
+ def test_filter_contracts_bond_too_low():
32
+ """Test _filter_contracts excludes contracts where bond is too low."""
33
+ from iwa.web.routers.olas.staking import _filter_contracts
34
+
35
+ results = [
36
+ {"name": "A", "usage": {"available": True}, "min_staking_deposit": 1000},
37
+ ]
38
+ filtered = _filter_contracts(results, service_bond=500, service_token=None)
39
+ assert len(filtered) == 0
40
+
41
+
42
+ def test_filter_contracts_token_mismatch():
43
+ """Test _filter_contracts excludes contracts with wrong token."""
44
+ from iwa.web.routers.olas.staking import _filter_contracts
45
+
46
+ results = [
47
+ {
48
+ "name": "A",
49
+ "usage": {"available": True},
50
+ "min_staking_deposit": 100,
51
+ "staking_token": "0xOtherToken",
52
+ },
53
+ ]
54
+ filtered = _filter_contracts(results, service_bond=500, service_token="0xmytoken")
55
+ assert len(filtered) == 0
56
+
57
+
58
+ def test_filter_contracts_compatible():
59
+ """Test _filter_contracts includes compatible contracts."""
60
+ from iwa.web.routers.olas.staking import _filter_contracts
61
+
62
+ results = [
63
+ {
64
+ "name": "A",
65
+ "usage": {"available": True},
66
+ "min_staking_deposit": 100,
67
+ "staking_token": "0xMyToken",
68
+ },
69
+ ]
70
+ filtered = _filter_contracts(results, service_bond=500, service_token="0xmytoken")
71
+ assert len(filtered) == 1
@@ -0,0 +1,31 @@
1
+ from unittest.mock import MagicMock, mock_open, patch
2
+
3
+ from iwa.plugins.olas.contracts.staking import StakingContract
4
+
5
+
6
+ def test_staking_contract_coverage():
7
+ with (
8
+ patch("iwa.core.contracts.contract.ChainInterfaces") as mock_chains,
9
+ patch("iwa.plugins.olas.contracts.staking.ActivityCheckerContract"),
10
+ patch("builtins.open", mock_open(read_data="[]")),
11
+ ):
12
+ # Setup mocks
13
+ mock_interface = MagicMock()
14
+ mock_chains.return_value.get.return_value = mock_interface
15
+
16
+ # Mock contract calls in __init__
17
+ mock_interface.call_contract.side_effect = lambda method, *args: {
18
+ "activityChecker": "0xChecker",
19
+ "availableRewards": 100,
20
+ "balance": 1000,
21
+ "livenessPeriod": 3600,
22
+ "rewardsPerSecond": 1,
23
+ "maxNumServices": 10,
24
+ "minStakingDeposit": 100,
25
+ "minStakingDuration": 86400,
26
+ "stakingToken": "0xToken",
27
+ }.get(method, 0)
28
+
29
+ # Instantiate (This covers __init__ logic)
30
+ contract = StakingContract(address="0x123")
31
+ assert contract.address == "0x123"
tests/test_tables.py ADDED
@@ -0,0 +1,76 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+
5
+ from iwa.core.models import StoredAccount, StoredSafeAccount
6
+ from iwa.core.tables import list_accounts
7
+
8
+
9
+ @pytest.fixture
10
+ def mock_console():
11
+ with patch("iwa.core.tables.Console") as mock:
12
+ yield mock.return_value
13
+
14
+
15
+ @pytest.fixture
16
+ def mock_chain_interface():
17
+ mock = MagicMock()
18
+ mock.chain.native_currency = "ETH"
19
+ return mock
20
+
21
+
22
+ def test_list_accounts_empty(mock_console, mock_chain_interface):
23
+ list_accounts(None, mock_chain_interface, None, None)
24
+ mock_console.print.assert_called_once()
25
+ # Could verify table content but that's harder with mocks.
26
+ # Just ensuring it runs without error and prints something is good for coverage.
27
+
28
+
29
+ def test_list_accounts_eoa(mock_console, mock_chain_interface):
30
+ accounts = {
31
+ "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4": StoredAccount(
32
+ address="0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", tag="tag1"
33
+ )
34
+ }
35
+ list_accounts(accounts, mock_chain_interface, None, None)
36
+ mock_console.print.assert_called_once()
37
+
38
+
39
+ def test_list_accounts_safe(mock_console, mock_chain_interface):
40
+ accounts = {
41
+ "0x61a4f49e9dD1f90EB312889632FA956a21353720": StoredSafeAccount(
42
+ address="0x61a4f49e9dD1f90EB312889632FA956a21353720",
43
+ tag="tag2",
44
+ signers=["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"],
45
+ threshold=1,
46
+ chains=["gnosis"],
47
+ )
48
+ }
49
+ list_accounts(accounts, mock_chain_interface, None, None)
50
+ mock_console.print.assert_called_once()
51
+
52
+
53
+ def test_list_accounts_with_tokens(mock_console, mock_chain_interface):
54
+ accounts = {
55
+ "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4": StoredAccount(
56
+ address="0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", tag="tag1"
57
+ )
58
+ }
59
+ token_names = ["native", "OLAS"]
60
+ token_balances = {"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4": {"native": 1.5, "OLAS": 100.0}}
61
+ list_accounts(accounts, mock_chain_interface, token_names, token_balances)
62
+ mock_console.print.assert_called_once()
63
+
64
+
65
+ def test_list_accounts_empty_with_tokens(mock_console, mock_chain_interface):
66
+ token_names = ["native"]
67
+ token_balances = {} # Should be empty if no accounts
68
+ list_accounts(None, mock_chain_interface, token_names, token_balances)
69
+ mock_console.print.assert_called_once()
70
+
71
+
72
+ def test_list_accounts_no_accounts_but_tokens(mock_console, mock_chain_interface):
73
+ token_names = ["native"]
74
+ token_balances = {"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4": {"native": 1.5}}
75
+ list_accounts(None, mock_chain_interface, token_names, token_balances)
76
+ mock_console.print.assert_called_once()