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/test_chain.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, PropertyMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from iwa.core.chain import (
|
|
6
|
+
Base,
|
|
7
|
+
ChainInterface,
|
|
8
|
+
ChainInterfaces,
|
|
9
|
+
Ethereum,
|
|
10
|
+
Gnosis,
|
|
11
|
+
SupportedChain,
|
|
12
|
+
)
|
|
13
|
+
from iwa.core.models import EthereumAddress
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_web3():
|
|
18
|
+
"""Mock Web3 and RateLimitedWeb3 to bypass rate limiting wrapper in tests."""
|
|
19
|
+
with (
|
|
20
|
+
patch("iwa.core.chain.interface.Web3") as mock_web3_class,
|
|
21
|
+
patch("iwa.core.chain.interface.RateLimitedWeb3") as mock_rl_web3,
|
|
22
|
+
):
|
|
23
|
+
# Make RateLimitedWeb3 just return the raw web3 instance passed to it
|
|
24
|
+
mock_rl_web3.side_effect = lambda w3, rl, ci: w3
|
|
25
|
+
yield mock_web3_class
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def mock_secrets():
|
|
30
|
+
with patch("iwa.core.chain.models.settings") as mock:
|
|
31
|
+
yield mock
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_supported_chain_get_token_address():
|
|
35
|
+
chain = SupportedChain(
|
|
36
|
+
name="Test",
|
|
37
|
+
rpcs=["http://rpc"],
|
|
38
|
+
chain_id=1,
|
|
39
|
+
native_currency="TEST",
|
|
40
|
+
tokens={"TKN": EthereumAddress("0x1234567890123456789012345678901234567890")},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Test getting by name
|
|
44
|
+
assert chain.get_token_address("TKN") == "0x1234567890123456789012345678901234567890"
|
|
45
|
+
|
|
46
|
+
# Test getting by address
|
|
47
|
+
assert (
|
|
48
|
+
chain.get_token_address("0x1234567890123456789012345678901234567890")
|
|
49
|
+
== "0x1234567890123456789012345678901234567890"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Test invalid
|
|
53
|
+
assert chain.get_token_address("INVALID") is None
|
|
54
|
+
assert chain.get_token_address("0xInvalid") is None
|
|
55
|
+
|
|
56
|
+
# Test valid address NOT in tokens
|
|
57
|
+
valid_addr_not_in_tokens = "0x0000000000000000000000000000000000000001"
|
|
58
|
+
assert chain.get_token_address(valid_addr_not_in_tokens) is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_chain_classes(mock_secrets):
|
|
62
|
+
mock_secrets.gnosis_rpc.get_secret_value.return_value = "https://gnosis"
|
|
63
|
+
mock_secrets.ethereum_rpc.get_secret_value.return_value = "https://eth"
|
|
64
|
+
mock_secrets.base_rpc.get_secret_value.return_value = "https://base"
|
|
65
|
+
|
|
66
|
+
# Reset singletons
|
|
67
|
+
Gnosis._instance = None
|
|
68
|
+
Ethereum._instance = None
|
|
69
|
+
Base._instance = None
|
|
70
|
+
|
|
71
|
+
assert Gnosis().name == "Gnosis"
|
|
72
|
+
assert Ethereum().name == "Ethereum"
|
|
73
|
+
assert Base().name == "Base"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_chain_interface_init(mock_web3, mock_secrets):
|
|
77
|
+
mock_secrets.gnosis_rpc.get_secret_value.return_value = "https://gnosis"
|
|
78
|
+
Gnosis._instance = None
|
|
79
|
+
|
|
80
|
+
ci = ChainInterface()
|
|
81
|
+
assert ci.chain.name == "Gnosis"
|
|
82
|
+
mock_web3.assert_called()
|
|
83
|
+
|
|
84
|
+
ci_eth = ChainInterface("ethereum")
|
|
85
|
+
assert ci_eth.chain.name == "Ethereum"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_chain_interface_insecure_rpc_warning(mock_web3, caplog):
|
|
89
|
+
chain = MagicMock(spec=SupportedChain)
|
|
90
|
+
chain.name = "TestChain"
|
|
91
|
+
chain.name = "Insecure"
|
|
92
|
+
chain.rpcs = ["http://insecure"]
|
|
93
|
+
|
|
94
|
+
# Needs to return property value for rpc
|
|
95
|
+
type(chain).rpc = PropertyMock(return_value="http://insecure")
|
|
96
|
+
|
|
97
|
+
ChainInterface(chain)
|
|
98
|
+
assert "Using insecure RPC URL" in caplog.text
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_is_contract(mock_web3):
|
|
102
|
+
chain = MagicMock(spec=SupportedChain)
|
|
103
|
+
chain.name = "TestChain"
|
|
104
|
+
chain.rpcs = ["https://rpc"]
|
|
105
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
106
|
+
ci = ChainInterface(chain)
|
|
107
|
+
ci.web3.eth.get_code.return_value = b"code"
|
|
108
|
+
assert ci.is_contract("0xAddress") is True
|
|
109
|
+
|
|
110
|
+
ci.web3.eth.get_code.return_value = b""
|
|
111
|
+
assert ci.is_contract("0xAddress") is False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_get_native_balance(mock_web3):
|
|
115
|
+
chain = MagicMock(spec=SupportedChain)
|
|
116
|
+
chain.name = "TestChain"
|
|
117
|
+
chain.rpcs = ["https://rpc"]
|
|
118
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
119
|
+
ci = ChainInterface(chain)
|
|
120
|
+
ci.web3.eth.get_balance.return_value = 10**18
|
|
121
|
+
ci.web3.from_wei.return_value = 1.0
|
|
122
|
+
|
|
123
|
+
assert ci.get_native_balance_wei("0xAddress") == 10**18
|
|
124
|
+
assert ci.get_native_balance_eth("0xAddress") == 1.0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# NOTE: Tests for sign_and_send_transaction were removed because the method was removed
|
|
128
|
+
# from ChainInterface for security reasons. Transaction signing is now handled exclusively
|
|
129
|
+
# through TransactionService.sign_and_send() which uses KeyStorage internally.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_estimate_gas(mock_web3):
|
|
133
|
+
chain = MagicMock(spec=SupportedChain)
|
|
134
|
+
chain.name = "TestChain"
|
|
135
|
+
chain.rpcs = ["https://rpc"]
|
|
136
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
137
|
+
ci = ChainInterface(chain)
|
|
138
|
+
built_method = MagicMock()
|
|
139
|
+
built_method.estimate_gas.return_value = 1000
|
|
140
|
+
|
|
141
|
+
# Not a contract
|
|
142
|
+
ci.web3.eth.get_code.return_value = b""
|
|
143
|
+
assert ci.estimate_gas(built_method, {"from": "0xSender"}) == 1100
|
|
144
|
+
|
|
145
|
+
# Is a contract
|
|
146
|
+
ci.web3.eth.get_code.return_value = b"code"
|
|
147
|
+
assert ci.estimate_gas(built_method, {"from": "0xSender"}) == 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_calculate_transaction_params(mock_web3):
|
|
151
|
+
chain = MagicMock(spec=SupportedChain)
|
|
152
|
+
chain.name = "TestChain"
|
|
153
|
+
chain.rpcs = ["https://rpc"]
|
|
154
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
155
|
+
ci = ChainInterface(chain)
|
|
156
|
+
ci.web3.eth.get_transaction_count.return_value = 5
|
|
157
|
+
ci.web3.eth.gas_price = 20
|
|
158
|
+
|
|
159
|
+
with patch.object(ci, "estimate_gas", return_value=1000):
|
|
160
|
+
params = ci.calculate_transaction_params(MagicMock(), {"from": "0xSender"})
|
|
161
|
+
assert params["nonce"] == 5
|
|
162
|
+
assert params["gas"] == 1000
|
|
163
|
+
assert params["gasPrice"] == 20
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_wait_for_no_pending_tx(mock_web3):
|
|
167
|
+
chain = MagicMock(spec=SupportedChain)
|
|
168
|
+
chain.name = "TestChain"
|
|
169
|
+
chain.rpcs = ["https://rpc"]
|
|
170
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
171
|
+
ci = ChainInterface(chain)
|
|
172
|
+
|
|
173
|
+
# pending == latest
|
|
174
|
+
ci.web3.eth.get_transaction_count.side_effect = [10, 10]
|
|
175
|
+
assert ci.wait_for_no_pending_tx("0xSender") is True
|
|
176
|
+
|
|
177
|
+
# pending != latest then pending == latest
|
|
178
|
+
ci.web3.eth.get_transaction_count.side_effect = [10, 11, 11, 11]
|
|
179
|
+
with patch("time.sleep"):
|
|
180
|
+
assert ci.wait_for_no_pending_tx("0xSender") is True
|
|
181
|
+
|
|
182
|
+
# Timeout
|
|
183
|
+
ci.web3.eth.get_transaction_count.return_value = 10
|
|
184
|
+
|
|
185
|
+
# Mock pending to be always different
|
|
186
|
+
def side_effect(address, block_identifier):
|
|
187
|
+
if block_identifier == "latest":
|
|
188
|
+
return 10
|
|
189
|
+
return 11
|
|
190
|
+
|
|
191
|
+
ci.web3.eth.get_transaction_count.side_effect = side_effect
|
|
192
|
+
|
|
193
|
+
with patch("time.time", side_effect=[0, 1, 61]):
|
|
194
|
+
with patch("time.sleep"):
|
|
195
|
+
assert ci.wait_for_no_pending_tx("0xSender") is False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_send_native_transfer(mock_web3):
|
|
199
|
+
chain = MagicMock(spec=SupportedChain, rpcs=["https://rpc"], chain_id=1, native_currency="ETH")
|
|
200
|
+
chain.name = "TestChain"
|
|
201
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
202
|
+
ci = ChainInterface(chain)
|
|
203
|
+
account = MagicMock(address="0xSender", key="key")
|
|
204
|
+
|
|
205
|
+
ci.web3.eth.get_transaction_count.return_value = 0
|
|
206
|
+
ci.web3.eth.gas_price = 10
|
|
207
|
+
ci.web3.eth.estimate_gas.return_value = 21000
|
|
208
|
+
|
|
209
|
+
# Sufficient balance
|
|
210
|
+
ci.web3.eth.get_balance.return_value = 10**18 # plenty
|
|
211
|
+
ci.web3.eth.get_balance.return_value = 10**18 # plenty
|
|
212
|
+
# Valid mock return for success: (True, dict_receipt)
|
|
213
|
+
# The actual method returns tx_hash.hex().
|
|
214
|
+
mock_signed_tx = MagicMock()
|
|
215
|
+
mock_signed_tx.raw_transaction = b"raw"
|
|
216
|
+
mock_receipt = {"transactionHash": b"hash", "status": 1}
|
|
217
|
+
|
|
218
|
+
with (
|
|
219
|
+
patch.object(ci.web3.eth, "send_raw_transaction", return_value=b"hash"),
|
|
220
|
+
patch.object(ci.web3.eth, "wait_for_transaction_receipt", return_value=mock_receipt),
|
|
221
|
+
patch.object(ci, "wait_for_no_pending_tx", return_value=True),
|
|
222
|
+
):
|
|
223
|
+
success, tx_hash = ci.send_native_transfer(
|
|
224
|
+
account.address, "0xReceiver", 1000, sign_callback=lambda tx: mock_signed_tx
|
|
225
|
+
)
|
|
226
|
+
assert success is True
|
|
227
|
+
assert tx_hash == "68617368"
|
|
228
|
+
|
|
229
|
+
# Insufficient balance
|
|
230
|
+
ci.web3.eth.get_balance.return_value = 0
|
|
231
|
+
ci.web3.from_wei.return_value = 0.0
|
|
232
|
+
assert ci.send_native_transfer(
|
|
233
|
+
account.address, "0xReceiver", 1000, sign_callback=lambda tx: mock_signed_tx
|
|
234
|
+
) == (False, None)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_chain_interfaces_get():
|
|
238
|
+
ChainInterfaces._instance = None
|
|
239
|
+
interfaces = ChainInterfaces()
|
|
240
|
+
assert interfaces.get("gnosis").chain.name == "Gnosis"
|
|
241
|
+
|
|
242
|
+
with pytest.raises(ValueError):
|
|
243
|
+
interfaces.get("invalid")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_chain_interface_get_token_address(mock_web3):
|
|
247
|
+
chain = MagicMock(spec=SupportedChain)
|
|
248
|
+
chain.name = "TestChain"
|
|
249
|
+
chain.rpcs = ["https://rpc"]
|
|
250
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
251
|
+
chain.get_token_address.return_value = "0xToken"
|
|
252
|
+
ci = ChainInterface(chain)
|
|
253
|
+
|
|
254
|
+
assert ci.get_token_address("Token") == "0xToken"
|
|
255
|
+
chain.get_token_address.assert_called_with("Token")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_rotate_rpc(mock_web3):
|
|
259
|
+
chain = MagicMock(spec=SupportedChain)
|
|
260
|
+
chain.name = "TestChain"
|
|
261
|
+
chain.rpcs = ["http://rpc1", "http://rpc2", "http://rpc3"]
|
|
262
|
+
# Needs to return property value for rpc if accessed
|
|
263
|
+
type(chain).rpc = PropertyMock(return_value="http://rpc1")
|
|
264
|
+
|
|
265
|
+
ci = ChainInterface(chain)
|
|
266
|
+
ci._current_rpc_index = 0
|
|
267
|
+
|
|
268
|
+
# Mock health check to always pass
|
|
269
|
+
with patch.object(ci, "check_rpc_health", return_value=True):
|
|
270
|
+
# Rotate 1
|
|
271
|
+
assert ci.rotate_rpc() is True
|
|
272
|
+
assert ci._current_rpc_index == 1
|
|
273
|
+
|
|
274
|
+
# Rotate 2
|
|
275
|
+
assert ci.rotate_rpc() is True
|
|
276
|
+
assert ci._current_rpc_index == 2
|
|
277
|
+
|
|
278
|
+
# Rotate 3 (back to 0)
|
|
279
|
+
assert ci.rotate_rpc() is True
|
|
280
|
+
assert ci._current_rpc_index == 0
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_rotate_rpc_no_rpcs(mock_web3):
|
|
284
|
+
chain = MagicMock(spec=SupportedChain)
|
|
285
|
+
chain.name = "TestChain"
|
|
286
|
+
chain.rpcs = []
|
|
287
|
+
chain.name = "TestChain"
|
|
288
|
+
type(chain).rpc = PropertyMock(return_value="")
|
|
289
|
+
ci = ChainInterface(chain)
|
|
290
|
+
assert ci.rotate_rpc() is False
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_rotate_rpc_single_rpc(mock_web3):
|
|
294
|
+
chain = MagicMock(spec=SupportedChain)
|
|
295
|
+
chain.name = "TestChain"
|
|
296
|
+
chain.rpcs = ["http://rpc1"]
|
|
297
|
+
chain.name = "TestChain"
|
|
298
|
+
type(chain).rpc = PropertyMock(return_value="http://rpc1")
|
|
299
|
+
ci = ChainInterface(chain)
|
|
300
|
+
assert ci.rotate_rpc() is False
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# --- Tests migrated from test_chain_interface_coverage.py ---
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def test_chain_interface_with_real_chains():
|
|
307
|
+
"""Test ChainInterface with real chain configurations."""
|
|
308
|
+
from eth_account import Account
|
|
309
|
+
|
|
310
|
+
valid_addr_1 = Account.create().address
|
|
311
|
+
valid_addr_2 = Account.create().address
|
|
312
|
+
|
|
313
|
+
# Patch RateLimitedWeb3 to bypass rate limiting wrapper
|
|
314
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3", side_effect=lambda w3, rl, ci: w3):
|
|
315
|
+
# Use Gnosis() directly (SupportedChain), not ChainInterfaces().gnosis (ChainInterface)
|
|
316
|
+
interface = ChainInterface(Gnosis())
|
|
317
|
+
interface.chain.rpcs = ["http://rpc1", "http://rpc2"]
|
|
318
|
+
interface.web3 = MagicMock()
|
|
319
|
+
interface.web3.provider.endpoint_uri = "http://rpc1"
|
|
320
|
+
interface.web3._web3.eth.block_number = 12345 # For health check
|
|
321
|
+
|
|
322
|
+
# Mock health check to pass
|
|
323
|
+
with patch.object(interface, "check_rpc_health", return_value=True):
|
|
324
|
+
rotated = interface.rotate_rpc()
|
|
325
|
+
assert rotated is True
|
|
326
|
+
|
|
327
|
+
interface.web3.eth.get_code = MagicMock(return_value=b"code")
|
|
328
|
+
assert interface.is_contract(valid_addr_1) is True
|
|
329
|
+
|
|
330
|
+
interface.web3.eth.get_code.return_value = b""
|
|
331
|
+
assert interface.is_contract(valid_addr_2) is False
|
|
332
|
+
|
|
333
|
+
with patch("iwa.core.contracts.erc20.ERC20Contract") as mock_erc20:
|
|
334
|
+
instance = mock_erc20.return_value
|
|
335
|
+
instance.symbol = "SYM"
|
|
336
|
+
instance.decimals = 18
|
|
337
|
+
|
|
338
|
+
interface.web3.eth.get_code.return_value = b"code"
|
|
339
|
+
|
|
340
|
+
sym = interface.get_token_symbol(valid_addr_1)
|
|
341
|
+
assert sym == "SYM"
|
|
342
|
+
|
|
343
|
+
dec = interface.get_token_decimals(valid_addr_1)
|
|
344
|
+
assert dec == 18
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# --- Negative Tests ---
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_send_native_transfer_insufficient_balance(mock_web3):
|
|
351
|
+
"""Test send_native_transfer fails with insufficient balance."""
|
|
352
|
+
chain = MagicMock(spec=SupportedChain)
|
|
353
|
+
chain.name = "TestChain"
|
|
354
|
+
chain.rpcs = ["https://rpc"]
|
|
355
|
+
chain.chain_id = 1
|
|
356
|
+
chain.native_currency = "ETH"
|
|
357
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
358
|
+
|
|
359
|
+
ci = ChainInterface(chain)
|
|
360
|
+
ci.web3.eth.get_transaction_count.return_value = 0
|
|
361
|
+
ci.web3.eth.gas_price = 1000000000 # 1 gwei
|
|
362
|
+
ci.web3.eth.estimate_gas.return_value = 21000
|
|
363
|
+
ci.web3.eth.get_balance.return_value = 1000 # Very low balance
|
|
364
|
+
ci.web3.from_wei.return_value = 0.000001
|
|
365
|
+
|
|
366
|
+
sign_callback = MagicMock()
|
|
367
|
+
|
|
368
|
+
success, tx_hash = ci.send_native_transfer(
|
|
369
|
+
from_address="0x1111111111111111111111111111111111111111",
|
|
370
|
+
to_address="0x2222222222222222222222222222222222222222",
|
|
371
|
+
value_wei=10**18, # 1 ETH - more than available
|
|
372
|
+
sign_callback=sign_callback,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
assert success is False
|
|
376
|
+
assert tx_hash is None
|
|
377
|
+
sign_callback.assert_not_called()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_send_native_transfer_rpc_error(mock_web3):
|
|
381
|
+
"""Test send_native_transfer handles RPC errors."""
|
|
382
|
+
chain = MagicMock(spec=SupportedChain)
|
|
383
|
+
chain.name = "TestChain"
|
|
384
|
+
chain.rpcs = ["https://rpc"]
|
|
385
|
+
chain.chain_id = 1
|
|
386
|
+
chain.native_currency = "ETH"
|
|
387
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
388
|
+
|
|
389
|
+
ci = ChainInterface(chain)
|
|
390
|
+
ci.web3.eth.get_transaction_count.return_value = 0
|
|
391
|
+
ci.web3.eth.gas_price = 1000000000
|
|
392
|
+
ci.web3.eth.estimate_gas.return_value = 21000
|
|
393
|
+
ci.web3.eth.get_balance.return_value = 10**19 # Enough balance
|
|
394
|
+
ci.web3.from_wei.return_value = 10.0
|
|
395
|
+
ci.web3.eth.send_raw_transaction.side_effect = Exception("Connection refused")
|
|
396
|
+
|
|
397
|
+
sign_callback = MagicMock()
|
|
398
|
+
sign_callback.return_value = MagicMock(raw_transaction=b"signed")
|
|
399
|
+
|
|
400
|
+
success, tx_hash = ci.send_native_transfer(
|
|
401
|
+
from_address="0x1111111111111111111111111111111111111111",
|
|
402
|
+
to_address="0x2222222222222222222222222222222222222222",
|
|
403
|
+
value_wei=10**17,
|
|
404
|
+
sign_callback=sign_callback,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
assert success is False
|
|
408
|
+
assert tx_hash is None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def test_get_token_symbol_fallback_on_error(mock_web3):
|
|
412
|
+
"""Test get_token_symbol returns truncated address on error."""
|
|
413
|
+
chain = MagicMock(spec=SupportedChain)
|
|
414
|
+
chain.name = "TestChain"
|
|
415
|
+
chain.rpcs = ["https://rpc"]
|
|
416
|
+
chain.tokens = {}
|
|
417
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
418
|
+
|
|
419
|
+
ci = ChainInterface(chain)
|
|
420
|
+
|
|
421
|
+
# Patch ERC20Contract to raise error
|
|
422
|
+
with patch("iwa.core.contracts.erc20.ERC20Contract") as mock_erc20:
|
|
423
|
+
mock_erc20.side_effect = Exception("Contract not found")
|
|
424
|
+
|
|
425
|
+
address = "0x1234567890123456789012345678901234567890"
|
|
426
|
+
symbol = ci.get_token_symbol(address)
|
|
427
|
+
|
|
428
|
+
# Should return truncated address as fallback
|
|
429
|
+
assert symbol == "0x1234...7890"
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def test_get_token_decimals_fallback_on_error(mock_web3):
|
|
433
|
+
"""Test get_token_decimals returns 18 on error."""
|
|
434
|
+
chain = MagicMock(spec=SupportedChain)
|
|
435
|
+
chain.name = "TestChain"
|
|
436
|
+
chain.rpcs = ["https://rpc"]
|
|
437
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
438
|
+
|
|
439
|
+
ci = ChainInterface(chain)
|
|
440
|
+
|
|
441
|
+
with patch("iwa.core.contracts.erc20.ERC20Contract") as mock_erc20:
|
|
442
|
+
mock_erc20.side_effect = Exception("Contract not found")
|
|
443
|
+
|
|
444
|
+
decimals = ci.get_token_decimals("0x1234567890123456789012345678901234567890")
|
|
445
|
+
|
|
446
|
+
# Should return default 18 as fallback
|
|
447
|
+
assert decimals == 18
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_is_rate_limit_error_detection(mock_web3):
|
|
451
|
+
"""Test _is_rate_limit_error detects various rate limit errors."""
|
|
452
|
+
chain = MagicMock(spec=SupportedChain)
|
|
453
|
+
chain.name = "TestChain"
|
|
454
|
+
chain.rpcs = ["https://rpc"]
|
|
455
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc")
|
|
456
|
+
|
|
457
|
+
ci = ChainInterface(chain)
|
|
458
|
+
|
|
459
|
+
# Should detect rate limit
|
|
460
|
+
assert ci._is_rate_limit_error(Exception("Error 429")) is True
|
|
461
|
+
assert ci._is_rate_limit_error(Exception("rate limit exceeded")) is True
|
|
462
|
+
assert ci._is_rate_limit_error(Exception("Too Many Requests")) is True
|
|
463
|
+
assert ci._is_rate_limit_error(Exception("ratelimit")) is True
|
|
464
|
+
|
|
465
|
+
# Should NOT detect as rate limit
|
|
466
|
+
assert ci._is_rate_limit_error(Exception("Connection timeout")) is False
|
|
467
|
+
assert ci._is_rate_limit_error(Exception("Invalid address")) is False
|
|
468
|
+
assert ci._is_rate_limit_error(Exception("Out of gas")) is False
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def test_handle_rpc_error_non_rate_limit(mock_web3):
|
|
472
|
+
"""Test _handle_rpc_error returns dict with should_retry for connection errors."""
|
|
473
|
+
chain = MagicMock(spec=SupportedChain)
|
|
474
|
+
chain.name = "TestChain"
|
|
475
|
+
chain.rpcs = ["https://rpc1", "https://rpc2"]
|
|
476
|
+
type(chain).rpc = PropertyMock(return_value="https://rpc1")
|
|
477
|
+
|
|
478
|
+
ci = ChainInterface(chain)
|
|
479
|
+
|
|
480
|
+
# Connection error should now return dict with should_retry
|
|
481
|
+
with patch.object(ci, "check_rpc_health", return_value=True):
|
|
482
|
+
result = ci._handle_rpc_error(Exception("Connection timeout"))
|
|
483
|
+
assert isinstance(result, dict)
|
|
484
|
+
assert result["is_connection_error"] is True
|
|
485
|
+
assert result["should_retry"] is True
|
|
486
|
+
|
|
487
|
+
# Non-retryable error (e.g., invalid address) should not trigger retry
|
|
488
|
+
result = ci._handle_rpc_error(Exception("Invalid address"))
|
|
489
|
+
assert isinstance(result, dict)
|
|
490
|
+
assert result["should_retry"] is False
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Tests for ChainInterface RPC error handling and retry logic."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from iwa.core.chain.interface import ChainInterface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def mock_chain_interface():
|
|
12
|
+
"""Create a ChainInterface with mocked web3."""
|
|
13
|
+
with patch("iwa.core.chain.interface.get_rate_limiter") as mock_limiter:
|
|
14
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3") as mock_rlw3:
|
|
15
|
+
mock_limiter.return_value = MagicMock()
|
|
16
|
+
mock_web3 = MagicMock()
|
|
17
|
+
mock_rlw3.return_value = mock_web3
|
|
18
|
+
|
|
19
|
+
# Construct with mock chain
|
|
20
|
+
with patch("iwa.core.chain.models.Gnosis") as mock_gnosis_cls:
|
|
21
|
+
mock_gnosis = mock_gnosis_cls.return_value
|
|
22
|
+
mock_gnosis.name = "gnosis"
|
|
23
|
+
mock_gnosis.rpc = "https://rpc.gnosis.gateway.fm"
|
|
24
|
+
mock_gnosis.rpcs = [
|
|
25
|
+
"https://rpc.gnosis.gateway.fm",
|
|
26
|
+
"https://rpc2.gnosis.gateway.fm",
|
|
27
|
+
]
|
|
28
|
+
mock_gnosis.chain_id = 100
|
|
29
|
+
mock_gnosis.native_currency = "xDAI"
|
|
30
|
+
mock_gnosis.tokens = {}
|
|
31
|
+
mock_gnosis.contracts = {}
|
|
32
|
+
|
|
33
|
+
interface = ChainInterface(mock_gnosis)
|
|
34
|
+
interface._rate_limiter = mock_limiter.return_value
|
|
35
|
+
|
|
36
|
+
yield interface, mock_web3
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_is_rate_limit_error():
|
|
40
|
+
"""Test rate limit error detection."""
|
|
41
|
+
with patch("iwa.core.chain.interface.get_rate_limiter"):
|
|
42
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3"):
|
|
43
|
+
with patch("iwa.core.chain.models.Gnosis") as mock_gnosis_cls:
|
|
44
|
+
mock_gnosis = mock_gnosis_cls.return_value
|
|
45
|
+
mock_gnosis.name = "gnosis"
|
|
46
|
+
mock_gnosis.rpc = "https://rpc.example.com"
|
|
47
|
+
mock_gnosis.rpcs = ["https://rpc.example.com"]
|
|
48
|
+
mock_gnosis.chain_id = 100
|
|
49
|
+
mock_gnosis.native_currency = "xDAI"
|
|
50
|
+
mock_gnosis.tokens = {}
|
|
51
|
+
mock_gnosis.contracts = {}
|
|
52
|
+
|
|
53
|
+
interface = ChainInterface(mock_gnosis)
|
|
54
|
+
|
|
55
|
+
assert interface._is_rate_limit_error(Exception("429 Too Many Requests"))
|
|
56
|
+
assert interface._is_rate_limit_error(Exception("rate limit exceeded"))
|
|
57
|
+
assert not interface._is_rate_limit_error(Exception("connection refused"))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_is_connection_error():
|
|
61
|
+
"""Test connection error detection."""
|
|
62
|
+
with patch("iwa.core.chain.interface.get_rate_limiter"):
|
|
63
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3"):
|
|
64
|
+
with patch("iwa.core.chain.models.Gnosis") as mock_gnosis_cls:
|
|
65
|
+
mock_gnosis = mock_gnosis_cls.return_value
|
|
66
|
+
mock_gnosis.name = "gnosis"
|
|
67
|
+
mock_gnosis.rpc = "https://rpc.example.com"
|
|
68
|
+
mock_gnosis.rpcs = ["https://rpc.example.com"]
|
|
69
|
+
mock_gnosis.chain_id = 100
|
|
70
|
+
mock_gnosis.native_currency = "xDAI"
|
|
71
|
+
mock_gnosis.tokens = {}
|
|
72
|
+
mock_gnosis.contracts = {}
|
|
73
|
+
|
|
74
|
+
interface = ChainInterface(mock_gnosis)
|
|
75
|
+
|
|
76
|
+
assert interface._is_connection_error(Exception("connection timeout"))
|
|
77
|
+
assert interface._is_connection_error(Exception("connection refused"))
|
|
78
|
+
assert interface._is_connection_error(Exception("read timeout"))
|
|
79
|
+
assert not interface._is_connection_error(Exception("429"))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_is_server_error():
|
|
83
|
+
"""Test server error detection."""
|
|
84
|
+
with patch("iwa.core.chain.interface.get_rate_limiter"):
|
|
85
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3"):
|
|
86
|
+
with patch("iwa.core.chain.models.Gnosis") as mock_gnosis_cls:
|
|
87
|
+
mock_gnosis = mock_gnosis_cls.return_value
|
|
88
|
+
mock_gnosis.name = "gnosis"
|
|
89
|
+
mock_gnosis.rpc = "https://rpc.example.com"
|
|
90
|
+
mock_gnosis.rpcs = ["https://rpc.example.com"]
|
|
91
|
+
mock_gnosis.chain_id = 100
|
|
92
|
+
mock_gnosis.native_currency = "xDAI"
|
|
93
|
+
mock_gnosis.tokens = {}
|
|
94
|
+
mock_gnosis.contracts = {}
|
|
95
|
+
|
|
96
|
+
interface = ChainInterface(mock_gnosis)
|
|
97
|
+
|
|
98
|
+
assert interface._is_server_error(Exception("500 internal server error"))
|
|
99
|
+
assert interface._is_server_error(Exception("502 bad gateway"))
|
|
100
|
+
assert not interface._is_server_error(Exception("404 not found"))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_is_tenderly_quota_exceeded():
|
|
104
|
+
"""Test Tenderly quota detection."""
|
|
105
|
+
with patch("iwa.core.chain.interface.get_rate_limiter"):
|
|
106
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3"):
|
|
107
|
+
with patch("iwa.core.chain.models.Gnosis") as mock_gnosis_cls:
|
|
108
|
+
mock_gnosis = mock_gnosis_cls.return_value
|
|
109
|
+
mock_gnosis.name = "gnosis"
|
|
110
|
+
mock_gnosis.rpc = "https://virtual.tenderly.co/xxx"
|
|
111
|
+
mock_gnosis.rpcs = ["https://virtual.tenderly.co/xxx"]
|
|
112
|
+
mock_gnosis.chain_id = 100
|
|
113
|
+
mock_gnosis.native_currency = "xDAI"
|
|
114
|
+
mock_gnosis.tokens = {}
|
|
115
|
+
mock_gnosis.contracts = {}
|
|
116
|
+
|
|
117
|
+
interface = ChainInterface(mock_gnosis)
|
|
118
|
+
|
|
119
|
+
assert interface._is_tenderly_quota_exceeded(
|
|
120
|
+
Exception("403 Forbidden tenderly virtual network")
|
|
121
|
+
)
|
|
122
|
+
assert not interface._is_tenderly_quota_exceeded(Exception("500 server error"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_handle_rpc_error_rotation(mock_chain_interface):
|
|
126
|
+
"""Test RPC error handling triggers rotation."""
|
|
127
|
+
interface, mock_web3 = mock_chain_interface
|
|
128
|
+
|
|
129
|
+
# Mock rotate_rpc to return True
|
|
130
|
+
interface.rotate_rpc = MagicMock(return_value=True)
|
|
131
|
+
|
|
132
|
+
error = Exception("429 rate limit")
|
|
133
|
+
result = interface._handle_rpc_error(error)
|
|
134
|
+
|
|
135
|
+
assert result["is_rate_limit"]
|
|
136
|
+
assert result["should_retry"]
|
|
137
|
+
interface.rotate_rpc.assert_called()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_handle_rpc_error_server_error(mock_chain_interface):
|
|
141
|
+
"""Test server error triggers retry without rotation."""
|
|
142
|
+
interface, _ = mock_chain_interface
|
|
143
|
+
interface.rotate_rpc = MagicMock(return_value=False)
|
|
144
|
+
|
|
145
|
+
error = Exception("503 service unavailable")
|
|
146
|
+
result = interface._handle_rpc_error(error)
|
|
147
|
+
|
|
148
|
+
assert result["is_server_error"]
|
|
149
|
+
assert result["should_retry"]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_check_rpc_health(mock_chain_interface):
|
|
153
|
+
"""Test RPC health check."""
|
|
154
|
+
interface, mock_web3 = mock_chain_interface
|
|
155
|
+
|
|
156
|
+
# Healthy
|
|
157
|
+
mock_web3._web3.eth.block_number = 1000
|
|
158
|
+
assert interface.check_rpc_health()
|
|
159
|
+
|
|
160
|
+
# Unhealthy
|
|
161
|
+
mock_web3._web3.eth.block_number = None
|
|
162
|
+
assert not interface.check_rpc_health()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_rotate_rpc_single_rpc():
|
|
166
|
+
"""Test rotation fails with single RPC."""
|
|
167
|
+
with patch("iwa.core.chain.interface.get_rate_limiter"):
|
|
168
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3"):
|
|
169
|
+
with patch("iwa.core.chain.models.Gnosis") as mock_gnosis_cls:
|
|
170
|
+
mock_gnosis = mock_gnosis_cls.return_value
|
|
171
|
+
mock_gnosis.name = "gnosis"
|
|
172
|
+
mock_gnosis.rpc = "https://rpc.example.com"
|
|
173
|
+
mock_gnosis.rpcs = ["https://rpc.example.com"] # Only one RPC
|
|
174
|
+
mock_gnosis.chain_id = 100
|
|
175
|
+
mock_gnosis.native_currency = "xDAI"
|
|
176
|
+
mock_gnosis.tokens = {}
|
|
177
|
+
mock_gnosis.contracts = {}
|
|
178
|
+
|
|
179
|
+
interface = ChainInterface(mock_gnosis)
|
|
180
|
+
|
|
181
|
+
assert not interface.rotate_rpc()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_is_tenderly_property():
|
|
185
|
+
"""Test is_tenderly property."""
|
|
186
|
+
with patch("iwa.core.chain.interface.get_rate_limiter"):
|
|
187
|
+
with patch("iwa.core.chain.interface.RateLimitedWeb3"):
|
|
188
|
+
with patch("iwa.core.chain.models.Gnosis") as mock_gnosis_cls:
|
|
189
|
+
mock_gnosis = mock_gnosis_cls.return_value
|
|
190
|
+
mock_gnosis.name = "gnosis"
|
|
191
|
+
mock_gnosis.rpc = "https://virtual.tenderly.co/xxx"
|
|
192
|
+
mock_gnosis.rpcs = ["https://virtual.tenderly.co/xxx"]
|
|
193
|
+
mock_gnosis.chain_id = 100
|
|
194
|
+
mock_gnosis.native_currency = "xDAI"
|
|
195
|
+
mock_gnosis.tokens = {}
|
|
196
|
+
mock_gnosis.contracts = {}
|
|
197
|
+
|
|
198
|
+
interface = ChainInterface(mock_gnosis)
|
|
199
|
+
|
|
200
|
+
assert interface.is_tenderly
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_reset_rpc_failure_counts(mock_chain_interface):
|
|
204
|
+
"""Test resetting failure counts."""
|
|
205
|
+
interface, _ = mock_chain_interface
|
|
206
|
+
interface._rpc_failure_counts = {0: 5, 1: 3}
|
|
207
|
+
|
|
208
|
+
interface.reset_rpc_failure_counts()
|
|
209
|
+
|
|
210
|
+
assert interface._rpc_failure_counts == {}
|