web3 7.8.0__py3-none-any.whl → 7.9.0__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.
- web3/_utils/contract_sources/contract_data/ambiguous_function_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/arrays_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/bytes_contracts.py +5 -5
- web3/_utils/contract_sources/contract_data/constructor_contracts.py +7 -7
- web3/_utils/contract_sources/contract_data/contract_caller_tester.py +3 -3
- web3/_utils/contract_sources/contract_data/emitter_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/event_contracts.py +7 -7
- web3/_utils/contract_sources/contract_data/extended_resolver.py +3 -3
- web3/_utils/contract_sources/contract_data/fallback_function_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/function_name_tester_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/math_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/offchain_lookup.py +3 -3
- web3/_utils/contract_sources/contract_data/offchain_resolver.py +3 -3
- web3/_utils/contract_sources/contract_data/panic_errors_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/payable_tester.py +3 -3
- web3/_utils/contract_sources/contract_data/receive_function_contracts.py +5 -5
- web3/_utils/contract_sources/contract_data/reflector_contracts.py +3 -3
- web3/_utils/contract_sources/contract_data/revert_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/simple_resolver.py +3 -3
- web3/_utils/contract_sources/contract_data/storage_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/string_contract.py +3 -3
- web3/_utils/contract_sources/contract_data/tuple_contracts.py +5 -5
- web3/_utils/error_formatters_utils.py +1 -1
- web3/_utils/formatters.py +28 -0
- web3/_utils/method_formatters.py +121 -20
- web3/_utils/module_testing/eth_module.py +99 -2
- web3/_utils/module_testing/persistent_connection_provider.py +1 -1
- web3/_utils/rpc_abi.py +1 -0
- web3/_utils/validation.py +191 -0
- web3/eth/async_eth.py +19 -0
- web3/eth/eth.py +15 -0
- web3/gas_strategies/time_based.py +1 -1
- web3/manager.py +13 -204
- web3/providers/persistent/persistent.py +29 -3
- web3/providers/persistent/subscription_manager.py +7 -2
- web3/providers/persistent/websocket.py +8 -3
- web3/types.py +26 -2
- {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/METADATA +11 -7
- {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/RECORD +42 -42
- {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/WHEEL +1 -1
- {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/LICENSE +0 -0
- {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/top_level.txt +0 -0
web3/_utils/validation.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import itertools
|
|
2
|
+
import logging
|
|
2
3
|
from typing import (
|
|
3
4
|
Any,
|
|
5
|
+
Callable,
|
|
4
6
|
Dict,
|
|
7
|
+
NoReturn,
|
|
8
|
+
Optional,
|
|
5
9
|
)
|
|
6
10
|
|
|
7
11
|
from eth_typing import (
|
|
@@ -53,11 +57,22 @@ from web3._utils.abi import (
|
|
|
53
57
|
length_of_array_type,
|
|
54
58
|
sub_type_of_array_type,
|
|
55
59
|
)
|
|
60
|
+
from web3._utils.formatters import (
|
|
61
|
+
apply_error_formatters,
|
|
62
|
+
)
|
|
56
63
|
from web3.exceptions import (
|
|
64
|
+
BadResponseFormat,
|
|
57
65
|
InvalidAddress,
|
|
66
|
+
MethodUnavailable,
|
|
67
|
+
RequestTimedOut,
|
|
68
|
+
TransactionNotFound,
|
|
69
|
+
Web3RPCError,
|
|
58
70
|
Web3TypeError,
|
|
59
71
|
Web3ValueError,
|
|
60
72
|
)
|
|
73
|
+
from web3.types import (
|
|
74
|
+
RPCResponse,
|
|
75
|
+
)
|
|
61
76
|
|
|
62
77
|
|
|
63
78
|
def _prepare_selector_collision_msg(duplicates: Dict[HexStr, ABIFunction]) -> str:
|
|
@@ -211,3 +226,179 @@ def assert_one_val(*args: Any, **kwargs: Any) -> None:
|
|
|
211
226
|
"Exactly one of the passed values can be specified. "
|
|
212
227
|
f"Instead, values were: {args!r}, {kwargs!r}"
|
|
213
228
|
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# -- RPC Response Validation -- #
|
|
232
|
+
|
|
233
|
+
KNOWN_REQUEST_TIMEOUT_MESSAGING = {
|
|
234
|
+
# Note: It's important to be very explicit here and not too broad. We don't want
|
|
235
|
+
# to accidentally catch a message that is not for a request timeout. In the worst
|
|
236
|
+
# case, we raise something more generic like `Web3RPCError`. JSON-RPC unfortunately
|
|
237
|
+
# has not standardized error codes for request timeouts.
|
|
238
|
+
"request timed out", # go-ethereum
|
|
239
|
+
}
|
|
240
|
+
METHOD_NOT_FOUND = -32601
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _validate_subscription_fields(response: RPCResponse) -> None:
|
|
244
|
+
params = response["params"]
|
|
245
|
+
subscription = params["subscription"]
|
|
246
|
+
if not isinstance(subscription, str) and not len(subscription) == 34:
|
|
247
|
+
_raise_bad_response_format(
|
|
248
|
+
response, "eth_subscription 'params' must include a 'subscription' field."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _raise_bad_response_format(response: RPCResponse, error: str = "") -> None:
|
|
253
|
+
message = "The response was in an unexpected format and unable to be parsed."
|
|
254
|
+
raw_response = f"The raw response is: {response}"
|
|
255
|
+
|
|
256
|
+
if error is not None and error != "":
|
|
257
|
+
error = error[:-1] if error.endswith(".") else error
|
|
258
|
+
message = f"{message} {error}. {raw_response}"
|
|
259
|
+
else:
|
|
260
|
+
message = f"{message} {raw_response}"
|
|
261
|
+
|
|
262
|
+
raise BadResponseFormat(message)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def raise_error_for_batch_response(
|
|
266
|
+
response: RPCResponse,
|
|
267
|
+
logger: Optional[logging.Logger] = None,
|
|
268
|
+
) -> NoReturn:
|
|
269
|
+
error = response.get("error")
|
|
270
|
+
if error is None:
|
|
271
|
+
_raise_bad_response_format(
|
|
272
|
+
response,
|
|
273
|
+
"Batch response must be formatted as a list of responses or "
|
|
274
|
+
"as a single JSON-RPC error response.",
|
|
275
|
+
)
|
|
276
|
+
validate_rpc_response_and_raise_if_error(
|
|
277
|
+
response,
|
|
278
|
+
None,
|
|
279
|
+
is_subscription_response=False,
|
|
280
|
+
logger=logger,
|
|
281
|
+
params=[],
|
|
282
|
+
)
|
|
283
|
+
# This should not be reached, but if it is, raise a generic `BadResponseFormat`
|
|
284
|
+
raise BadResponseFormat(
|
|
285
|
+
"Batch response was in an unexpected format and unable to be parsed."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def validate_rpc_response_and_raise_if_error(
|
|
290
|
+
response: RPCResponse,
|
|
291
|
+
error_formatters: Optional[Callable[..., Any]],
|
|
292
|
+
is_subscription_response: bool = False,
|
|
293
|
+
logger: Optional[logging.Logger] = None,
|
|
294
|
+
params: Optional[Any] = None,
|
|
295
|
+
) -> None:
|
|
296
|
+
if "jsonrpc" not in response or response["jsonrpc"] != "2.0":
|
|
297
|
+
_raise_bad_response_format(
|
|
298
|
+
response, 'The "jsonrpc" field must be present with a value of "2.0".'
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
response_id = response.get("id")
|
|
302
|
+
if "id" in response:
|
|
303
|
+
int_error_msg = (
|
|
304
|
+
'"id" must be an integer or a string representation of an integer.'
|
|
305
|
+
)
|
|
306
|
+
if response_id is None and "error" in response:
|
|
307
|
+
# errors can sometimes have null `id`, according to the JSON-RPC spec
|
|
308
|
+
pass
|
|
309
|
+
elif not isinstance(response_id, (str, int)):
|
|
310
|
+
_raise_bad_response_format(response, int_error_msg)
|
|
311
|
+
elif isinstance(response_id, str):
|
|
312
|
+
try:
|
|
313
|
+
int(response_id)
|
|
314
|
+
except ValueError:
|
|
315
|
+
_raise_bad_response_format(response, int_error_msg)
|
|
316
|
+
elif is_subscription_response:
|
|
317
|
+
# if `id` is not present, this must be a subscription response
|
|
318
|
+
_validate_subscription_fields(response)
|
|
319
|
+
else:
|
|
320
|
+
_raise_bad_response_format(
|
|
321
|
+
response,
|
|
322
|
+
'Response must include an "id" field or be formatted as an '
|
|
323
|
+
"`eth_subscription` response.",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if all(key in response for key in {"error", "result"}):
|
|
327
|
+
_raise_bad_response_format(
|
|
328
|
+
response, 'Response cannot include both "error" and "result".'
|
|
329
|
+
)
|
|
330
|
+
elif (
|
|
331
|
+
not any(key in response for key in {"error", "result"})
|
|
332
|
+
and not is_subscription_response
|
|
333
|
+
):
|
|
334
|
+
_raise_bad_response_format(
|
|
335
|
+
response, 'Response must include either "error" or "result".'
|
|
336
|
+
)
|
|
337
|
+
elif "error" in response:
|
|
338
|
+
web3_rpc_error: Optional[Web3RPCError] = None
|
|
339
|
+
error = response["error"]
|
|
340
|
+
|
|
341
|
+
# raise the error when the value is a string
|
|
342
|
+
if error is None or not isinstance(error, dict):
|
|
343
|
+
_raise_bad_response_format(
|
|
344
|
+
response,
|
|
345
|
+
'response["error"] must be a valid object as defined by the '
|
|
346
|
+
"JSON-RPC 2.0 specification.",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# errors must include a message
|
|
350
|
+
error_message = error.get("message")
|
|
351
|
+
if not isinstance(error_message, str):
|
|
352
|
+
_raise_bad_response_format(
|
|
353
|
+
response, 'error["message"] is required and must be a string value.'
|
|
354
|
+
)
|
|
355
|
+
elif error_message == "transaction not found":
|
|
356
|
+
transaction_hash = params[0]
|
|
357
|
+
web3_rpc_error = TransactionNotFound(
|
|
358
|
+
repr(error),
|
|
359
|
+
rpc_response=response,
|
|
360
|
+
user_message=(f"Transaction with hash {transaction_hash!r} not found."),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# errors must include an integer code
|
|
364
|
+
code = error.get("code")
|
|
365
|
+
if not isinstance(code, int):
|
|
366
|
+
_raise_bad_response_format(
|
|
367
|
+
response, 'error["code"] is required and must be an integer value.'
|
|
368
|
+
)
|
|
369
|
+
elif code == METHOD_NOT_FOUND:
|
|
370
|
+
web3_rpc_error = MethodUnavailable(
|
|
371
|
+
repr(error),
|
|
372
|
+
rpc_response=response,
|
|
373
|
+
user_message=(
|
|
374
|
+
"This method is not available. Check your node provider or your "
|
|
375
|
+
"client's API docs to see what methods are supported and / or "
|
|
376
|
+
"currently enabled."
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
elif any(
|
|
380
|
+
# parse specific timeout messages
|
|
381
|
+
timeout_str in error_message.lower()
|
|
382
|
+
for timeout_str in KNOWN_REQUEST_TIMEOUT_MESSAGING
|
|
383
|
+
):
|
|
384
|
+
web3_rpc_error = RequestTimedOut(
|
|
385
|
+
repr(error),
|
|
386
|
+
rpc_response=response,
|
|
387
|
+
user_message=(
|
|
388
|
+
"The request timed out. Check the connection to your node and "
|
|
389
|
+
"try again."
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if web3_rpc_error is None:
|
|
394
|
+
# if no condition was met above, raise a more generic `Web3RPCError`
|
|
395
|
+
web3_rpc_error = Web3RPCError(repr(error), rpc_response=response)
|
|
396
|
+
|
|
397
|
+
response = apply_error_formatters(error_formatters, response)
|
|
398
|
+
if logger is not None:
|
|
399
|
+
logger.debug(f"RPC error response: {response}")
|
|
400
|
+
|
|
401
|
+
raise web3_rpc_error
|
|
402
|
+
|
|
403
|
+
elif "result" not in response and not is_subscription_response:
|
|
404
|
+
_raise_bad_response_format(response)
|
web3/eth/async_eth.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import (
|
|
|
7
7
|
Dict,
|
|
8
8
|
List,
|
|
9
9
|
Optional,
|
|
10
|
+
Sequence,
|
|
10
11
|
Tuple,
|
|
11
12
|
Type,
|
|
12
13
|
Union,
|
|
@@ -89,6 +90,8 @@ from web3.types import (
|
|
|
89
90
|
LogsSubscriptionArg,
|
|
90
91
|
Nonce,
|
|
91
92
|
SignedTx,
|
|
93
|
+
SimulateV1Payload,
|
|
94
|
+
SimulateV1Result,
|
|
92
95
|
StateOverride,
|
|
93
96
|
SubscriptionType,
|
|
94
97
|
SyncStatus,
|
|
@@ -288,6 +291,22 @@ class AsyncEth(BaseEth):
|
|
|
288
291
|
|
|
289
292
|
raise TooManyRequests("Too many CCIP read redirects")
|
|
290
293
|
|
|
294
|
+
# eth_simulateV1
|
|
295
|
+
|
|
296
|
+
_simulateV1: Method[
|
|
297
|
+
Callable[
|
|
298
|
+
[SimulateV1Payload, BlockIdentifier],
|
|
299
|
+
Awaitable[Sequence[SimulateV1Result]],
|
|
300
|
+
]
|
|
301
|
+
] = Method(RPC.eth_simulateV1)
|
|
302
|
+
|
|
303
|
+
async def simulate_v1(
|
|
304
|
+
self,
|
|
305
|
+
payload: SimulateV1Payload,
|
|
306
|
+
block_identifier: BlockIdentifier,
|
|
307
|
+
) -> Sequence[SimulateV1Result]:
|
|
308
|
+
return await self._simulateV1(payload, block_identifier)
|
|
309
|
+
|
|
291
310
|
# eth_createAccessList
|
|
292
311
|
|
|
293
312
|
_create_access_list: Method[
|
web3/eth/eth.py
CHANGED
|
@@ -85,6 +85,8 @@ from web3.types import (
|
|
|
85
85
|
MerkleProof,
|
|
86
86
|
Nonce,
|
|
87
87
|
SignedTx,
|
|
88
|
+
SimulateV1Payload,
|
|
89
|
+
SimulateV1Result,
|
|
88
90
|
StateOverride,
|
|
89
91
|
SyncStatus,
|
|
90
92
|
TxData,
|
|
@@ -270,6 +272,19 @@ class Eth(BaseEth):
|
|
|
270
272
|
|
|
271
273
|
raise TooManyRequests("Too many CCIP read redirects")
|
|
272
274
|
|
|
275
|
+
# eth_simulateV1
|
|
276
|
+
|
|
277
|
+
_simulateV1: Method[
|
|
278
|
+
Callable[[SimulateV1Payload, BlockIdentifier], Sequence[SimulateV1Result]]
|
|
279
|
+
] = Method(RPC.eth_simulateV1)
|
|
280
|
+
|
|
281
|
+
def simulate_v1(
|
|
282
|
+
self,
|
|
283
|
+
payload: SimulateV1Payload,
|
|
284
|
+
block_identifier: BlockIdentifier,
|
|
285
|
+
) -> Sequence[SimulateV1Result]:
|
|
286
|
+
return self._simulateV1(payload, block_identifier)
|
|
287
|
+
|
|
273
288
|
# eth_createAccessList
|
|
274
289
|
|
|
275
290
|
_create_access_list: Method[
|
|
@@ -158,7 +158,7 @@ def _compute_gas_price(
|
|
|
158
158
|
|
|
159
159
|
:param probabilities: An iterable of `Probability` named-tuples
|
|
160
160
|
sorted in reverse order.
|
|
161
|
-
:param desired_probability:
|
|
161
|
+
:param desired_probability: A floating point representation of the desired
|
|
162
162
|
probability. (e.g. ``85% -> 0.85``)
|
|
163
163
|
"""
|
|
164
164
|
first = probabilities[0]
|
web3/manager.py
CHANGED
|
@@ -8,7 +8,6 @@ from typing import (
|
|
|
8
8
|
Coroutine,
|
|
9
9
|
Dict,
|
|
10
10
|
List,
|
|
11
|
-
NoReturn,
|
|
12
11
|
Optional,
|
|
13
12
|
Sequence,
|
|
14
13
|
Tuple,
|
|
@@ -32,17 +31,19 @@ from web3._utils.caching import (
|
|
|
32
31
|
from web3._utils.compat import (
|
|
33
32
|
Self,
|
|
34
33
|
)
|
|
34
|
+
from web3._utils.formatters import (
|
|
35
|
+
apply_null_result_formatters,
|
|
36
|
+
)
|
|
37
|
+
from web3._utils.validation import (
|
|
38
|
+
raise_error_for_batch_response,
|
|
39
|
+
validate_rpc_response_and_raise_if_error,
|
|
40
|
+
)
|
|
35
41
|
from web3.datastructures import (
|
|
36
42
|
NamedElementOnion,
|
|
37
43
|
)
|
|
38
44
|
from web3.exceptions import (
|
|
39
|
-
BadResponseFormat,
|
|
40
|
-
MethodUnavailable,
|
|
41
45
|
ProviderConnectionError,
|
|
42
|
-
RequestTimedOut,
|
|
43
46
|
TaskNotRunning,
|
|
44
|
-
TransactionNotFound,
|
|
45
|
-
Web3RPCError,
|
|
46
47
|
Web3TypeError,
|
|
47
48
|
)
|
|
48
49
|
from web3.method import (
|
|
@@ -95,200 +96,6 @@ if TYPE_CHECKING:
|
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
NULL_RESPONSES = [None, HexBytes("0x"), "0x"]
|
|
98
|
-
KNOWN_REQUEST_TIMEOUT_MESSAGING = {
|
|
99
|
-
# Note: It's important to be very explicit here and not too broad. We don't want
|
|
100
|
-
# to accidentally catch a message that is not for a request timeout. In the worst
|
|
101
|
-
# case, we raise something more generic like `Web3RPCError`. JSON-RPC unfortunately
|
|
102
|
-
# has not standardized error codes for request timeouts.
|
|
103
|
-
"request timed out", # go-ethereum
|
|
104
|
-
}
|
|
105
|
-
METHOD_NOT_FOUND = -32601
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _raise_bad_response_format(response: RPCResponse, error: str = "") -> None:
|
|
109
|
-
message = "The response was in an unexpected format and unable to be parsed."
|
|
110
|
-
raw_response = f"The raw response is: {response}"
|
|
111
|
-
|
|
112
|
-
if error is not None and error != "":
|
|
113
|
-
error = error[:-1] if error.endswith(".") else error
|
|
114
|
-
message = f"{message} {error}. {raw_response}"
|
|
115
|
-
else:
|
|
116
|
-
message = f"{message} {raw_response}"
|
|
117
|
-
|
|
118
|
-
raise BadResponseFormat(message)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def apply_error_formatters(
|
|
122
|
-
error_formatters: Callable[..., Any],
|
|
123
|
-
response: RPCResponse,
|
|
124
|
-
) -> RPCResponse:
|
|
125
|
-
if error_formatters:
|
|
126
|
-
formatted_resp = pipe(response, error_formatters)
|
|
127
|
-
return formatted_resp
|
|
128
|
-
else:
|
|
129
|
-
return response
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def apply_null_result_formatters(
|
|
133
|
-
null_result_formatters: Callable[..., Any],
|
|
134
|
-
response: RPCResponse,
|
|
135
|
-
params: Optional[Any] = None,
|
|
136
|
-
) -> RPCResponse:
|
|
137
|
-
if null_result_formatters:
|
|
138
|
-
formatted_resp = pipe(params, null_result_formatters)
|
|
139
|
-
return formatted_resp
|
|
140
|
-
else:
|
|
141
|
-
return response
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _validate_subscription_fields(response: RPCResponse) -> None:
|
|
145
|
-
params = response["params"]
|
|
146
|
-
subscription = params["subscription"]
|
|
147
|
-
if not isinstance(subscription, str) and not len(subscription) == 34:
|
|
148
|
-
_raise_bad_response_format(
|
|
149
|
-
response, "eth_subscription 'params' must include a 'subscription' field."
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _validate_response(
|
|
154
|
-
response: RPCResponse,
|
|
155
|
-
error_formatters: Optional[Callable[..., Any]],
|
|
156
|
-
is_subscription_response: bool = False,
|
|
157
|
-
logger: Optional[logging.Logger] = None,
|
|
158
|
-
params: Optional[Any] = None,
|
|
159
|
-
) -> None:
|
|
160
|
-
if "jsonrpc" not in response or response["jsonrpc"] != "2.0":
|
|
161
|
-
_raise_bad_response_format(
|
|
162
|
-
response, 'The "jsonrpc" field must be present with a value of "2.0".'
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
response_id = response.get("id")
|
|
166
|
-
if "id" in response:
|
|
167
|
-
int_error_msg = (
|
|
168
|
-
'"id" must be an integer or a string representation of an integer.'
|
|
169
|
-
)
|
|
170
|
-
if response_id is None and "error" in response:
|
|
171
|
-
# errors can sometimes have null `id`, according to the JSON-RPC spec
|
|
172
|
-
pass
|
|
173
|
-
elif not isinstance(response_id, (str, int)):
|
|
174
|
-
_raise_bad_response_format(response, int_error_msg)
|
|
175
|
-
elif isinstance(response_id, str):
|
|
176
|
-
try:
|
|
177
|
-
int(response_id)
|
|
178
|
-
except ValueError:
|
|
179
|
-
_raise_bad_response_format(response, int_error_msg)
|
|
180
|
-
elif is_subscription_response:
|
|
181
|
-
# if `id` is not present, this must be a subscription response
|
|
182
|
-
_validate_subscription_fields(response)
|
|
183
|
-
else:
|
|
184
|
-
_raise_bad_response_format(
|
|
185
|
-
response,
|
|
186
|
-
'Response must include an "id" field or be formatted as an '
|
|
187
|
-
"`eth_subscription` response.",
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
if all(key in response for key in {"error", "result"}):
|
|
191
|
-
_raise_bad_response_format(
|
|
192
|
-
response, 'Response cannot include both "error" and "result".'
|
|
193
|
-
)
|
|
194
|
-
elif (
|
|
195
|
-
not any(key in response for key in {"error", "result"})
|
|
196
|
-
and not is_subscription_response
|
|
197
|
-
):
|
|
198
|
-
_raise_bad_response_format(
|
|
199
|
-
response, 'Response must include either "error" or "result".'
|
|
200
|
-
)
|
|
201
|
-
elif "error" in response:
|
|
202
|
-
web3_rpc_error: Optional[Web3RPCError] = None
|
|
203
|
-
error = response["error"]
|
|
204
|
-
|
|
205
|
-
# raise the error when the value is a string
|
|
206
|
-
if error is None or not isinstance(error, dict):
|
|
207
|
-
_raise_bad_response_format(
|
|
208
|
-
response,
|
|
209
|
-
'response["error"] must be a valid object as defined by the '
|
|
210
|
-
"JSON-RPC 2.0 specification.",
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
# errors must include a message
|
|
214
|
-
error_message = error.get("message")
|
|
215
|
-
if not isinstance(error_message, str):
|
|
216
|
-
_raise_bad_response_format(
|
|
217
|
-
response, 'error["message"] is required and must be a string value.'
|
|
218
|
-
)
|
|
219
|
-
elif error_message == "transaction not found":
|
|
220
|
-
transaction_hash = params[0]
|
|
221
|
-
web3_rpc_error = TransactionNotFound(
|
|
222
|
-
repr(error),
|
|
223
|
-
rpc_response=response,
|
|
224
|
-
user_message=(f"Transaction with hash {transaction_hash!r} not found."),
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
# errors must include an integer code
|
|
228
|
-
code = error.get("code")
|
|
229
|
-
if not isinstance(code, int):
|
|
230
|
-
_raise_bad_response_format(
|
|
231
|
-
response, 'error["code"] is required and must be an integer value.'
|
|
232
|
-
)
|
|
233
|
-
elif code == METHOD_NOT_FOUND:
|
|
234
|
-
web3_rpc_error = MethodUnavailable(
|
|
235
|
-
repr(error),
|
|
236
|
-
rpc_response=response,
|
|
237
|
-
user_message=(
|
|
238
|
-
"This method is not available. Check your node provider or your "
|
|
239
|
-
"client's API docs to see what methods are supported and / or "
|
|
240
|
-
"currently enabled."
|
|
241
|
-
),
|
|
242
|
-
)
|
|
243
|
-
elif any(
|
|
244
|
-
# parse specific timeout messages
|
|
245
|
-
timeout_str in error_message.lower()
|
|
246
|
-
for timeout_str in KNOWN_REQUEST_TIMEOUT_MESSAGING
|
|
247
|
-
):
|
|
248
|
-
web3_rpc_error = RequestTimedOut(
|
|
249
|
-
repr(error),
|
|
250
|
-
rpc_response=response,
|
|
251
|
-
user_message=(
|
|
252
|
-
"The request timed out. Check the connection to your node and "
|
|
253
|
-
"try again."
|
|
254
|
-
),
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
if web3_rpc_error is None:
|
|
258
|
-
# if no condition was met above, raise a more generic `Web3RPCError`
|
|
259
|
-
web3_rpc_error = Web3RPCError(repr(error), rpc_response=response)
|
|
260
|
-
|
|
261
|
-
response = apply_error_formatters(error_formatters, response)
|
|
262
|
-
logger.debug(f"RPC error response: {response}")
|
|
263
|
-
|
|
264
|
-
raise web3_rpc_error
|
|
265
|
-
|
|
266
|
-
elif "result" not in response and not is_subscription_response:
|
|
267
|
-
_raise_bad_response_format(response)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def _raise_error_for_batch_response(
|
|
271
|
-
response: RPCResponse,
|
|
272
|
-
logger: Optional[logging.Logger] = None,
|
|
273
|
-
) -> NoReturn:
|
|
274
|
-
error = response.get("error")
|
|
275
|
-
if error is None:
|
|
276
|
-
_raise_bad_response_format(
|
|
277
|
-
response,
|
|
278
|
-
"Batch response must be formatted as a list of responses or "
|
|
279
|
-
"as a single JSON-RPC error response.",
|
|
280
|
-
)
|
|
281
|
-
_validate_response(
|
|
282
|
-
response,
|
|
283
|
-
None,
|
|
284
|
-
is_subscription_response=False,
|
|
285
|
-
logger=logger,
|
|
286
|
-
params=[],
|
|
287
|
-
)
|
|
288
|
-
# This should not be reached, but if it is, raise a generic `BadResponseFormat`
|
|
289
|
-
raise BadResponseFormat(
|
|
290
|
-
"Batch response was in an unexpected format and unable to be parsed."
|
|
291
|
-
)
|
|
292
99
|
|
|
293
100
|
|
|
294
101
|
class RequestManager:
|
|
@@ -388,7 +195,7 @@ class RequestManager:
|
|
|
388
195
|
and response["params"].get("result") is not None
|
|
389
196
|
)
|
|
390
197
|
|
|
391
|
-
|
|
198
|
+
validate_rpc_response_and_raise_if_error(
|
|
392
199
|
response,
|
|
393
200
|
error_formatters,
|
|
394
201
|
is_subscription_response=is_subscription_response,
|
|
@@ -447,6 +254,8 @@ class RequestManager:
|
|
|
447
254
|
"""
|
|
448
255
|
Context manager for making batch requests
|
|
449
256
|
"""
|
|
257
|
+
if isinstance(self.provider, AutoProvider):
|
|
258
|
+
self.provider = self.provider._get_active_provider(use_cache=True)
|
|
450
259
|
if not isinstance(self.provider, (AsyncJSONBaseProvider, JSONBaseProvider)):
|
|
451
260
|
raise Web3TypeError("Batch requests are not supported by this provider.")
|
|
452
261
|
return RequestBatcher(self.w3)
|
|
@@ -477,7 +286,7 @@ class RequestManager:
|
|
|
477
286
|
return list(formatted_responses)
|
|
478
287
|
else:
|
|
479
288
|
# expect a single response with an error
|
|
480
|
-
|
|
289
|
+
raise_error_for_batch_response(response, self.logger)
|
|
481
290
|
|
|
482
291
|
async def _async_make_batch_request(
|
|
483
292
|
self,
|
|
@@ -520,7 +329,7 @@ class RequestManager:
|
|
|
520
329
|
return list(formatted_responses)
|
|
521
330
|
else:
|
|
522
331
|
# expect a single response with an error
|
|
523
|
-
|
|
332
|
+
raise_error_for_batch_response(response, self.logger)
|
|
524
333
|
|
|
525
334
|
def _format_batched_response(
|
|
526
335
|
self,
|
|
@@ -528,7 +337,7 @@ class RequestManager:
|
|
|
528
337
|
response: RPCResponse,
|
|
529
338
|
) -> RPCResponse:
|
|
530
339
|
result_formatters, error_formatters, null_result_formatters = requests_info[1]
|
|
531
|
-
|
|
340
|
+
validate_rpc_response_and_raise_if_error(
|
|
532
341
|
response,
|
|
533
342
|
error_formatters,
|
|
534
343
|
is_subscription_response=False,
|
|
@@ -33,6 +33,9 @@ from web3._utils.caching.caching_utils import (
|
|
|
33
33
|
async_handle_recv_caching,
|
|
34
34
|
async_handle_send_caching,
|
|
35
35
|
)
|
|
36
|
+
from web3._utils.validation import (
|
|
37
|
+
validate_rpc_response_and_raise_if_error,
|
|
38
|
+
)
|
|
36
39
|
from web3.exceptions import (
|
|
37
40
|
PersistentConnectionClosedOK,
|
|
38
41
|
ProviderConnectionError,
|
|
@@ -302,6 +305,27 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
|
|
|
302
305
|
TaskNotRunning(message_listener_task)
|
|
303
306
|
)
|
|
304
307
|
|
|
308
|
+
def _raise_stray_errors_from_cache(self) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Check the request response cache for any errors not tied to current requests
|
|
311
|
+
and raise them if found.
|
|
312
|
+
"""
|
|
313
|
+
if not self._is_batching:
|
|
314
|
+
for (
|
|
315
|
+
response
|
|
316
|
+
) in self._request_processor._request_response_cache._data.values():
|
|
317
|
+
request = (
|
|
318
|
+
self._request_processor._request_information_cache.get_cache_entry(
|
|
319
|
+
generate_cache_key(response["id"])
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
if "error" in response and request is None:
|
|
323
|
+
# if we find an error response in the cache without a corresponding
|
|
324
|
+
# request, raise the error
|
|
325
|
+
validate_rpc_response_and_raise_if_error(
|
|
326
|
+
response, None, logger=self.logger
|
|
327
|
+
)
|
|
328
|
+
|
|
305
329
|
async def _message_listener(self) -> None:
|
|
306
330
|
self.logger.info(
|
|
307
331
|
f"{self.__class__.__qualname__} listener background task started. Storing "
|
|
@@ -327,6 +351,7 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
|
|
|
327
351
|
await self._request_processor.cache_raw_response(
|
|
328
352
|
response, subscription=subscription
|
|
329
353
|
)
|
|
354
|
+
self._raise_stray_errors_from_cache()
|
|
330
355
|
except PersistentConnectionClosedOK as e:
|
|
331
356
|
self.logger.info(
|
|
332
357
|
"Message listener background task has ended gracefully: "
|
|
@@ -376,6 +401,10 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
|
|
|
376
401
|
request_cache_key = generate_cache_key(request_id)
|
|
377
402
|
|
|
378
403
|
while True:
|
|
404
|
+
# check if an exception was recorded in the listener task and raise
|
|
405
|
+
# it in the main loop if so
|
|
406
|
+
self._handle_listener_task_exceptions()
|
|
407
|
+
|
|
379
408
|
if request_cache_key in self._request_processor._request_response_cache:
|
|
380
409
|
self.logger.debug(
|
|
381
410
|
f"Popping response for id {request_id} from cache."
|
|
@@ -385,9 +414,6 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
|
|
|
385
414
|
)
|
|
386
415
|
return popped_response
|
|
387
416
|
else:
|
|
388
|
-
# check if an exception was recorded in the listener task and raise
|
|
389
|
-
# it in the main loop if so
|
|
390
|
-
self._handle_listener_task_exceptions()
|
|
391
417
|
await asyncio.sleep(0)
|
|
392
418
|
|
|
393
419
|
try:
|
|
@@ -206,7 +206,10 @@ class SubscriptionManager:
|
|
|
206
206
|
raise Web3ValueError("No subscriptions provided.")
|
|
207
207
|
|
|
208
208
|
unsubscribed: List[bool] = []
|
|
209
|
-
|
|
209
|
+
# re-create the subscription list to prevent modifying the original list
|
|
210
|
+
# in case ``subscription_manager.subscriptions`` was passed in directly
|
|
211
|
+
subs = [sub for sub in subscriptions]
|
|
212
|
+
for sub in subs:
|
|
210
213
|
if isinstance(sub, str):
|
|
211
214
|
sub = HexStr(sub)
|
|
212
215
|
unsubscribed.append(await self.unsubscribe(sub))
|
|
@@ -226,7 +229,9 @@ class SubscriptionManager:
|
|
|
226
229
|
:rtype: bool
|
|
227
230
|
"""
|
|
228
231
|
unsubscribed = [
|
|
229
|
-
await self.unsubscribe(sub)
|
|
232
|
+
await self.unsubscribe(sub)
|
|
233
|
+
# use copy to prevent modifying the list while iterating over it
|
|
234
|
+
for sub in self.subscriptions.copy()
|
|
230
235
|
]
|
|
231
236
|
if all(unsubscribed):
|
|
232
237
|
self.logger.info("Successfully unsubscribed from all subscriptions.")
|
|
@@ -63,6 +63,8 @@ class WebSocketProvider(PersistentConnectionProvider):
|
|
|
63
63
|
self,
|
|
64
64
|
endpoint_uri: Optional[Union[URI, str]] = None,
|
|
65
65
|
websocket_kwargs: Optional[Dict[str, Any]] = None,
|
|
66
|
+
# uses binary frames by default
|
|
67
|
+
use_text_frames: Optional[bool] = False,
|
|
66
68
|
# `PersistentConnectionProvider` kwargs can be passed through
|
|
67
69
|
**kwargs: Any,
|
|
68
70
|
) -> None:
|
|
@@ -71,6 +73,7 @@ class WebSocketProvider(PersistentConnectionProvider):
|
|
|
71
73
|
URI(endpoint_uri) if endpoint_uri is not None else get_default_endpoint()
|
|
72
74
|
)
|
|
73
75
|
super().__init__(**kwargs)
|
|
76
|
+
self.use_text_frames = use_text_frames
|
|
74
77
|
self._ws: Optional[WebSocketClientProtocol] = None
|
|
75
78
|
|
|
76
79
|
if not any(
|
|
@@ -118,9 +121,11 @@ class WebSocketProvider(PersistentConnectionProvider):
|
|
|
118
121
|
"Connection to websocket has not been initiated for the provider."
|
|
119
122
|
)
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
payload: Union[bytes, str] = request_data
|
|
125
|
+
if self.use_text_frames:
|
|
126
|
+
payload = request_data.decode("utf-8")
|
|
127
|
+
|
|
128
|
+
await asyncio.wait_for(self._ws.send(payload), timeout=self.request_timeout)
|
|
124
129
|
|
|
125
130
|
async def socket_recv(self) -> RPCResponse:
|
|
126
131
|
raw_response = await self._ws.recv()
|