iwa 0.0.0__py3-none-any.whl → 0.0.1a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. conftest.py +22 -0
  2. iwa/__init__.py +1 -0
  3. iwa/__main__.py +6 -0
  4. iwa/core/__init__.py +1 -0
  5. iwa/core/chain/__init__.py +68 -0
  6. iwa/core/chain/errors.py +47 -0
  7. iwa/core/chain/interface.py +514 -0
  8. iwa/core/chain/manager.py +38 -0
  9. iwa/core/chain/models.py +128 -0
  10. iwa/core/chain/rate_limiter.py +193 -0
  11. iwa/core/cli.py +210 -0
  12. iwa/core/constants.py +28 -0
  13. iwa/core/contracts/__init__.py +1 -0
  14. iwa/core/contracts/contract.py +297 -0
  15. iwa/core/contracts/erc20.py +79 -0
  16. iwa/core/contracts/multisend.py +71 -0
  17. iwa/core/db.py +317 -0
  18. iwa/core/keys.py +361 -0
  19. iwa/core/mnemonic.py +385 -0
  20. iwa/core/models.py +344 -0
  21. iwa/core/monitor.py +209 -0
  22. iwa/core/plugins.py +45 -0
  23. iwa/core/pricing.py +91 -0
  24. iwa/core/services/__init__.py +17 -0
  25. iwa/core/services/account.py +57 -0
  26. iwa/core/services/balance.py +113 -0
  27. iwa/core/services/plugin.py +88 -0
  28. iwa/core/services/safe.py +392 -0
  29. iwa/core/services/transaction.py +172 -0
  30. iwa/core/services/transfer/__init__.py +166 -0
  31. iwa/core/services/transfer/base.py +260 -0
  32. iwa/core/services/transfer/erc20.py +247 -0
  33. iwa/core/services/transfer/multisend.py +386 -0
  34. iwa/core/services/transfer/native.py +262 -0
  35. iwa/core/services/transfer/swap.py +326 -0
  36. iwa/core/settings.py +95 -0
  37. iwa/core/tables.py +60 -0
  38. iwa/core/test.py +27 -0
  39. iwa/core/tests/test_wallet.py +255 -0
  40. iwa/core/types.py +59 -0
  41. iwa/core/ui.py +99 -0
  42. iwa/core/utils.py +59 -0
  43. iwa/core/wallet.py +380 -0
  44. iwa/plugins/__init__.py +1 -0
  45. iwa/plugins/gnosis/__init__.py +5 -0
  46. iwa/plugins/gnosis/cow/__init__.py +6 -0
  47. iwa/plugins/gnosis/cow/quotes.py +148 -0
  48. iwa/plugins/gnosis/cow/swap.py +403 -0
  49. iwa/plugins/gnosis/cow/types.py +20 -0
  50. iwa/plugins/gnosis/cow_utils.py +44 -0
  51. iwa/plugins/gnosis/plugin.py +68 -0
  52. iwa/plugins/gnosis/safe.py +157 -0
  53. iwa/plugins/gnosis/tests/test_cow.py +227 -0
  54. iwa/plugins/gnosis/tests/test_safe.py +100 -0
  55. iwa/plugins/olas/__init__.py +5 -0
  56. iwa/plugins/olas/constants.py +106 -0
  57. iwa/plugins/olas/contracts/activity_checker.py +93 -0
  58. iwa/plugins/olas/contracts/base.py +10 -0
  59. iwa/plugins/olas/contracts/mech.py +49 -0
  60. iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
  61. iwa/plugins/olas/contracts/service.py +215 -0
  62. iwa/plugins/olas/contracts/staking.py +403 -0
  63. iwa/plugins/olas/importer.py +736 -0
  64. iwa/plugins/olas/mech_reference.py +135 -0
  65. iwa/plugins/olas/models.py +110 -0
  66. iwa/plugins/olas/plugin.py +243 -0
  67. iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
  68. iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
  69. iwa/plugins/olas/service_manager/__init__.py +60 -0
  70. iwa/plugins/olas/service_manager/base.py +113 -0
  71. iwa/plugins/olas/service_manager/drain.py +336 -0
  72. iwa/plugins/olas/service_manager/lifecycle.py +839 -0
  73. iwa/plugins/olas/service_manager/mech.py +322 -0
  74. iwa/plugins/olas/service_manager/staking.py +530 -0
  75. iwa/plugins/olas/tests/conftest.py +30 -0
  76. iwa/plugins/olas/tests/test_importer.py +128 -0
  77. iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
  78. iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
  79. iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
  80. iwa/plugins/olas/tests/test_olas_integration.py +561 -0
  81. iwa/plugins/olas/tests/test_olas_models.py +144 -0
  82. iwa/plugins/olas/tests/test_olas_view.py +258 -0
  83. iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
  84. iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
  85. iwa/plugins/olas/tests/test_plugin.py +70 -0
  86. iwa/plugins/olas/tests/test_plugin_full.py +212 -0
  87. iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
  88. iwa/plugins/olas/tests/test_service_manager.py +1065 -0
  89. iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
  90. iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
  91. iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
  92. iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
  93. iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
  94. iwa/plugins/olas/tests/test_service_staking.py +342 -0
  95. iwa/plugins/olas/tests/test_staking_integration.py +269 -0
  96. iwa/plugins/olas/tests/test_staking_validation.py +109 -0
  97. iwa/plugins/olas/tui/__init__.py +1 -0
  98. iwa/plugins/olas/tui/olas_view.py +952 -0
  99. iwa/tools/check_profile.py +67 -0
  100. iwa/tools/release.py +111 -0
  101. iwa/tools/reset_env.py +111 -0
  102. iwa/tools/reset_tenderly.py +362 -0
  103. iwa/tools/restore_backup.py +82 -0
  104. iwa/tui/__init__.py +1 -0
  105. iwa/tui/app.py +174 -0
  106. iwa/tui/modals/__init__.py +5 -0
  107. iwa/tui/modals/base.py +406 -0
  108. iwa/tui/rpc.py +63 -0
  109. iwa/tui/screens/__init__.py +1 -0
  110. iwa/tui/screens/wallets.py +749 -0
  111. iwa/tui/tests/test_app.py +125 -0
  112. iwa/tui/tests/test_rpc.py +139 -0
  113. iwa/tui/tests/test_wallets_refactor.py +30 -0
  114. iwa/tui/tests/test_widgets.py +123 -0
  115. iwa/tui/widgets/__init__.py +5 -0
  116. iwa/tui/widgets/base.py +100 -0
  117. iwa/tui/workers.py +42 -0
  118. iwa/web/dependencies.py +76 -0
  119. iwa/web/models.py +76 -0
  120. iwa/web/routers/accounts.py +115 -0
  121. iwa/web/routers/olas/__init__.py +24 -0
  122. iwa/web/routers/olas/admin.py +169 -0
  123. iwa/web/routers/olas/funding.py +135 -0
  124. iwa/web/routers/olas/general.py +29 -0
  125. iwa/web/routers/olas/services.py +378 -0
  126. iwa/web/routers/olas/staking.py +341 -0
  127. iwa/web/routers/state.py +65 -0
  128. iwa/web/routers/swap.py +617 -0
  129. iwa/web/routers/transactions.py +153 -0
  130. iwa/web/server.py +155 -0
  131. iwa/web/tests/test_web_endpoints.py +713 -0
  132. iwa/web/tests/test_web_olas.py +430 -0
  133. iwa/web/tests/test_web_swap.py +103 -0
  134. iwa-0.0.1a2.dist-info/METADATA +234 -0
  135. iwa-0.0.1a2.dist-info/RECORD +186 -0
  136. iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
  137. iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
  138. iwa-0.0.1a2.dist-info/top_level.txt +4 -0
  139. tests/legacy_cow.py +248 -0
  140. tests/legacy_safe.py +93 -0
  141. tests/legacy_transaction_retry_logic.py +51 -0
  142. tests/legacy_tui.py +440 -0
  143. tests/legacy_wallets_screen.py +554 -0
  144. tests/legacy_web.py +243 -0
  145. tests/test_account_service.py +120 -0
  146. tests/test_balance_service.py +186 -0
  147. tests/test_chain.py +490 -0
  148. tests/test_chain_interface.py +210 -0
  149. tests/test_cli.py +139 -0
  150. tests/test_contract.py +195 -0
  151. tests/test_db.py +180 -0
  152. tests/test_drain_coverage.py +174 -0
  153. tests/test_erc20.py +95 -0
  154. tests/test_gnosis_plugin.py +111 -0
  155. tests/test_keys.py +449 -0
  156. tests/test_legacy_wallet.py +1285 -0
  157. tests/test_main.py +13 -0
  158. tests/test_mnemonic.py +217 -0
  159. tests/test_modals.py +109 -0
  160. tests/test_models.py +213 -0
  161. tests/test_monitor.py +202 -0
  162. tests/test_multisend.py +84 -0
  163. tests/test_plugin_service.py +119 -0
  164. tests/test_pricing.py +143 -0
  165. tests/test_rate_limiter.py +199 -0
  166. tests/test_reset_tenderly.py +202 -0
  167. tests/test_rpc_view.py +73 -0
  168. tests/test_safe_coverage.py +139 -0
  169. tests/test_safe_service.py +168 -0
  170. tests/test_service_manager_integration.py +61 -0
  171. tests/test_service_manager_structure.py +31 -0
  172. tests/test_service_transaction.py +176 -0
  173. tests/test_staking_router.py +71 -0
  174. tests/test_staking_simple.py +31 -0
  175. tests/test_tables.py +76 -0
  176. tests/test_transaction_service.py +161 -0
  177. tests/test_transfer_multisend.py +179 -0
  178. tests/test_transfer_native.py +220 -0
  179. tests/test_transfer_security.py +93 -0
  180. tests/test_transfer_structure.py +37 -0
  181. tests/test_transfer_swap_unit.py +155 -0
  182. tests/test_ui_coverage.py +66 -0
  183. tests/test_utils.py +53 -0
  184. tests/test_workers.py +91 -0
  185. tools/verify_drain.py +183 -0
  186. __init__.py +0 -2
  187. hello.py +0 -6
  188. iwa-0.0.0.dist-info/METADATA +0 -10
  189. iwa-0.0.0.dist-info/RECORD +0 -6
  190. iwa-0.0.0.dist-info/top_level.txt +0 -2
  191. {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
tests/legacy_web.py ADDED
@@ -0,0 +1,243 @@
1
+ """Tests for the Web Server module."""
2
+
3
+ import pytest
4
+
5
+
6
+ class TestObscureUrl:
7
+ """Tests for URL obscuring utility."""
8
+
9
+ def test_obscure_url_hides_path(self):
10
+ """Test that _obscure_url hides the path and query."""
11
+ from iwa.web.server import _obscure_url
12
+
13
+ result = _obscure_url("https://rpc.example.com/v1/api-key-12345")
14
+ assert result == "https://rpc.example.com/..."
15
+ assert "api-key" not in result
16
+
17
+ def test_obscure_url_handles_empty(self):
18
+ """Test that _obscure_url handles empty URLs."""
19
+ from iwa.web.server import _obscure_url
20
+
21
+ result = _obscure_url("")
22
+ # Empty URL gives minimal output
23
+ assert "..." in result
24
+
25
+ def test_obscure_url_preserves_scheme_and_host(self):
26
+ """Test that scheme and host are preserved."""
27
+ from iwa.web.server import _obscure_url
28
+
29
+ result = _obscure_url("wss://ws.alchemy.com/v2/secret")
30
+ assert result == "wss://ws.alchemy.com/..."
31
+
32
+ def test_obscure_url_handles_no_path(self):
33
+ """Test URL with no path."""
34
+ from iwa.web.server import _obscure_url
35
+
36
+ result = _obscure_url("https://example.com")
37
+ assert result == "https://example.com/..."
38
+
39
+
40
+ class TestInputValidation:
41
+ """Tests for Pydantic input validation models."""
42
+
43
+ def test_transaction_request_valid(self):
44
+ """Test valid transaction request passes validation."""
45
+ from iwa.web.server import TransactionRequest
46
+
47
+ req = TransactionRequest(
48
+ from_address="0x1234567890abcdef1234567890abcdef12345678",
49
+ to_address="0xabcdef1234567890abcdef1234567890abcdef12",
50
+ amount_eth=1.0,
51
+ token="native",
52
+ chain="gnosis",
53
+ )
54
+ assert req.from_address == "0x1234567890abcdef1234567890abcdef12345678"
55
+ assert req.amount_eth == 1.0
56
+
57
+ def test_transaction_request_valid_with_tag(self):
58
+ """Test valid transaction request with tag passes validation."""
59
+ from iwa.web.server import TransactionRequest
60
+
61
+ req = TransactionRequest(
62
+ from_address="my_wallet",
63
+ to_address="receiver_wallet",
64
+ amount_eth=1.0,
65
+ token="native",
66
+ chain="gnosis",
67
+ )
68
+ assert req.from_address == "my_wallet"
69
+
70
+ def test_transaction_request_invalid_address_xss(self):
71
+ """Test that XSS in address is rejected."""
72
+ from pydantic import ValidationError
73
+
74
+ from iwa.web.server import TransactionRequest
75
+
76
+ with pytest.raises(ValidationError) as exc:
77
+ TransactionRequest(
78
+ from_address="<script>alert(1)</script>",
79
+ to_address="0x1234567890abcdef1234567890abcdef12345678",
80
+ amount_eth=1.0,
81
+ token="native",
82
+ chain="gnosis",
83
+ )
84
+ assert "alphanumeric" in str(exc.value).lower()
85
+
86
+ def test_transaction_request_invalid_address_format(self):
87
+ """Test that invalid hex address is rejected."""
88
+ from pydantic import ValidationError
89
+
90
+ from iwa.web.server import TransactionRequest
91
+
92
+ with pytest.raises(ValidationError) as exc:
93
+ TransactionRequest(
94
+ from_address="0xINVALIDHEXADDRESS",
95
+ to_address="0x1234567890abcdef1234567890abcdef12345678",
96
+ amount_eth=1.0,
97
+ token="native",
98
+ chain="gnosis",
99
+ )
100
+ assert "invalid" in str(exc.value).lower()
101
+
102
+ def test_transaction_request_empty_address(self):
103
+ """Test that empty address is rejected."""
104
+ from pydantic import ValidationError
105
+
106
+ from iwa.web.server import TransactionRequest
107
+
108
+ with pytest.raises(ValidationError):
109
+ TransactionRequest(
110
+ from_address="",
111
+ to_address="0x1234567890abcdef1234567890abcdef12345678",
112
+ amount_eth=1.0,
113
+ token="native",
114
+ chain="gnosis",
115
+ )
116
+
117
+ def test_transaction_request_negative_amount(self):
118
+ """Test that negative amounts are rejected."""
119
+ from pydantic import ValidationError
120
+
121
+ from iwa.web.server import TransactionRequest
122
+
123
+ with pytest.raises(ValidationError) as exc:
124
+ TransactionRequest(
125
+ from_address="0x1234567890abcdef1234567890abcdef12345678",
126
+ to_address="0xabcdef1234567890abcdef1234567890abcdef12",
127
+ amount_eth=-1.0,
128
+ token="native",
129
+ chain="gnosis",
130
+ )
131
+ assert "positive" in str(exc.value).lower()
132
+
133
+ def test_transaction_request_zero_amount(self):
134
+ """Test that zero amount is rejected."""
135
+ from pydantic import ValidationError
136
+
137
+ from iwa.web.server import TransactionRequest
138
+
139
+ with pytest.raises(ValidationError):
140
+ TransactionRequest(
141
+ from_address="0x1234567890abcdef1234567890abcdef12345678",
142
+ to_address="0xabcdef1234567890abcdef1234567890abcdef12",
143
+ amount_eth=0,
144
+ token="native",
145
+ chain="gnosis",
146
+ )
147
+
148
+ def test_transaction_request_excessive_amount(self):
149
+ """Test that excessive amount is rejected."""
150
+ from pydantic import ValidationError
151
+
152
+ from iwa.web.server import TransactionRequest
153
+
154
+ with pytest.raises(ValidationError) as exc:
155
+ TransactionRequest(
156
+ from_address="0x1234567890abcdef1234567890abcdef12345678",
157
+ to_address="0xabcdef1234567890abcdef1234567890abcdef12",
158
+ amount_eth=1e20,
159
+ token="native",
160
+ chain="gnosis",
161
+ )
162
+ assert "large" in str(exc.value).lower()
163
+
164
+ def test_transaction_request_xss_chain(self):
165
+ """Test that XSS in chain field is rejected."""
166
+ from pydantic import ValidationError
167
+
168
+ from iwa.web.server import TransactionRequest
169
+
170
+ with pytest.raises(ValidationError):
171
+ TransactionRequest(
172
+ from_address="0x1234567890abcdef1234567890abcdef12345678",
173
+ to_address="0xabcdef1234567890abcdef1234567890abcdef12",
174
+ amount_eth=1.0,
175
+ token="native",
176
+ chain="<script>",
177
+ )
178
+
179
+ def test_account_create_request_valid_tag(self):
180
+ """Test valid tag passes validation."""
181
+ from iwa.web.server import AccountCreateRequest
182
+
183
+ req = AccountCreateRequest(tag="my_wallet_123")
184
+ assert req.tag == "my_wallet_123"
185
+
186
+ def test_account_create_request_none_tag(self):
187
+ """Test None tag is allowed."""
188
+ from iwa.web.server import AccountCreateRequest
189
+
190
+ req = AccountCreateRequest(tag=None)
191
+ assert req.tag is None
192
+
193
+ def test_account_create_request_empty_tag(self):
194
+ """Test empty tag becomes None."""
195
+ from iwa.web.server import AccountCreateRequest
196
+
197
+ req = AccountCreateRequest(tag=" ")
198
+ assert req.tag is None
199
+
200
+ def test_account_create_request_xss_tag(self):
201
+ """Test that XSS in tag is rejected."""
202
+ from pydantic import ValidationError
203
+
204
+ from iwa.web.server import AccountCreateRequest
205
+
206
+ with pytest.raises(ValidationError) as exc:
207
+ AccountCreateRequest(tag="<script>alert(1)</script>")
208
+ assert "alphanumeric" in str(exc.value).lower()
209
+
210
+ def test_account_create_request_long_tag(self):
211
+ """Test that too-long tags are rejected."""
212
+ from pydantic import ValidationError
213
+
214
+ from iwa.web.server import AccountCreateRequest
215
+
216
+ with pytest.raises(ValidationError) as exc:
217
+ AccountCreateRequest(tag="a" * 51)
218
+ assert "long" in str(exc.value).lower()
219
+
220
+ def test_account_create_request_with_underscore_hyphen(self):
221
+ """Test that underscores and hyphens are allowed in tags."""
222
+ from iwa.web.server import AccountCreateRequest
223
+
224
+ req = AccountCreateRequest(tag="my-wallet_name")
225
+ assert req.tag == "my-wallet_name"
226
+
227
+
228
+ class TestAppConfiguration:
229
+ """Tests for app configuration."""
230
+
231
+ def test_cors_middleware_configured(self):
232
+ """Test that CORS middleware is configured."""
233
+ from iwa.web.server import app
234
+
235
+ # Check that user_middleware list is not empty (CORS was added)
236
+ assert len(app.user_middleware) > 0
237
+
238
+ def test_static_files_mounted(self):
239
+ """Test that static files are mounted."""
240
+ from iwa.web.server import app
241
+
242
+ routes = [r.path for r in app.routes if hasattr(r, "path")]
243
+ assert "/static" in routes
@@ -0,0 +1,120 @@
1
+ """Tests for AccountService."""
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+ import pytest
6
+
7
+ from iwa.core.constants import NATIVE_CURRENCY_ADDRESS
8
+ from iwa.core.models import EthereumAddress, StoredSafeAccount
9
+ from iwa.core.services.account import AccountService
10
+
11
+
12
+ @pytest.fixture
13
+ def mock_key_storage():
14
+ mock = MagicMock()
15
+ mock.master_account = StoredSafeAccount(
16
+ tag="master",
17
+ address="0x1111111111111111111111111111111111111111",
18
+ chains=["gnosis"],
19
+ threshold=1,
20
+ signers=["0x2222222222222222222222222222222222222222"],
21
+ )
22
+ mock.get_account.return_value = MagicMock(address="0x1234")
23
+ mock.get_tag_by_address.return_value = "my_tag"
24
+ mock.accounts = {}
25
+ return mock
26
+
27
+
28
+ @pytest.fixture
29
+ def account_service(mock_key_storage):
30
+ return AccountService(mock_key_storage)
31
+
32
+
33
+ def test_init(account_service, mock_key_storage):
34
+ """Test AccountService initialization."""
35
+ assert account_service.key_storage == mock_key_storage
36
+
37
+
38
+ def test_master_account(account_service, mock_key_storage):
39
+ """Test master_account property."""
40
+ result = account_service.master_account
41
+
42
+ assert result == mock_key_storage.master_account
43
+ assert result.tag == "master"
44
+
45
+
46
+ def test_get_token_address_native(account_service):
47
+ """Test get_token_address returns native address."""
48
+ mock_chain = MagicMock()
49
+
50
+ result = account_service.get_token_address("native", mock_chain)
51
+
52
+ assert result == EthereumAddress(NATIVE_CURRENCY_ADDRESS)
53
+
54
+
55
+ def test_get_token_address_valid_address(account_service):
56
+ """Test get_token_address with valid Ethereum address."""
57
+ mock_chain = MagicMock()
58
+ valid_address = "0x1234567890123456789012345678901234567890"
59
+
60
+ result = account_service.get_token_address(valid_address, mock_chain)
61
+
62
+ assert result == EthereumAddress(valid_address)
63
+
64
+
65
+ def test_get_token_address_by_name(account_service):
66
+ """Test get_token_address resolves token name."""
67
+ mock_chain = MagicMock()
68
+ mock_chain.get_token_address.return_value = EthereumAddress(
69
+ "0x6B175474E89094C44Da98b954EedeAC495271E01"
70
+ )
71
+
72
+ result = account_service.get_token_address("DAI", mock_chain)
73
+
74
+ assert result == EthereumAddress("0x6B175474E89094C44Da98b954EedeAC495271E01")
75
+ mock_chain.get_token_address.assert_called_with("DAI")
76
+
77
+
78
+ def test_get_token_address_not_found(account_service):
79
+ """Test get_token_address returns None for unknown token."""
80
+ mock_chain = MagicMock()
81
+ mock_chain.name = "gnosis"
82
+ mock_chain.get_token_address.return_value = None
83
+
84
+ result = account_service.get_token_address("UNKNOWN", mock_chain)
85
+
86
+ assert result is None
87
+
88
+
89
+ def test_resolve_account(account_service, mock_key_storage):
90
+ """Test resolve_account delegates to key_storage."""
91
+ result = account_service.resolve_account("my_tag")
92
+
93
+ mock_key_storage.get_account.assert_called_with("my_tag")
94
+ assert result.address == "0x1234"
95
+
96
+
97
+ def test_resolve_account_not_found(account_service, mock_key_storage):
98
+ """Test resolve_account returns None for unknown account."""
99
+ mock_key_storage.get_account.return_value = None
100
+
101
+ result = account_service.resolve_account("unknown")
102
+
103
+ assert result is None
104
+
105
+
106
+ def test_get_tag_by_address(account_service, mock_key_storage):
107
+ """Test get_tag_by_address delegates to key_storage."""
108
+ result = account_service.get_tag_by_address("0x1234")
109
+
110
+ mock_key_storage.get_tag_by_address.assert_called_with("0x1234")
111
+ assert result == "my_tag"
112
+
113
+
114
+ def test_get_account_data(account_service, mock_key_storage):
115
+ """Test get_account_data returns accounts dict."""
116
+ mock_key_storage.accounts = {"0x1234": MagicMock()}
117
+
118
+ result = account_service.get_account_data()
119
+
120
+ assert result == mock_key_storage.accounts
@@ -0,0 +1,186 @@
1
+ """Tests for BalanceService."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+
8
+ @pytest.fixture
9
+ def mock_chain_interfaces():
10
+ with patch("iwa.core.services.balance.ChainInterfaces") as mock:
11
+ instance = mock.return_value
12
+ gnosis_interface = MagicMock()
13
+ gnosis_interface.chain.name = "Gnosis"
14
+ gnosis_interface.get_native_balance_eth.return_value = 1.5
15
+ gnosis_interface.get_native_balance_wei.return_value = 1500000000000000000
16
+ instance.get.return_value = gnosis_interface
17
+ yield instance
18
+
19
+
20
+ @pytest.fixture
21
+ def mock_account_service():
22
+ mock = MagicMock()
23
+ mock.get_token_address.return_value = "0xTokenAddress"
24
+ mock.resolve_account.return_value = MagicMock(address="0xAccountAddress")
25
+ return mock
26
+
27
+
28
+ @pytest.fixture
29
+ def mock_key_storage():
30
+ return MagicMock()
31
+
32
+
33
+ @pytest.fixture
34
+ def balance_service(mock_key_storage, mock_account_service):
35
+ from iwa.core.services.balance import BalanceService
36
+
37
+ return BalanceService(mock_key_storage, mock_account_service)
38
+
39
+
40
+ def test_get_native_balance_eth(balance_service, mock_chain_interfaces, mock_account_service):
41
+ """Test get_native_balance_eth returns correct value."""
42
+ result = balance_service.get_native_balance_eth("0xAccount", "gnosis")
43
+
44
+ assert result == 1.5
45
+ # Now resolves account first, so expects resolved address
46
+ mock_chain_interfaces.get.return_value.get_native_balance_eth.assert_called_with(
47
+ "0xAccountAddress"
48
+ )
49
+ mock_account_service.resolve_account.assert_called_with("0xAccount")
50
+
51
+
52
+ def test_get_native_balance_wei(balance_service, mock_chain_interfaces, mock_account_service):
53
+ """Test get_native_balance_wei returns correct value."""
54
+ result = balance_service.get_native_balance_wei("0xAccount", "gnosis")
55
+
56
+ assert result == 1500000000000000000
57
+ # Now resolves account first, so expects resolved address
58
+ mock_chain_interfaces.get.return_value.get_native_balance_wei.assert_called_with(
59
+ "0xAccountAddress"
60
+ )
61
+ mock_account_service.resolve_account.assert_called_with("0xAccount")
62
+
63
+
64
+ def test_get_erc20_balance_eth_success(
65
+ balance_service, mock_chain_interfaces, mock_account_service
66
+ ):
67
+ """Test get_erc20_balance_eth returns correct value."""
68
+ with patch("iwa.core.services.balance.ERC20Contract") as mock_erc20:
69
+ mock_erc20.return_value.balance_of_eth.return_value = 100.5
70
+
71
+ result = balance_service.get_erc20_balance_eth("0xAccount", "DAI", "gnosis")
72
+
73
+ assert result == 100.5
74
+ mock_account_service.get_token_address.assert_called()
75
+ mock_account_service.resolve_account.assert_called_with("0xAccount")
76
+
77
+
78
+ def test_get_erc20_balance_eth_token_not_found(
79
+ balance_service, mock_chain_interfaces, mock_account_service
80
+ ):
81
+ """Test get_erc20_balance_eth returns None when token not found."""
82
+ mock_account_service.get_token_address.return_value = None
83
+
84
+ result = balance_service.get_erc20_balance_eth("0xAccount", "UNKNOWN", "gnosis")
85
+
86
+ assert result is None
87
+
88
+
89
+ def test_get_erc20_balance_eth_account_not_found(
90
+ balance_service, mock_chain_interfaces, mock_account_service
91
+ ):
92
+ """Test get_erc20_balance_eth returns None when account not found."""
93
+ mock_account_service.resolve_account.return_value = None
94
+
95
+ result = balance_service.get_erc20_balance_eth("unknown_tag", "DAI", "gnosis")
96
+
97
+ assert result is None
98
+
99
+
100
+ def test_get_erc20_balance_wei_success(
101
+ balance_service, mock_chain_interfaces, mock_account_service
102
+ ):
103
+ """Test get_erc20_balance_wei returns correct value."""
104
+ with patch("iwa.core.services.balance.ERC20Contract") as mock_erc20:
105
+ mock_erc20.return_value.balance_of_wei.return_value = 100500000000000000000
106
+
107
+ result = balance_service.get_erc20_balance_wei("0xAccount", "DAI", "gnosis")
108
+
109
+ assert result == 100500000000000000000
110
+
111
+
112
+ def test_get_erc20_balance_wei_token_not_found(
113
+ balance_service, mock_chain_interfaces, mock_account_service
114
+ ):
115
+ """Test get_erc20_balance_wei returns None when token not found."""
116
+ mock_account_service.get_token_address.return_value = None
117
+
118
+ result = balance_service.get_erc20_balance_wei("0xAccount", "UNKNOWN", "gnosis")
119
+
120
+ assert result is None
121
+
122
+
123
+ def test_get_erc20_balance_wei_account_not_found(
124
+ balance_service, mock_chain_interfaces, mock_account_service
125
+ ):
126
+ """Test get_erc20_balance_wei returns None when account not found."""
127
+ mock_account_service.resolve_account.return_value = None
128
+
129
+ result = balance_service.get_erc20_balance_wei("unknown_tag", "DAI", "gnosis")
130
+
131
+ assert result is None
132
+
133
+
134
+ def test_get_erc20_balance_with_retry_success(
135
+ balance_service, mock_chain_interfaces, mock_account_service
136
+ ):
137
+ """Test get_erc20_balance_with_retry succeeds on first try."""
138
+ with patch("iwa.core.services.balance.ERC20Contract") as mock_erc20:
139
+ mock_erc20.return_value.balance_of_eth.return_value = 50.0
140
+
141
+ result = balance_service.get_erc20_balance_with_retry("0xAccount", "DAI", "gnosis")
142
+
143
+ assert result == 50.0
144
+
145
+
146
+ def test_get_erc20_balance_with_retry_fails_then_succeeds(
147
+ balance_service, mock_chain_interfaces, mock_account_service
148
+ ):
149
+ """Test get_erc20_balance_with_retry retries on failure."""
150
+ with patch("iwa.core.services.balance.ERC20Contract") as mock_erc20, patch("time.sleep"):
151
+ mock_erc20.return_value.balance_of_eth.side_effect = [
152
+ Exception("Network error"),
153
+ 25.0,
154
+ ]
155
+
156
+ result = balance_service.get_erc20_balance_with_retry(
157
+ "0xAccount", "DAI", "gnosis", retries=3
158
+ )
159
+
160
+ assert result == 25.0
161
+
162
+
163
+ def test_get_erc20_balance_with_retry_all_attempts_fail(
164
+ balance_service, mock_chain_interfaces, mock_account_service
165
+ ):
166
+ """Test get_erc20_balance_with_retry returns None after all retries fail."""
167
+ with patch("iwa.core.services.balance.ERC20Contract") as mock_erc20, patch("time.sleep"):
168
+ mock_erc20.return_value.balance_of_eth.side_effect = Exception("Network error")
169
+
170
+ result = balance_service.get_erc20_balance_with_retry(
171
+ "0xAccount", "DAI", "gnosis", retries=3
172
+ )
173
+
174
+ assert result is None
175
+
176
+
177
+ def test_balance_service_with_wallet(mock_account_service):
178
+ """Test BalanceService initialization with Wallet (has key_storage attr)."""
179
+ from iwa.core.services.balance import BalanceService
180
+
181
+ mock_wallet = MagicMock()
182
+ mock_wallet.key_storage = MagicMock()
183
+
184
+ service = BalanceService(mock_wallet, mock_account_service)
185
+
186
+ assert service.key_storage == mock_wallet.key_storage