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.
Files changed (42) hide show
  1. web3/_utils/contract_sources/contract_data/ambiguous_function_contract.py +3 -3
  2. web3/_utils/contract_sources/contract_data/arrays_contract.py +3 -3
  3. web3/_utils/contract_sources/contract_data/bytes_contracts.py +5 -5
  4. web3/_utils/contract_sources/contract_data/constructor_contracts.py +7 -7
  5. web3/_utils/contract_sources/contract_data/contract_caller_tester.py +3 -3
  6. web3/_utils/contract_sources/contract_data/emitter_contract.py +3 -3
  7. web3/_utils/contract_sources/contract_data/event_contracts.py +7 -7
  8. web3/_utils/contract_sources/contract_data/extended_resolver.py +3 -3
  9. web3/_utils/contract_sources/contract_data/fallback_function_contract.py +3 -3
  10. web3/_utils/contract_sources/contract_data/function_name_tester_contract.py +3 -3
  11. web3/_utils/contract_sources/contract_data/math_contract.py +3 -3
  12. web3/_utils/contract_sources/contract_data/offchain_lookup.py +3 -3
  13. web3/_utils/contract_sources/contract_data/offchain_resolver.py +3 -3
  14. web3/_utils/contract_sources/contract_data/panic_errors_contract.py +3 -3
  15. web3/_utils/contract_sources/contract_data/payable_tester.py +3 -3
  16. web3/_utils/contract_sources/contract_data/receive_function_contracts.py +5 -5
  17. web3/_utils/contract_sources/contract_data/reflector_contracts.py +3 -3
  18. web3/_utils/contract_sources/contract_data/revert_contract.py +3 -3
  19. web3/_utils/contract_sources/contract_data/simple_resolver.py +3 -3
  20. web3/_utils/contract_sources/contract_data/storage_contract.py +3 -3
  21. web3/_utils/contract_sources/contract_data/string_contract.py +3 -3
  22. web3/_utils/contract_sources/contract_data/tuple_contracts.py +5 -5
  23. web3/_utils/error_formatters_utils.py +1 -1
  24. web3/_utils/formatters.py +28 -0
  25. web3/_utils/method_formatters.py +121 -20
  26. web3/_utils/module_testing/eth_module.py +99 -2
  27. web3/_utils/module_testing/persistent_connection_provider.py +1 -1
  28. web3/_utils/rpc_abi.py +1 -0
  29. web3/_utils/validation.py +191 -0
  30. web3/eth/async_eth.py +19 -0
  31. web3/eth/eth.py +15 -0
  32. web3/gas_strategies/time_based.py +1 -1
  33. web3/manager.py +13 -204
  34. web3/providers/persistent/persistent.py +29 -3
  35. web3/providers/persistent/subscription_manager.py +7 -2
  36. web3/providers/persistent/websocket.py +8 -3
  37. web3/types.py +26 -2
  38. {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/METADATA +11 -7
  39. {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/RECORD +42 -42
  40. {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/WHEEL +1 -1
  41. {web3-7.8.0.dist-info → web3-7.9.0.dist-info}/LICENSE +0 -0
  42. {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: An floating point representation of the desired
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
- _validate_response(
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
- _raise_error_for_batch_response(response, self.logger)
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
- _raise_error_for_batch_response(response, self.logger)
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
- _validate_response(
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
- for sub in subscriptions:
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) for sub in self.subscriptions.copy()
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
- await asyncio.wait_for(
122
- self._ws.send(request_data), timeout=self.request_timeout
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()