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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Tests for RPC rate limiting."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from iwa.core.chain import RPCRateLimiter, get_rate_limiter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestRPCRateLimiter:
|
|
11
|
+
"""Test cases for RPCRateLimiter."""
|
|
12
|
+
|
|
13
|
+
def test_init_defaults(self):
|
|
14
|
+
"""Test default initialization."""
|
|
15
|
+
limiter = RPCRateLimiter()
|
|
16
|
+
assert limiter.rate == RPCRateLimiter.DEFAULT_RATE
|
|
17
|
+
assert limiter.burst == RPCRateLimiter.DEFAULT_BURST
|
|
18
|
+
assert limiter.tokens == float(limiter.burst)
|
|
19
|
+
|
|
20
|
+
def test_init_custom_values(self):
|
|
21
|
+
"""Test custom initialization."""
|
|
22
|
+
limiter = RPCRateLimiter(rate=10.0, burst=20)
|
|
23
|
+
assert limiter.rate == 10.0
|
|
24
|
+
assert limiter.burst == 20
|
|
25
|
+
|
|
26
|
+
def test_acquire_success(self):
|
|
27
|
+
"""Test successful token acquisition."""
|
|
28
|
+
limiter = RPCRateLimiter(rate=100.0, burst=10)
|
|
29
|
+
assert limiter.acquire(timeout=1.0) is True
|
|
30
|
+
assert limiter.tokens == 9.0
|
|
31
|
+
|
|
32
|
+
def test_acquire_multiple(self):
|
|
33
|
+
"""Test acquiring multiple tokens."""
|
|
34
|
+
limiter = RPCRateLimiter(rate=100.0, burst=10)
|
|
35
|
+
for _ in range(5):
|
|
36
|
+
assert limiter.acquire(timeout=1.0) is True
|
|
37
|
+
# Allow small floating point variance due to time passage
|
|
38
|
+
assert 4.9 <= limiter.tokens <= 5.1
|
|
39
|
+
|
|
40
|
+
def test_acquire_exhausted_waits(self):
|
|
41
|
+
"""Test that acquire waits when tokens exhausted."""
|
|
42
|
+
limiter = RPCRateLimiter(rate=100.0, burst=2)
|
|
43
|
+
# Exhaust tokens
|
|
44
|
+
limiter.tokens = 0.0
|
|
45
|
+
limiter.last_update = time.monotonic()
|
|
46
|
+
|
|
47
|
+
start = time.monotonic()
|
|
48
|
+
result = limiter.acquire(timeout=1.0)
|
|
49
|
+
elapsed = time.monotonic() - start
|
|
50
|
+
|
|
51
|
+
assert result is True
|
|
52
|
+
# Should have waited a bit for token refill
|
|
53
|
+
assert elapsed >= 0.005 # At least some wait
|
|
54
|
+
|
|
55
|
+
def test_acquire_timeout(self):
|
|
56
|
+
"""Test that acquire times out."""
|
|
57
|
+
limiter = RPCRateLimiter(rate=0.1, burst=1) # Very slow refill
|
|
58
|
+
limiter.tokens = 0.0
|
|
59
|
+
limiter.last_update = time.monotonic()
|
|
60
|
+
|
|
61
|
+
result = limiter.acquire(timeout=0.01)
|
|
62
|
+
assert result is False
|
|
63
|
+
|
|
64
|
+
def test_trigger_backoff(self):
|
|
65
|
+
"""Test backoff triggering."""
|
|
66
|
+
limiter = RPCRateLimiter()
|
|
67
|
+
_ = limiter.tokens # Check tokens exist
|
|
68
|
+
|
|
69
|
+
limiter.trigger_backoff(seconds=1.0)
|
|
70
|
+
|
|
71
|
+
assert limiter.tokens == 0
|
|
72
|
+
status = limiter.get_status()
|
|
73
|
+
assert status["in_backoff"] is True
|
|
74
|
+
assert status["backoff_remaining"] > 0
|
|
75
|
+
|
|
76
|
+
def test_backoff_blocks_acquire(self):
|
|
77
|
+
"""Test that backoff blocks acquire."""
|
|
78
|
+
limiter = RPCRateLimiter(rate=100.0, burst=50)
|
|
79
|
+
limiter.trigger_backoff(seconds=10.0)
|
|
80
|
+
|
|
81
|
+
# Should timeout waiting for backoff
|
|
82
|
+
result = limiter.acquire(timeout=0.01)
|
|
83
|
+
assert result is False
|
|
84
|
+
|
|
85
|
+
def test_get_status(self):
|
|
86
|
+
"""Test status reporting."""
|
|
87
|
+
limiter = RPCRateLimiter(rate=25.0, burst=50)
|
|
88
|
+
status = limiter.get_status()
|
|
89
|
+
|
|
90
|
+
assert "tokens" in status
|
|
91
|
+
assert "rate" in status
|
|
92
|
+
assert "burst" in status
|
|
93
|
+
assert "in_backoff" in status
|
|
94
|
+
assert status["rate"] == 25.0
|
|
95
|
+
assert status["burst"] == 50
|
|
96
|
+
|
|
97
|
+
def test_thread_safety(self):
|
|
98
|
+
"""Test that rate limiter is thread-safe."""
|
|
99
|
+
limiter = RPCRateLimiter(rate=1000.0, burst=100)
|
|
100
|
+
acquired = []
|
|
101
|
+
|
|
102
|
+
def acquire_tokens():
|
|
103
|
+
for _ in range(10):
|
|
104
|
+
if limiter.acquire(timeout=1.0):
|
|
105
|
+
acquired.append(1)
|
|
106
|
+
|
|
107
|
+
threads = [threading.Thread(target=acquire_tokens) for _ in range(5)]
|
|
108
|
+
for t in threads:
|
|
109
|
+
t.start()
|
|
110
|
+
for t in threads:
|
|
111
|
+
t.join()
|
|
112
|
+
|
|
113
|
+
# All should have acquired
|
|
114
|
+
assert len(acquired) == 50
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TestGetRateLimiter:
|
|
118
|
+
"""Test get_rate_limiter function."""
|
|
119
|
+
|
|
120
|
+
def test_creates_new_limiter(self):
|
|
121
|
+
"""Test creating a new rate limiter."""
|
|
122
|
+
# Use unique chain name to avoid interference
|
|
123
|
+
limiter = get_rate_limiter("test_chain_unique_1")
|
|
124
|
+
assert isinstance(limiter, RPCRateLimiter)
|
|
125
|
+
|
|
126
|
+
def test_returns_same_limiter(self):
|
|
127
|
+
"""Test that same chain returns same limiter."""
|
|
128
|
+
limiter1 = get_rate_limiter("test_chain_unique_2")
|
|
129
|
+
limiter2 = get_rate_limiter("test_chain_unique_2")
|
|
130
|
+
assert limiter1 is limiter2
|
|
131
|
+
|
|
132
|
+
def test_different_chains_different_limiters(self):
|
|
133
|
+
"""Test that different chains get different limiters."""
|
|
134
|
+
limiter1 = get_rate_limiter("chain_a")
|
|
135
|
+
limiter2 = get_rate_limiter("chain_b")
|
|
136
|
+
assert limiter1 is not limiter2
|
|
137
|
+
|
|
138
|
+
def test_custom_rate(self):
|
|
139
|
+
"""Test creating limiter with custom rate."""
|
|
140
|
+
limiter = get_rate_limiter("test_chain_unique_3", rate=50.0, burst=100)
|
|
141
|
+
assert limiter.rate == 50.0
|
|
142
|
+
assert limiter.burst == 100
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestRateLimitRotationInterplay:
|
|
146
|
+
"""Test interaction between rate limiting and RPC rotation."""
|
|
147
|
+
|
|
148
|
+
def test_rate_limit_triggers_rotation_first(self):
|
|
149
|
+
"""Test that rate limit error triggers RPC rotation before backoff."""
|
|
150
|
+
from unittest.mock import MagicMock, PropertyMock
|
|
151
|
+
|
|
152
|
+
from iwa.core.chain import ChainInterface, SupportedChain
|
|
153
|
+
|
|
154
|
+
# Create a mock chain with multiple RPCs
|
|
155
|
+
with patch("iwa.core.chain.interface.Web3"):
|
|
156
|
+
chain = MagicMock(spec=SupportedChain)
|
|
157
|
+
chain.name = "TestChain"
|
|
158
|
+
chain.rpcs = ["https://rpc1", "https://rpc2", "https://rpc3"]
|
|
159
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc1")
|
|
160
|
+
|
|
161
|
+
ci = ChainInterface(chain)
|
|
162
|
+
original_index = ci._current_rpc_index
|
|
163
|
+
|
|
164
|
+
# Mock health check to pass
|
|
165
|
+
with patch.object(ci, "check_rpc_health", return_value=True):
|
|
166
|
+
# Simulate rate limit error
|
|
167
|
+
rate_limit_error = Exception("Error 429: Too Many Requests")
|
|
168
|
+
result = ci._handle_rpc_error(rate_limit_error)
|
|
169
|
+
|
|
170
|
+
# Should have rotated
|
|
171
|
+
assert result["rotated"] is True
|
|
172
|
+
assert result["should_retry"] is True
|
|
173
|
+
assert ci._current_rpc_index != original_index
|
|
174
|
+
# Backoff should NOT be triggered since rotation succeeded
|
|
175
|
+
assert ci._rate_limiter.get_status()["in_backoff"] is False
|
|
176
|
+
|
|
177
|
+
def test_rate_limit_triggers_backoff_when_no_rotation(self):
|
|
178
|
+
"""Test that rate limit triggers backoff when no other RPCs available."""
|
|
179
|
+
from unittest.mock import MagicMock, PropertyMock
|
|
180
|
+
|
|
181
|
+
from iwa.core.chain import ChainInterface, SupportedChain
|
|
182
|
+
|
|
183
|
+
# Create a mock chain with single RPC (can't rotate)
|
|
184
|
+
with patch("iwa.core.chain.interface.Web3"):
|
|
185
|
+
chain = MagicMock(spec=SupportedChain)
|
|
186
|
+
chain.name = "TestChainSingle"
|
|
187
|
+
chain.rpcs = ["https://rpc1"] # Only one RPC
|
|
188
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc1")
|
|
189
|
+
|
|
190
|
+
ci = ChainInterface(chain)
|
|
191
|
+
|
|
192
|
+
# Simulate rate limit error
|
|
193
|
+
rate_limit_error = Exception("Error 429: Too Many Requests")
|
|
194
|
+
result = ci._handle_rpc_error(rate_limit_error)
|
|
195
|
+
|
|
196
|
+
# Should have triggered backoff since can't rotate
|
|
197
|
+
assert result["should_retry"] is True
|
|
198
|
+
assert result["rotated"] is False
|
|
199
|
+
assert ci._rate_limiter.get_status()["in_backoff"] is True
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from unittest.mock import MagicMock, mock_open, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from iwa.core.models import FundRequirements, TenderlyConfig, TokenAmount, VirtualNet
|
|
7
|
+
from iwa.tools.reset_tenderly import (
|
|
8
|
+
_create_vnet,
|
|
9
|
+
_delete_vnet,
|
|
10
|
+
_fund_wallet,
|
|
11
|
+
_generate_vnet_slug,
|
|
12
|
+
main,
|
|
13
|
+
update_rpc_variables,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_requests():
|
|
19
|
+
with patch("iwa.tools.reset_tenderly.requests") as mock:
|
|
20
|
+
import requests
|
|
21
|
+
|
|
22
|
+
mock.exceptions.JSONDecodeError = requests.exceptions.JSONDecodeError
|
|
23
|
+
yield mock
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def mock_tenderly_config():
|
|
28
|
+
config = MagicMock(spec=TenderlyConfig)
|
|
29
|
+
vnet = MagicMock(spec=VirtualNet)
|
|
30
|
+
vnet.vnet_id = "old_id"
|
|
31
|
+
vnet.chain_id = 1
|
|
32
|
+
vnet.public_rpc = "https://rpc.com"
|
|
33
|
+
vnet.admin_rpc = "https://admin.rpc.com"
|
|
34
|
+
vnet.funds_requirements = {
|
|
35
|
+
"tag1": FundRequirements(
|
|
36
|
+
native_eth=1.0,
|
|
37
|
+
tokens=[
|
|
38
|
+
TokenAmount(
|
|
39
|
+
address="0x1234567890123456789012345678901234567890",
|
|
40
|
+
amount_eth=10.0,
|
|
41
|
+
symbol="TKN",
|
|
42
|
+
)
|
|
43
|
+
],
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
config.vnets = {"Gnosis": vnet}
|
|
47
|
+
return config
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_delete_vnet(mock_requests):
|
|
51
|
+
_delete_vnet("key", "account", "project", "vnet_id")
|
|
52
|
+
mock_requests.delete.assert_called_once()
|
|
53
|
+
args, kwargs = mock_requests.delete.call_args
|
|
54
|
+
assert "vnet_id" in kwargs["url"]
|
|
55
|
+
assert kwargs["headers"]["X-Access-Key"] == "key"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_create_vnet(mock_requests):
|
|
59
|
+
mock_response = MagicMock()
|
|
60
|
+
mock_response.json.return_value = {
|
|
61
|
+
"id": "new_id",
|
|
62
|
+
"rpcs": [
|
|
63
|
+
{"name": "Admin RPC", "url": "admin_url"},
|
|
64
|
+
{"name": "Public RPC", "url": "public_url"},
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
mock_requests.post.return_value = mock_response
|
|
68
|
+
|
|
69
|
+
vnet_id, admin_rpc, public_rpc = _create_vnet("key", "account", "project", 1, 1, "slug", "name")
|
|
70
|
+
|
|
71
|
+
assert vnet_id == "new_id"
|
|
72
|
+
assert admin_rpc == "admin_url"
|
|
73
|
+
assert public_rpc == "public_url"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_generate_vnet_slug():
|
|
77
|
+
slug = _generate_vnet_slug("prefix", 5)
|
|
78
|
+
assert slug.startswith("prefix-")
|
|
79
|
+
assert len(slug) == len("prefix-") + 5
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_update_rpc_variables(mock_tenderly_config):
|
|
83
|
+
mock_file_content = "gnosis_test_rpc=old_url\nother_var=1"
|
|
84
|
+
with patch("builtins.open", mock_open(read_data=mock_file_content)) as mock_file:
|
|
85
|
+
update_rpc_variables(mock_tenderly_config)
|
|
86
|
+
|
|
87
|
+
mock_file().write.assert_called_once()
|
|
88
|
+
written_content = mock_file().write.call_args[0][0]
|
|
89
|
+
assert "gnosis_test_rpc=https://rpc.com" in written_content
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_update_rpc_variables_new(mock_tenderly_config):
|
|
93
|
+
mock_file_content = "other_var=1"
|
|
94
|
+
with patch("builtins.open", mock_open(read_data=mock_file_content)) as mock_file:
|
|
95
|
+
update_rpc_variables(mock_tenderly_config)
|
|
96
|
+
|
|
97
|
+
mock_file().write.assert_called_once()
|
|
98
|
+
written_content = mock_file().write.call_args[0][0]
|
|
99
|
+
assert "gnosis_test_rpc=https://rpc.com" in written_content
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_fund_wallet_native(mock_requests):
|
|
103
|
+
mock_requests.post.return_value.status_code = 200
|
|
104
|
+
_fund_wallet("admin_url", ["0x1"], 1.0, "native")
|
|
105
|
+
|
|
106
|
+
mock_requests.post.assert_called_once()
|
|
107
|
+
kwargs = mock_requests.post.call_args[1]
|
|
108
|
+
assert kwargs["json"]["method"] == "tenderly_setBalance"
|
|
109
|
+
assert kwargs["json"]["params"][1] == hex(int(1e18))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_fund_wallet_token(mock_requests):
|
|
113
|
+
mock_requests.post.return_value.status_code = 200
|
|
114
|
+
_fund_wallet("admin_url", ["0x1"], 1.0, "0xToken")
|
|
115
|
+
|
|
116
|
+
mock_requests.post.assert_called_once()
|
|
117
|
+
kwargs = mock_requests.post.call_args[1]
|
|
118
|
+
assert kwargs["json"]["method"] == "tenderly_setErc20Balance"
|
|
119
|
+
assert kwargs["json"]["params"][0] == "0xToken"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_fund_wallet_error(mock_requests, capsys):
|
|
123
|
+
mock_requests.post.return_value.status_code = 500
|
|
124
|
+
mock_requests.post.return_value.json.return_value = {"error": "fail"}
|
|
125
|
+
|
|
126
|
+
_fund_wallet("admin_url", ["0x1"], 1.0)
|
|
127
|
+
|
|
128
|
+
captured = capsys.readouterr()
|
|
129
|
+
assert "500" in captured.out
|
|
130
|
+
assert "{'error': 'fail'}" in captured.out
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_fund_wallet_json_error(mock_requests, capsys):
|
|
134
|
+
import requests
|
|
135
|
+
|
|
136
|
+
mock_requests.post.return_value.status_code = 500
|
|
137
|
+
mock_requests.post.return_value.json.side_effect = requests.exceptions.JSONDecodeError(
|
|
138
|
+
"msg", "doc", 0
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
_fund_wallet("admin_url", ["0x1"], 1.0)
|
|
142
|
+
|
|
143
|
+
captured = capsys.readouterr()
|
|
144
|
+
assert "500" in captured.out
|
|
145
|
+
# Should not print json error
|
|
146
|
+
assert "msg" not in captured.out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_main(mock_requests, mock_tenderly_config):
|
|
150
|
+
# Mock env vars
|
|
151
|
+
with patch.dict(
|
|
152
|
+
os.environ,
|
|
153
|
+
{
|
|
154
|
+
"tenderly_account_slug": "acc",
|
|
155
|
+
"tenderly_project_slug": "proj",
|
|
156
|
+
"tenderly_access_key": "key",
|
|
157
|
+
},
|
|
158
|
+
):
|
|
159
|
+
# Mock TenderlyConfig.load
|
|
160
|
+
with patch("iwa.core.models.TenderlyConfig.load", return_value=mock_tenderly_config):
|
|
161
|
+
with patch(
|
|
162
|
+
"iwa.tools.reset_tenderly.get_tenderly_credentials",
|
|
163
|
+
return_value=("acc", "proj", "key"),
|
|
164
|
+
):
|
|
165
|
+
# Mock KeyStorage
|
|
166
|
+
with patch("iwa.tools.reset_tenderly.KeyStorage") as mock_key_storage:
|
|
167
|
+
mock_keys = mock_key_storage.return_value
|
|
168
|
+
mock_keys.get_account.return_value.address = "0xAddress"
|
|
169
|
+
mock_keys.accounts.keys.return_value = ["tag1"]
|
|
170
|
+
|
|
171
|
+
# Mock _create_vnet return values (since we mock requests, _create_vnet logic runs, but we can also mock _create_vnet directly)
|
|
172
|
+
# But let's let it run with mocked requests
|
|
173
|
+
mock_response_create = MagicMock()
|
|
174
|
+
mock_response_create.json.return_value = {
|
|
175
|
+
"id": "new_id",
|
|
176
|
+
"rpcs": [
|
|
177
|
+
{"name": "Admin RPC", "url": "admin"},
|
|
178
|
+
{"name": "Public RPC", "url": "public"},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Mock requests.post for create and fund
|
|
183
|
+
# We need side_effect to handle different calls
|
|
184
|
+
def post_side_effect(*args, **kwargs):
|
|
185
|
+
if "vnets" in kwargs.get("url", ""):
|
|
186
|
+
return mock_response_create
|
|
187
|
+
return MagicMock(status_code=200)
|
|
188
|
+
|
|
189
|
+
mock_requests.post.side_effect = post_side_effect
|
|
190
|
+
|
|
191
|
+
# Mock update_rpc_variables to avoid file I/O
|
|
192
|
+
with patch("iwa.tools.reset_tenderly.update_rpc_variables") as mock_update:
|
|
193
|
+
# Mock SafeService
|
|
194
|
+
with patch("iwa.core.services.SafeService") as mock_safe_service_cls:
|
|
195
|
+
main()
|
|
196
|
+
|
|
197
|
+
# Verify interactions
|
|
198
|
+
mock_requests.delete.assert_called() # _delete_vnet
|
|
199
|
+
mock_tenderly_config.save.assert_called()
|
|
200
|
+
mock_update.assert_called()
|
|
201
|
+
# mock_keys.redeploy_safes.assert_called() # Removed
|
|
202
|
+
mock_safe_service_cls.return_value.redeploy_safes.assert_called()
|
tests/test_rpc_view.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Tests for TUI RPC View."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.tui.rpc import RPCView
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_chain_interfaces():
|
|
12
|
+
with patch("iwa.tui.rpc.ChainInterfaces") as mock:
|
|
13
|
+
yield mock
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_rpc_view_compose():
|
|
17
|
+
"""Test RPCView compose method returns expected widgets."""
|
|
18
|
+
view = RPCView()
|
|
19
|
+
result = list(view.compose())
|
|
20
|
+
|
|
21
|
+
assert len(result) == 2 # Label and DataTable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_check_rpcs_interface_not_found(mock_chain_interfaces):
|
|
25
|
+
"""Test check_rpcs handles missing interface."""
|
|
26
|
+
mock_chain_interfaces.return_value.get.return_value = None
|
|
27
|
+
|
|
28
|
+
view = RPCView()
|
|
29
|
+
|
|
30
|
+
# Verify the method exists and is callable
|
|
31
|
+
assert hasattr(view, "check_rpcs")
|
|
32
|
+
assert callable(view.check_rpcs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_check_rpcs_no_rpc_url(mock_chain_interfaces):
|
|
36
|
+
"""Test check_rpcs handles missing RPC URL."""
|
|
37
|
+
mock_interface = MagicMock()
|
|
38
|
+
mock_interface.chain.rpc = ""
|
|
39
|
+
mock_chain_interfaces.return_value.get.return_value = mock_interface
|
|
40
|
+
|
|
41
|
+
view = RPCView()
|
|
42
|
+
|
|
43
|
+
# Verify method exists and has correct signature
|
|
44
|
+
assert callable(view.check_rpcs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_check_rpcs_connection_error(mock_chain_interfaces):
|
|
48
|
+
"""Test check_rpcs handles connection errors."""
|
|
49
|
+
mock_interface = MagicMock()
|
|
50
|
+
mock_interface.chain.rpc = "http://localhost:8545"
|
|
51
|
+
mock_interface.web3.is_connected.side_effect = Exception("Connection refused")
|
|
52
|
+
mock_chain_interfaces.return_value.get.return_value = mock_interface
|
|
53
|
+
|
|
54
|
+
view = RPCView()
|
|
55
|
+
|
|
56
|
+
assert callable(view.check_rpcs)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_update_table():
|
|
60
|
+
"""Test update_table method updates DataTable."""
|
|
61
|
+
view = RPCView()
|
|
62
|
+
|
|
63
|
+
mock_table = MagicMock()
|
|
64
|
+
with patch.object(view, "query_one", return_value=mock_table):
|
|
65
|
+
results = [
|
|
66
|
+
("gnosis", "http://rpc1", "Online", "50.00"),
|
|
67
|
+
("ethereum", "http://rpc2", "Offline", "-"),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
view.update_table(results)
|
|
71
|
+
|
|
72
|
+
mock_table.clear.assert_called_once()
|
|
73
|
+
assert mock_table.add_row.call_count == 2
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Tests for SafeService coverage."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.models import StoredSafeAccount
|
|
8
|
+
from iwa.core.services.safe import SafeService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_deps():
|
|
13
|
+
"""Mock dependencies for SafeService."""
|
|
14
|
+
mock_key_storage = MagicMock()
|
|
15
|
+
mock_account_service = MagicMock()
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
"key_storage": mock_key_storage,
|
|
19
|
+
"account_service": mock_account_service,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def safe_service(mock_deps):
|
|
25
|
+
"""SafeService instance."""
|
|
26
|
+
return SafeService(mock_deps["key_storage"], mock_deps["account_service"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_execute_safe_transaction_success(safe_service, mock_deps):
|
|
30
|
+
"""Test execute_safe_transaction success."""
|
|
31
|
+
# Mock inputs
|
|
32
|
+
safe_address = "0xSafe"
|
|
33
|
+
to_address = "0xTo"
|
|
34
|
+
value = 1000
|
|
35
|
+
chain_name = "gnosis"
|
|
36
|
+
|
|
37
|
+
# Mock Safe Account
|
|
38
|
+
mock_account = MagicMock(spec=StoredSafeAccount)
|
|
39
|
+
mock_account.address = safe_address
|
|
40
|
+
mock_account.signers = ["0xSigner1", "0xSigner2"]
|
|
41
|
+
mock_account.threshold = 1
|
|
42
|
+
mock_deps["key_storage"].find_stored_account.return_value = mock_account
|
|
43
|
+
|
|
44
|
+
# Mock Private Keys
|
|
45
|
+
mock_deps["key_storage"]._get_private_key.return_value = "0xPrivKey"
|
|
46
|
+
|
|
47
|
+
# Mock SafeMultisig via patch
|
|
48
|
+
# The import is inside the method: from iwa.plugins.gnosis.safe import SafeMultisig
|
|
49
|
+
# We need to patch where it is IMPORTED from
|
|
50
|
+
with patch("iwa.plugins.gnosis.safe.SafeMultisig") as mock_safe_multisig_cls:
|
|
51
|
+
# But wait, execute_safe_transaction does local import.
|
|
52
|
+
# patch('iwa.core.services.safe.SafeMultisig') won't work if it's not global.
|
|
53
|
+
# We must patch 'iwa.plugins.gnosis.safe.SafeMultisig'.
|
|
54
|
+
# And since it's imported INSIDE the function, patching the source module works.
|
|
55
|
+
|
|
56
|
+
mock_safe_instance = mock_safe_multisig_cls.return_value
|
|
57
|
+
mock_safe_tx = MagicMock()
|
|
58
|
+
mock_safe_instance.build_tx.return_value = mock_safe_tx
|
|
59
|
+
mock_safe_tx.tx_hash.hex.return_value = "0xTxHash"
|
|
60
|
+
|
|
61
|
+
# Execute
|
|
62
|
+
tx_hash = safe_service.execute_safe_transaction(safe_address, to_address, value, chain_name)
|
|
63
|
+
|
|
64
|
+
# Verify
|
|
65
|
+
assert tx_hash == "0xTxHash"
|
|
66
|
+
mock_safe_tx.sign.assert_called()
|
|
67
|
+
mock_safe_tx.execute.assert_called()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_execute_safe_transaction_account_not_found(safe_service, mock_deps):
|
|
71
|
+
"""Test execute_safe_transaction fails if account not found."""
|
|
72
|
+
mock_deps["key_storage"].find_stored_account.return_value = None
|
|
73
|
+
|
|
74
|
+
with pytest.raises(ValueError, match="Safe account '0xSafe' not found"):
|
|
75
|
+
safe_service.execute_safe_transaction("0xSafe", "0xTo", 0, "gnosis")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_get_sign_and_execute_callback(safe_service, mock_deps):
|
|
79
|
+
"""Test get_sign_and_execute_callback returns working callback."""
|
|
80
|
+
safe_address = "0xSafe"
|
|
81
|
+
mock_account = MagicMock(spec=StoredSafeAccount)
|
|
82
|
+
mock_account.address = safe_address
|
|
83
|
+
mock_account.signers = ["0xSigner1"]
|
|
84
|
+
mock_account.threshold = 1
|
|
85
|
+
mock_deps["key_storage"].find_stored_account.return_value = mock_account
|
|
86
|
+
mock_deps["key_storage"]._get_private_key.return_value = "0xPrivKey"
|
|
87
|
+
|
|
88
|
+
callback = safe_service.get_sign_and_execute_callback(safe_address)
|
|
89
|
+
assert callable(callback)
|
|
90
|
+
|
|
91
|
+
# Test executing callback
|
|
92
|
+
mock_safe_tx = MagicMock()
|
|
93
|
+
mock_safe_tx.tx_hash.hex.return_value = "0xTxHash"
|
|
94
|
+
|
|
95
|
+
result = callback(mock_safe_tx)
|
|
96
|
+
|
|
97
|
+
assert result == "0xTxHash"
|
|
98
|
+
mock_safe_tx.sign.assert_called()
|
|
99
|
+
mock_safe_tx.execute.assert_called()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_get_sign_and_execute_callback_fail(safe_service, mock_deps):
|
|
103
|
+
"""Test callback generation fails if account missing."""
|
|
104
|
+
mock_deps["key_storage"].find_stored_account.return_value = None
|
|
105
|
+
with pytest.raises(ValueError):
|
|
106
|
+
safe_service.get_sign_and_execute_callback("0xSafe")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_redeploy_safes(safe_service, mock_deps):
|
|
110
|
+
"""Test redeploy_safes logic."""
|
|
111
|
+
# Mock accounts
|
|
112
|
+
account1 = MagicMock(spec=StoredSafeAccount)
|
|
113
|
+
account1.address = "0xSafe1"
|
|
114
|
+
account1.chains = ["gnosis"]
|
|
115
|
+
account1.signers = ["0xSigner"]
|
|
116
|
+
account1.threshold = 1
|
|
117
|
+
# account1.tag needs to be accessible
|
|
118
|
+
account1.tag = " Safe1"
|
|
119
|
+
|
|
120
|
+
mock_deps["key_storage"].accounts = {"0xSafe1": account1}
|
|
121
|
+
|
|
122
|
+
with patch("iwa.core.services.safe.settings") as mock_settings:
|
|
123
|
+
mock_settings.gnosis_rpc.get_secret_value.return_value = "http://rpc"
|
|
124
|
+
|
|
125
|
+
with patch("iwa.core.services.safe.EthereumClient") as mock_eth_client:
|
|
126
|
+
with patch.object(safe_service, "create_safe") as mock_create:
|
|
127
|
+
mock_w3 = mock_eth_client.return_value.w3
|
|
128
|
+
|
|
129
|
+
# Case 1: Code exists (no redeploy)
|
|
130
|
+
mock_w3.eth.get_code.return_value = b"code"
|
|
131
|
+
safe_service.redeploy_safes()
|
|
132
|
+
mock_create.assert_not_called()
|
|
133
|
+
|
|
134
|
+
# Case 2: No code (redeploy)
|
|
135
|
+
mock_w3.eth.get_code.return_value = b""
|
|
136
|
+
# Need to mock remove_account
|
|
137
|
+
safe_service.redeploy_safes()
|
|
138
|
+
mock_deps["key_storage"].remove_account.assert_called_with("0xSafe1")
|
|
139
|
+
mock_create.assert_called()
|