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.
- ens/async_ens.py +1 -1
- ens/ens.py +1 -1
- 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/events.py +1 -1
- web3/_utils/formatters.py +28 -0
- web3/_utils/http_session_manager.py +19 -1
- web3/_utils/method_formatters.py +121 -20
- web3/_utils/module_testing/eth_module.py +102 -2
- web3/_utils/module_testing/module_testing_utils.py +0 -42
- web3/_utils/module_testing/persistent_connection_provider.py +39 -1
- web3/_utils/rpc_abi.py +1 -0
- web3/_utils/validation.py +191 -0
- web3/beacon/api_endpoints.py +10 -0
- web3/beacon/async_beacon.py +47 -0
- web3/beacon/beacon.py +45 -0
- web3/contract/async_contract.py +2 -206
- web3/contract/base_contract.py +208 -12
- web3/contract/contract.py +2 -205
- web3/datastructures.py +4 -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 +47 -194
- web3/middleware/base.py +14 -6
- web3/providers/async_base.py +5 -4
- web3/providers/base.py +3 -3
- web3/providers/persistent/persistent.py +29 -3
- web3/providers/persistent/request_processor.py +11 -1
- web3/providers/persistent/subscription_manager.py +116 -46
- web3/providers/persistent/websocket.py +8 -3
- web3/providers/rpc/async_rpc.py +8 -3
- web3/providers/rpc/rpc.py +8 -3
- web3/types.py +36 -13
- web3/utils/subscriptions.py +5 -1
- {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/LICENSE +1 -1
- {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/METADATA +11 -7
- {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/RECORD +61 -61
- {web3-7.7.0.dist-info → web3-7.9.0.dist-info}/WHEEL +1 -1
- {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)
|
web3/beacon/api_endpoints.py
CHANGED
|
@@ -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}"
|
web3/beacon/async_beacon.py
CHANGED
|
@@ -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
|
+
)
|