Skip to content

Commit b68cd9f

Browse files
committed
Catch newly raised geth errors when tx indexing in progress:
- Geth ``1.13.11`` started to return an error when transaction indexing is in progress and a ``eth_getTransactionReceipt`` call is made. Handle this in the ``wait_for_transaction_receipt`` method by catching the error in order to continue waiting.
1 parent 4410048 commit b68cd9f

File tree

10 files changed

+152
-33
lines changed

10 files changed

+152
-33
lines changed

tests/core/contracts/test_contract_panic_errors.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
from tests.core.contracts.utils import (
55
deploy,
66
)
7-
from web3._utils.contract_error_handling import (
8-
PANIC_ERROR_CODES,
9-
)
107
from web3._utils.contract_sources.contract_data.panic_errors_contract import (
118
PANIC_ERRORS_CONTRACT_DATA,
129
)
10+
from web3._utils.error_formatters_utils import (
11+
PANIC_ERROR_CODES,
12+
)
1313
from web3.exceptions import (
1414
ContractPanicError,
1515
)

tests/core/eth-module/test_transactions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
RECEIPT_TIMEOUT = 0.2
3030

3131

32+
def _tx_indexing_response_iterator():
33+
while True:
34+
yield {"error": {"message": "transaction indexing in progress"}}
35+
yield {"error": {"message": "transaction indexing in progress"}}
36+
yield {"result": {"status": "0x1"}}
37+
38+
3239
@pytest.mark.parametrize(
3340
"transaction",
3441
(
@@ -202,6 +209,18 @@ def test_unmined_transaction_wait_for_receipt(w3, request_mocker):
202209
assert txn_receipt["blockHash"] is not None
203210

204211

212+
def test_eth_wait_for_transaction_receipt_transaction_indexing_in_progress(
213+
w3, request_mocker
214+
):
215+
i = _tx_indexing_response_iterator()
216+
with request_mocker(
217+
w3,
218+
mock_responses={"eth_getTransactionReceipt": lambda *_: next(i)},
219+
):
220+
receipt = w3.eth.wait_for_transaction_receipt(f"0x{'00' * 32}")
221+
assert receipt == {"status": 1}
222+
223+
205224
def test_get_transaction_formatters(w3, request_mocker):
206225
non_checksummed_addr = "0xB2930B35844A230F00E51431ACAE96FE543A0347" # all uppercase
207226
unformatted_transaction = {
@@ -304,3 +323,19 @@ def test_get_transaction_formatters(w3, request_mocker):
304323
)
305324

306325
assert received_tx == expected
326+
327+
328+
# --- async --- #
329+
330+
331+
@pytest.mark.asyncio
332+
async def test_async_wait_for_transaction_receipt_transaction_indexing_in_progress(
333+
async_w3, request_mocker
334+
):
335+
i = _tx_indexing_response_iterator()
336+
async with request_mocker(
337+
async_w3,
338+
mock_responses={"eth_getTransactionReceipt": lambda *_: next(i)},
339+
):
340+
receipt = await async_w3.eth.wait_for_transaction_receipt(f"0x{'00' * 32}")
341+
assert receipt == {"status": 1}

web3/_utils/contract_error_handling.py renamed to web3/_utils/error_formatters_utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ContractLogicError,
1313
ContractPanicError,
1414
OffchainLookup,
15+
TransactionIndexingInProgress,
1516
)
1617
from web3.types import (
1718
RPCResponse,
@@ -164,3 +165,21 @@ def raise_contract_logic_error_on_revert(response: RPCResponse) -> RPCResponse:
164165
raise ContractLogicError("execution reverted", data=data)
165166

166167
return response
168+
169+
170+
def raise_transaction_indexing_error_if_indexing(response: RPCResponse) -> RPCResponse:
171+
"""
172+
Raise an error if ``eth_getTransactionReceipt`` returns a response indicating that
173+
transactions are still being indexed.
174+
"""
175+
176+
error = response.get("error")
177+
if not isinstance(error, str) and error is not None:
178+
message = error.get("message")
179+
if message is not None:
180+
if all(
181+
idx_key_phrases in message for idx_key_phrases in ("index", "progress")
182+
):
183+
raise TransactionIndexingInProgress(message)
184+
185+
return response

web3/_utils/method_formatters.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,14 @@
5252
from web3._utils.abi import (
5353
is_length,
5454
)
55-
from web3._utils.contract_error_handling import (
56-
raise_contract_logic_error_on_revert,
57-
)
5855
from web3._utils.encoding import (
5956
hexstr_if_str,
6057
to_hex,
6158
)
59+
from web3._utils.error_formatters_utils import (
60+
raise_contract_logic_error_on_revert,
61+
raise_transaction_indexing_error_if_indexing,
62+
)
6263
from web3._utils.filters import (
6364
AsyncBlockFilter,
6465
AsyncLogFilter,
@@ -791,6 +792,7 @@ def subscription_formatter(value: Any) -> Union[HexBytes, HexStr, Dict[str, Any]
791792
ERROR_FORMATTERS: Dict[RPCEndpoint, Callable[..., Any]] = {
792793
RPC.eth_estimateGas: raise_contract_logic_error_on_revert,
793794
RPC.eth_call: raise_contract_logic_error_on_revert,
795+
RPC.eth_getTransactionReceipt: raise_transaction_indexing_error_if_indexing,
794796
}
795797

796798

web3/_utils/module_testing/eth_module.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@
4141
HexBytes,
4242
)
4343

44-
from web3._utils.contract_error_handling import (
45-
PANIC_ERROR_CODES,
46-
)
4744
from web3._utils.empty import (
4845
empty,
4946
)
5047
from web3._utils.ens import (
5148
ens_addresses,
5249
)
50+
from web3._utils.error_formatters_utils import (
51+
PANIC_ERROR_CODES,
52+
)
5353
from web3._utils.fee_utils import (
5454
PRIORITY_FEE_MIN,
5555
)

web3/_utils/module_testing/utils.py

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,46 @@ def test_my_w3(w3, request_mocker):
4545
4646
assert w3.eth.block_number == 0
4747
48-
``mock_results`` is a dict mapping method names to the desired "result" object of
49-
the RPC response. ``mock_errors`` is a dict mapping method names to the desired
50-
"error" object of the RPC response. If a method name is not in either dict,
51-
the request is made as usual.
48+
Example with async and a mocked response object:
49+
50+
async def test_my_w3(async_w3, request_mocker):
51+
def _iter_responses():
52+
yield {"error": {"code": -32000, "message": "indexing in progress"}}
53+
yield {"error": {"code": -32000, "message": "indexing in progress"}}
54+
yield {"result": "0x1"}
55+
56+
iter_responses = _iter_responses()
57+
58+
async with request_mocker(
59+
async_w3,
60+
mock_responses={"eth_getTransactionReceipt": next(iter_responses)}
61+
):
62+
assert await w3.eth.get_transaction_receipt("0x1") == "0x1"
63+
64+
65+
- ``mock_results`` is a dict mapping method names to the desired "result" object of
66+
the RPC response.
67+
- ``mock_errors`` is a dict mapping method names to the desired
68+
"error" object of the RPC response.
69+
-``mock_responses`` is a dict mapping method names to the entire RPC response
70+
object. This can be useful if you wish to return an iterator which returns
71+
different responses on each call to the method.
72+
73+
If a method name is not present in any of the dicts above, the request is made as
74+
usual.
5275
"""
5376

5477
def __init__(
5578
self,
5679
w3: Union["AsyncWeb3", "Web3"],
5780
mock_results: Dict[Union["RPCEndpoint", str], Any] = None,
5881
mock_errors: Dict[Union["RPCEndpoint", str], Any] = None,
82+
mock_responses: Dict[Union["RPCEndpoint", str], Any] = None,
5983
):
6084
self.w3 = w3
6185
self.mock_results = mock_results or {}
6286
self.mock_errors = mock_errors or {}
87+
self.mock_responses = mock_responses or {}
6388
self._make_request: Union["AsyncMakeRequestFn", "MakeRequestFn"] = (
6489
w3.provider.make_request
6590
)
@@ -83,7 +108,10 @@ def _mock_request_handler(
83108
self.w3 = cast("Web3", self.w3)
84109
self._make_request = cast("MakeRequestFn", self._make_request)
85110

86-
if method not in self.mock_errors and method not in self.mock_results:
111+
if all(
112+
method not in mock_dict
113+
for mock_dict in (self.mock_errors, self.mock_results, self.mock_responses)
114+
):
87115
return self._make_request(method, params)
88116

89117
request_id = (
@@ -93,7 +121,18 @@ def _mock_request_handler(
93121
)
94122
response_dict = {"jsonrpc": "2.0", "id": request_id}
95123

96-
if method in self.mock_results:
124+
if method in self.mock_responses:
125+
mock_return = self.mock_responses[method]
126+
if callable(mock_return):
127+
mock_return = mock_return(method, params)
128+
129+
if "result" in mock_return:
130+
mock_return = {"result": mock_return["result"]}
131+
elif "error" in mock_return:
132+
mock_return = self._create_error_object(mock_return["error"])
133+
134+
mocked_response = merge(response_dict, mock_return)
135+
elif method in self.mock_results:
97136
mock_return = self.mock_results[method]
98137
if callable(mock_return):
99138
mock_return = mock_return(method, params)
@@ -102,12 +141,7 @@ def _mock_request_handler(
102141
error = self.mock_errors[method]
103142
if callable(error):
104143
error = error(method, params)
105-
code = error.get("code", -32000)
106-
message = error.get("message", "Mocked error")
107-
mocked_response = merge(
108-
response_dict,
109-
{"error": merge({"code": code, "message": message}, error)},
110-
)
144+
mocked_response = merge(response_dict, self._create_error_object(error))
111145
else:
112146
raise Exception("Invariant: unreachable code path")
113147

@@ -140,7 +174,10 @@ async def _async_mock_request_handler(
140174
self.w3 = cast("AsyncWeb3", self.w3)
141175
self._make_request = cast("AsyncMakeRequestFn", self._make_request)
142176

143-
if method not in self.mock_errors and method not in self.mock_results:
177+
if all(
178+
method not in mock_dict
179+
for mock_dict in (self.mock_errors, self.mock_results, self.mock_responses)
180+
):
144181
return await self._make_request(method, params)
145182

146183
request_id = (
@@ -150,7 +187,22 @@ async def _async_mock_request_handler(
150187
)
151188
response_dict = {"jsonrpc": "2.0", "id": request_id}
152189

153-
if method in self.mock_results:
190+
if method in self.mock_responses:
191+
mock_return = self.mock_responses[method]
192+
193+
if callable(mock_return):
194+
mock_return = mock_return(method, params)
195+
elif iscoroutinefunction(mock_return):
196+
# this is the "correct" way to mock the async make_request
197+
mock_return = await mock_return(method, params)
198+
199+
if "result" in mock_return:
200+
mock_return = {"result": mock_return["result"]}
201+
elif "error" in mock_return:
202+
mock_return = self._create_error_object(mock_return["error"])
203+
204+
mocked_result = merge(response_dict, mock_return)
205+
elif method in self.mock_results:
154206
mock_return = self.mock_results[method]
155207
if callable(mock_return):
156208
# handle callable to make things easier since we're mocking
@@ -167,13 +219,7 @@ async def _async_mock_request_handler(
167219
error = error(method, params)
168220
elif iscoroutinefunction(error):
169221
error = await error(method, params)
170-
171-
code = error.get("code", -32000)
172-
message = error.get("message", "Mocked error")
173-
mocked_result = merge(
174-
response_dict,
175-
{"error": merge({"code": code, "message": message}, error)},
176-
)
222+
mocked_result = merge(response_dict, self._create_error_object(error))
177223

178224
else:
179225
raise Exception("Invariant: unreachable code path")
@@ -192,3 +238,9 @@ async def _coro(
192238
return await decorator(_coro)(self.w3.provider, method, params)
193239
else:
194240
return mocked_result
241+
242+
@staticmethod
243+
def _create_error_object(error: Dict[str, Any]) -> Dict[str, Any]:
244+
code = error.get("code", -32000)
245+
message = error.get("message", "Mocked error")
246+
return {"error": merge({"code": code, "message": message}, error)}

web3/eth/async_eth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
OffchainLookup,
6161
TimeExhausted,
6262
TooManyRequests,
63+
TransactionIndexingInProgress,
6364
TransactionNotFound,
6465
)
6566
from web3.method import (
@@ -517,7 +518,7 @@ async def _wait_for_tx_receipt_with_timeout(
517518
while True:
518519
try:
519520
tx_receipt = await self._transaction_receipt(_tx_hash)
520-
except TransactionNotFound:
521+
except (TransactionNotFound, TransactionIndexingInProgress):
521522
tx_receipt = None
522523
if tx_receipt is not None:
523524
break

web3/eth/eth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
OffchainLookup,
6161
TimeExhausted,
6262
TooManyRequests,
63+
TransactionIndexingInProgress,
6364
TransactionNotFound,
6465
)
6566
from web3.method import (
@@ -485,7 +486,7 @@ def wait_for_transaction_receipt(
485486
while True:
486487
try:
487488
tx_receipt = self._transaction_receipt(transaction_hash)
488-
except TransactionNotFound:
489+
except (TransactionNotFound, TransactionIndexingInProgress):
489490
tx_receipt = None
490491
if tx_receipt is not None:
491492
break

web3/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,15 @@ class TransactionNotFound(Web3Exception):
223223
pass
224224

225225

226+
class TransactionIndexingInProgress(Web3Exception):
227+
"""
228+
Raised when a transaction receipt is not yet available due to transaction indexing
229+
still being in progress.
230+
"""
231+
232+
pass
233+
234+
226235
class BlockNotFound(Web3Exception):
227236
"""
228237
Raised when the block id used to lookup a block in a jsonrpc call cannot be found.

web3/providers/eth_tester/defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from web3 import (
4646
Web3,
4747
)
48-
from web3._utils.contract_error_handling import (
48+
from web3._utils.error_formatters_utils import (
4949
OFFCHAIN_LOOKUP_FIELDS,
5050
PANIC_ERROR_CODES,
5151
)

0 commit comments

Comments
 (0)