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.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {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
|