web3 7.7.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 (61) hide show
  1. ens/async_ens.py +1 -1
  2. ens/ens.py +1 -1
  3. web3/_utils/contract_sources/contract_data/ambiguous_function_contract.py +3 -3
  4. web3/_utils/contract_sources/contract_data/arrays_contract.py +3 -3
  5. web3/_utils/contract_sources/contract_data/bytes_contracts.py +5 -5
  6. web3/_utils/contract_sources/contract_data/constructor_contracts.py +7 -7
  7. web3/_utils/contract_sources/contract_data/contract_caller_tester.py +3 -3
  8. web3/_utils/contract_sources/contract_data/emitter_contract.py +3 -3
  9. web3/_utils/contract_sources/contract_data/event_contracts.py +7 -7
  10. web3/_utils/contract_sources/contract_data/extended_resolver.py +3 -3
  11. web3/_utils/contract_sources/contract_data/fallback_function_contract.py +3 -3
  12. web3/_utils/contract_sources/contract_data/function_name_tester_contract.py +3 -3
  13. web3/_utils/contract_sources/contract_data/math_contract.py +3 -3
  14. web3/_utils/contract_sources/contract_data/offchain_lookup.py +3 -3
  15. web3/_utils/contract_sources/contract_data/offchain_resolver.py +3 -3
  16. web3/_utils/contract_sources/contract_data/panic_errors_contract.py +3 -3
  17. web3/_utils/contract_sources/contract_data/payable_tester.py +3 -3
  18. web3/_utils/contract_sources/contract_data/receive_function_contracts.py +5 -5
  19. web3/_utils/contract_sources/contract_data/reflector_contracts.py +3 -3
  20. web3/_utils/contract_sources/contract_data/revert_contract.py +3 -3
  21. web3/_utils/contract_sources/contract_data/simple_resolver.py +3 -3
  22. web3/_utils/contract_sources/contract_data/storage_contract.py +3 -3
  23. web3/_utils/contract_sources/contract_data/string_contract.py +3 -3
  24. web3/_utils/contract_sources/contract_data/tuple_contracts.py +5 -5
  25. web3/_utils/error_formatters_utils.py +1 -1
  26. web3/_utils/events.py +1 -1
  27. web3/_utils/formatters.py +28 -0
  28. web3/_utils/http_session_manager.py +19 -1
  29. web3/_utils/method_formatters.py +121 -20
  30. web3/_utils/module_testing/eth_module.py +102 -2
  31. web3/_utils/module_testing/module_testing_utils.py +0 -42
  32. web3/_utils/module_testing/persistent_connection_provider.py +39 -1
  33. web3/_utils/rpc_abi.py +1 -0
  34. web3/_utils/validation.py +191 -0
  35. web3/beacon/api_endpoints.py +10 -0
  36. web3/beacon/async_beacon.py +47 -0
  37. web3/beacon/beacon.py +45 -0
  38. web3/contract/async_contract.py +2 -206
  39. web3/contract/base_contract.py +208 -12
  40. web3/contract/contract.py +2 -205
  41. web3/datastructures.py +4 -0
  42. web3/eth/async_eth.py +19 -0
  43. web3/eth/eth.py +15 -0
  44. web3/gas_strategies/time_based.py +1 -1
  45. web3/manager.py +47 -194
  46. web3/middleware/base.py +14 -6
  47. web3/providers/async_base.py +5 -4
  48. web3/providers/base.py +3 -3
  49. web3/providers/persistent/persistent.py +29 -3
  50. web3/providers/persistent/request_processor.py +11 -1
  51. web3/providers/persistent/subscription_manager.py +116 -46
  52. web3/providers/persistent/websocket.py +8 -3
  53. web3/providers/rpc/async_rpc.py +8 -3
  54. web3/providers/rpc/rpc.py +8 -3
  55. web3/types.py +36 -13
  56. web3/utils/subscriptions.py +5 -1
  57. {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/LICENSE +1 -1
  58. {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/METADATA +11 -7
  59. {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/RECORD +61 -61
  60. {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/WHEEL +1 -1
  61. {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
+ import pytest
1
2
  import asyncio
2
3
  import json
3
4
  import math
4
- import pytest
5
5
  from random import (
6
6
  randint,
7
7
  )
@@ -1690,6 +1690,52 @@ class AsyncEthModuleTest:
1690
1690
  with pytest.raises(TooManyRequests, match="Too many CCIP read redirects"):
1691
1691
  await async_offchain_lookup_contract.caller().continuousOffchainLookup() # noqa: E501 type: ignore
1692
1692
 
1693
+ @pytest.mark.asyncio
1694
+ async def test_eth_simulate_v1(self, async_w3: "AsyncWeb3") -> None:
1695
+ simulate_result = await async_w3.eth.simulate_v1(
1696
+ {
1697
+ "blockStateCalls": [
1698
+ {
1699
+ "blockOverrides": {
1700
+ "baseFeePerGas": Wei(10),
1701
+ },
1702
+ "stateOverrides": {
1703
+ "0xc100000000000000000000000000000000000000": {
1704
+ "balance": Wei(500000000),
1705
+ }
1706
+ },
1707
+ "calls": [
1708
+ {
1709
+ "from": "0xc100000000000000000000000000000000000000",
1710
+ "to": "0xc100000000000000000000000000000000000000",
1711
+ "maxFeePerGas": Wei(10),
1712
+ "maxPriorityFeePerGas": Wei(10),
1713
+ }
1714
+ ],
1715
+ }
1716
+ ],
1717
+ "validation": True,
1718
+ "traceTransfers": True,
1719
+ },
1720
+ "latest",
1721
+ )
1722
+
1723
+ assert len(simulate_result) == 1
1724
+
1725
+ result = simulate_result[0]
1726
+ assert result.get("baseFeePerGas") == 10
1727
+
1728
+ calls_result = result.get("calls")
1729
+ assert calls_result is not None
1730
+ assert len(calls_result) == 1
1731
+ call_entry = calls_result[0]
1732
+
1733
+ assert all(
1734
+ key in call_entry for key in ("returnData", "logs", "gasUsed", "status")
1735
+ )
1736
+ assert call_entry["status"] == 1
1737
+ assert call_entry["gasUsed"] == int("0x5208", 16)
1738
+
1693
1739
  @pytest.mark.asyncio
1694
1740
  async def test_async_eth_chain_id(self, async_w3: "AsyncWeb3") -> None:
1695
1741
  chain_id = await async_w3.eth.chain_id
@@ -2283,6 +2329,9 @@ class AsyncEthModuleTest:
2283
2329
  async_w3: "AsyncWeb3",
2284
2330
  async_keyfile_account_address_dual_type: ChecksumAddress,
2285
2331
  ) -> None:
2332
+ # Note: `underpriced transaction` error is only consistent with
2333
+ # ``txpool.nolocals`` flag as of Geth ``v1.15.4``.
2334
+ # https://github.com/ethereum/web3.py/pull/3636
2286
2335
  txn_params: TxParams = {
2287
2336
  "from": async_keyfile_account_address_dual_type,
2288
2337
  "to": async_keyfile_account_address_dual_type,
@@ -2388,6 +2437,7 @@ class AsyncEthModuleTest:
2388
2437
  with pytest.raises(Web3ValueError):
2389
2438
  await async_w3.eth.replace_transaction(txn_hash, txn_params)
2390
2439
 
2440
+ @flaky_geth_dev_mining
2391
2441
  @pytest.mark.asyncio
2392
2442
  async def test_async_eth_replace_transaction_gas_price_defaulting_minimum(
2393
2443
  self, async_w3: "AsyncWeb3", async_keyfile_account_address: ChecksumAddress
@@ -2411,6 +2461,7 @@ class AsyncEthModuleTest:
2411
2461
  gas_price * 1.125
2412
2462
  ) # minimum gas price
2413
2463
 
2464
+ @flaky_geth_dev_mining
2414
2465
  @pytest.mark.asyncio
2415
2466
  async def test_async_eth_replace_transaction_gas_price_defaulting_strategy_higher(
2416
2467
  self, async_w3: "AsyncWeb3", async_keyfile_account_address: ChecksumAddress
@@ -2439,6 +2490,7 @@ class AsyncEthModuleTest:
2439
2490
  ) # Strategy provides higher gas price
2440
2491
  async_w3.eth.set_gas_price_strategy(None) # reset strategy
2441
2492
 
2493
+ @flaky_geth_dev_mining
2442
2494
  @pytest.mark.asyncio
2443
2495
  async def test_async_eth_replace_transaction_gas_price_defaulting_strategy_lower(
2444
2496
  self, async_w3: "AsyncWeb3", async_keyfile_account_address: ChecksumAddress
@@ -3479,6 +3531,9 @@ class EthModuleTest:
3479
3531
  def test_eth_replace_transaction_underpriced(
3480
3532
  self, w3: "Web3", keyfile_account_address_dual_type: ChecksumAddress
3481
3533
  ) -> None:
3534
+ # Note: `underpriced transaction` error is only consistent with
3535
+ # ``txpool.nolocals`` flag as of Geth ``v1.15.4``.
3536
+ # https://github.com/ethereum/web3.py/pull/3636
3482
3537
  txn_params: TxParams = {
3483
3538
  "from": keyfile_account_address_dual_type,
3484
3539
  "to": keyfile_account_address_dual_type,
@@ -3818,7 +3873,7 @@ class EthModuleTest:
3818
3873
  math_contract: "Contract",
3819
3874
  params: StateOverrideParams,
3820
3875
  ) -> None:
3821
- txn_params: TxParams = {"from": w3.eth.accounts[0]}
3876
+ txn_params: TxParams = {"from": w3.eth.accounts[0], "to": math_contract.address}
3822
3877
 
3823
3878
  # assert does not raise
3824
3879
  w3.eth.call(txn_params, "latest", {math_contract.address: params})
@@ -3911,6 +3966,51 @@ class EthModuleTest:
3911
3966
  w3.eth.call(txn_params)
3912
3967
  assert excinfo.value.data == data
3913
3968
 
3969
+ def test_eth_simulate_v1(self, w3: "Web3") -> None:
3970
+ simulate_result = w3.eth.simulate_v1(
3971
+ {
3972
+ "blockStateCalls": [
3973
+ {
3974
+ "blockOverrides": {
3975
+ "baseFeePerGas": Wei(10),
3976
+ },
3977
+ "stateOverrides": {
3978
+ "0xc100000000000000000000000000000000000000": {
3979
+ "balance": Wei(500000000),
3980
+ }
3981
+ },
3982
+ "calls": [
3983
+ {
3984
+ "from": "0xc100000000000000000000000000000000000000",
3985
+ "to": "0xc100000000000000000000000000000000000000",
3986
+ "maxFeePerGas": Wei(10),
3987
+ "maxPriorityFeePerGas": Wei(10),
3988
+ }
3989
+ ],
3990
+ }
3991
+ ],
3992
+ "validation": True,
3993
+ "traceTransfers": True,
3994
+ },
3995
+ "latest",
3996
+ )
3997
+
3998
+ assert len(simulate_result) == 1
3999
+
4000
+ result = simulate_result[0]
4001
+ assert result.get("baseFeePerGas") == 10
4002
+
4003
+ calls_result = result.get("calls")
4004
+ assert calls_result is not None
4005
+ assert len(calls_result) == 1
4006
+ call_entry = calls_result[0]
4007
+
4008
+ assert all(
4009
+ key in call_entry for key in ("returnData", "logs", "gasUsed", "status")
4010
+ )
4011
+ assert call_entry["status"] == 1
4012
+ assert call_entry["gasUsed"] == int("0x5208", 16)
4013
+
3914
4014
  @pytest.mark.parametrize(
3915
4015
  "panic_error,params",
3916
4016
  (
@@ -1,17 +1,12 @@
1
1
  import asyncio
2
- import functools
3
- import pytest
4
2
  from typing import (
5
3
  TYPE_CHECKING,
6
4
  Any,
7
- Callable,
8
5
  Collection,
9
6
  Dict,
10
7
  Generator,
11
8
  Literal,
12
9
  Sequence,
13
- Tuple,
14
- Type,
15
10
  Union,
16
11
  )
17
12
 
@@ -63,43 +58,6 @@ due to timing of the test running as a block is mined.
63
58
  flaky_geth_dev_mining = flaky(max_runs=3, min_passes=1)
64
59
 
65
60
 
66
- def flaky_with_xfail_on_exception(
67
- reason: str,
68
- exception: Union[Type[Exception], Tuple[Type[Exception], ...]],
69
- max_runs: int = 3,
70
- min_passes: int = 1,
71
- ) -> Callable[[Any], Any]:
72
- """
73
- Some tests inconsistently fail hard with a particular exception and retrying
74
- these tests often times does not get them "unstuck". If we've exhausted all flaky
75
- retries and this expected exception is raised, `xfail` the test with the given
76
- reason.
77
- """
78
- runs = max_runs
79
-
80
- def decorator(func: Any) -> Any:
81
- @flaky(max_runs=max_runs, min_passes=min_passes)
82
- @functools.wraps(func)
83
- async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
84
- nonlocal runs
85
- try:
86
- return await func(self, *args, **kwargs)
87
- except exception:
88
- # xfail the test only if the exception is raised and we have exhausted
89
- # all flaky retries
90
- if runs == 1:
91
- pytest.xfail(reason)
92
- runs -= 1
93
- pytest.fail(f"xfailed but {runs} run(s) remaining with flaky...")
94
- except Exception as e:
95
- # let flaky handle it
96
- raise e
97
-
98
- return wrapper
99
-
100
- return decorator
101
-
102
-
103
61
  def assert_contains_log(
104
62
  result: Sequence[LogReceipt],
105
63
  block_with_txn_with_log: BlockData,
@@ -1,8 +1,8 @@
1
+ import pytest
1
2
  import asyncio
2
3
  from dataclasses import (
3
4
  dataclass,
4
5
  )
5
- import pytest
6
6
  from typing import (
7
7
  TYPE_CHECKING,
8
8
  Any,
@@ -876,3 +876,41 @@ class PersistentConnectionProviderTest:
876
876
 
877
877
  assert_no_subscriptions_left(sub_manager._subscription_container)
878
878
  await clean_up_task(unsubscribe_task)
879
+
880
+ @pytest.mark.asyncio
881
+ async def test_run_forever_starts_with_0_subs_and_runs_until_task_cancelled(
882
+ self, async_w3: AsyncWeb3
883
+ ) -> None:
884
+ sub_manager = async_w3.subscription_manager
885
+ assert_no_subscriptions_left(sub_manager._subscription_container)
886
+
887
+ run_forever_task = asyncio.create_task(
888
+ sub_manager.handle_subscriptions(run_forever=True)
889
+ )
890
+
891
+ await asyncio.sleep(0.1)
892
+ assert run_forever_task.done() is False
893
+ assert sub_manager.subscriptions == []
894
+
895
+ # subscribe to newHeads and validate it
896
+ new_heads_handler_test = SubscriptionHandlerTest()
897
+ sub1 = NewHeadsSubscription(
898
+ label="foo",
899
+ handler=new_heads_handler,
900
+ handler_context={"new_heads_handler_test": new_heads_handler_test},
901
+ )
902
+ sub_id = await sub_manager.subscribe(sub1)
903
+ assert is_hexstr(sub_id)
904
+ assert len(sub_manager.subscriptions) == 1
905
+ assert sub_manager.subscriptions[0] == sub1
906
+
907
+ # wait for the handler to unsubscribe
908
+ while sub_manager.subscriptions:
909
+ await asyncio.sleep(0.1)
910
+
911
+ assert new_heads_handler_test.passed
912
+ assert run_forever_task.done() is False
913
+ assert run_forever_task.cancelled() is False
914
+
915
+ # cleanup
916
+ await clean_up_task(run_forever_task)
web3/_utils/rpc_abi.py CHANGED
@@ -51,6 +51,7 @@ class RPC:
51
51
  eth_blobBaseFee = RPCEndpoint("eth_blobBaseFee")
52
52
  eth_blockNumber = RPCEndpoint("eth_blockNumber")
53
53
  eth_call = RPCEndpoint("eth_call")
54
+ eth_simulateV1 = RPCEndpoint("eth_simulateV1")
54
55
  eth_createAccessList = RPCEndpoint("eth_createAccessList")
55
56
  eth_chainId = RPCEndpoint("eth_chainId")
56
57
  eth_estimateGas = RPCEndpoint("eth_estimateGas")
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)
@@ -59,6 +59,16 @@ GET_BEACON_HEADS = "/eth/v1/debug/beacon/heads"
59
59
  GET_NODE_IDENTITY = "/eth/v1/node/identity"
60
60
  GET_PEERS = "/eth/v1/node/peers"
61
61
  GET_PEER = "/eth/v1/node/peers/{0}"
62
+ GET_PEER_COUNT = "/eth/v1/node/peer_count"
62
63
  GET_HEALTH = "/eth/v1/node/health"
63
64
  GET_VERSION = "/eth/v1/node/version"
64
65
  GET_SYNCING = "/eth/v1/node/syncing"
66
+
67
+ # [ VALIDATOR endpoints ]
68
+
69
+ GET_ATTESTER_DUTIES = "/eth/v1/validator/duties/attester/{0}"
70
+ GET_BLOCK_PROPOSERS_DUTIES = "/eth/v1/validator/duties/proposer/{0}"
71
+ GET_SYNC_COMMITTEE_DUTIES = "/eth/v1/validator/duties/sync/{0}"
72
+
73
+ # [ REWARDS endpoints ]
74
+ GET_ATTESTATIONS_REWARDS = "/eth/v1/beacon/rewards/attestations/{0}"
@@ -3,6 +3,7 @@ from typing import (
3
3
  Dict,
4
4
  List,
5
5
  Optional,
6
+ Union,
6
7
  )
7
8
 
8
9
  from aiohttp import (
@@ -18,6 +19,8 @@ from web3._utils.http_session_manager import (
18
19
  )
19
20
  from web3.beacon.api_endpoints import (
20
21
  GET_ATTESTATIONS,
22
+ GET_ATTESTATIONS_REWARDS,
23
+ GET_ATTESTER_DUTIES,
21
24
  GET_ATTESTER_SLASHINGS,
22
25
  GET_BEACON_HEADS,
23
26
  GET_BEACON_STATE,
@@ -27,6 +30,7 @@ from web3.beacon.api_endpoints import (
27
30
  GET_BLOCK_ATTESTATIONS,
28
31
  GET_BLOCK_HEADER,
29
32
  GET_BLOCK_HEADERS,
33
+ GET_BLOCK_PROPOSERS_DUTIES,
30
34
  GET_BLOCK_ROOT,
31
35
  GET_BLS_TO_EXECUTION_CHANGES,
32
36
  GET_DEPOSIT_CONTRACT,
@@ -45,10 +49,12 @@ from web3.beacon.api_endpoints import (
45
49
  GET_LIGHT_CLIENT_UPDATES,
46
50
  GET_NODE_IDENTITY,
47
51
  GET_PEER,
52
+ GET_PEER_COUNT,
48
53
  GET_PEERS,
49
54
  GET_PROPOSER_SLASHINGS,
50
55
  GET_REWARDS,
51
56
  GET_SPEC,
57
+ GET_SYNC_COMMITTEE_DUTIES,
52
58
  GET_SYNCING,
53
59
  GET_VALIDATOR,
54
60
  GET_VALIDATOR_BALANCES,
@@ -78,6 +84,14 @@ class AsyncBeacon:
78
84
  uri, params=params, timeout=ClientTimeout(self.request_timeout)
79
85
  )
80
86
 
87
+ async def _async_make_post_request(
88
+ self, endpoint_uri: str, body: Union[List[str], Dict[str, Any]]
89
+ ) -> Dict[str, Any]:
90
+ uri = URI(self.base_url + endpoint_uri)
91
+ return await self._request_session_manager.async_json_make_post_request(
92
+ uri, json=body, timeout=self.request_timeout
93
+ )
94
+
81
95
  # [ BEACON endpoints ]
82
96
 
83
97
  # states
@@ -216,6 +230,9 @@ class AsyncBeacon:
216
230
  async def get_peer(self, peer_id: str) -> Dict[str, Any]:
217
231
  return await self._async_make_get_request(GET_PEER.format(peer_id))
218
232
 
233
+ async def get_peer_count(self) -> Dict[str, Any]:
234
+ return await self._async_make_get_request(GET_PEER_COUNT)
235
+
219
236
  async def get_health(self) -> int:
220
237
  url = URI(self.base_url + GET_HEALTH)
221
238
  response = (
@@ -239,3 +256,33 @@ class AsyncBeacon:
239
256
  GET_BLOB_SIDECARS.format(block_id),
240
257
  params=indices_param,
241
258
  )
259
+
260
+ # [ VALIDATOR endpoints ]
261
+
262
+ async def get_attester_duties(
263
+ self, epoch: str, validator_indices: List[str]
264
+ ) -> Dict[str, Any]:
265
+ return await self._async_make_post_request(
266
+ GET_ATTESTER_DUTIES.format(epoch), validator_indices
267
+ )
268
+
269
+ async def get_block_proposer_duties(self, epoch: str) -> Dict[str, Any]:
270
+ return await self._async_make_get_request(
271
+ GET_BLOCK_PROPOSERS_DUTIES.format(epoch)
272
+ )
273
+
274
+ async def get_sync_committee_duties(
275
+ self, epoch: str, validator_indices: List[str]
276
+ ) -> Dict[str, Any]:
277
+ return await self._async_make_post_request(
278
+ GET_SYNC_COMMITTEE_DUTIES.format(epoch), validator_indices
279
+ )
280
+
281
+ # [ REWARDS endpoints ]
282
+
283
+ async def get_attestations_rewards(
284
+ self, epoch: str, validator_indices: List[str]
285
+ ) -> Dict[str, Any]:
286
+ return await self._async_make_post_request(
287
+ GET_ATTESTATIONS_REWARDS.format(epoch), validator_indices
288
+ )