From d722f3107612a4b713d8ad360a3ca2d637964721 Mon Sep 17 00:00:00 2001 From: Michael Tang Date: Thu, 5 Aug 2021 01:11:04 +0800 Subject: [PATCH] Add eth_feeHistory RPC endpoint (#2094) * add fee_history to eth module * add async support, add tests, make reward_percentiles argument optional --- newsfragments/2038.feature.rst | 1 + tests/integration/test_ethereum_tester.py | 12 +++++ web3/_utils/method_formatters.py | 12 +++++ web3/_utils/module_testing/eth_module.py | 57 +++++++++++++++++++++++ web3/_utils/rpc_abi.py | 1 + web3/eth.py | 24 ++++++++++ web3/providers/eth_tester/defaults.py | 1 + web3/types.py | 7 +++ 8 files changed, 115 insertions(+) create mode 100644 newsfragments/2038.feature.rst diff --git a/newsfragments/2038.feature.rst b/newsfragments/2038.feature.rst new file mode 100644 index 0000000000..a6685e2b5e --- /dev/null +++ b/newsfragments/2038.feature.rst @@ -0,0 +1 @@ +Add support for eth_feeHistory RPC method diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py index 7dac70280f..213c8a6e0e 100644 --- a/tests/integration/test_ethereum_tester.py +++ b/tests/integration/test_ethereum_tester.py @@ -283,6 +283,18 @@ def test_eth_getBlockByHash_pending( block = web3.eth.get_block('pending') assert block['hash'] is not None + @pytest.mark.xfail(reason='eth_feeHistory is not implemented on eth-tester') + def test_eth_fee_history(self, web3: "Web3"): + super().test_eth_fee_history(web3) + + @pytest.mark.xfail(reason='eth_feeHistory is not implemented on eth-tester') + def test_eth_fee_history_with_integer(self, web3: "Web3"): + super().test_eth_fee_history_with_integer(web3) + + @pytest.mark.xfail(reason='eth_feeHistory is not implemented on eth-tester') + def test_eth_fee_history_no_reward_percentiles(self, web3: "Web3"): + super().test_eth_fee_history_no_reward_percentiles(web3) + @pytest.mark.xfail(reason='EIP 1559 is not implemented on eth-tester') def test_eth_get_transaction_receipt_unmined(self, eth_tester, web3, unlocked_account): super().test_eth_get_transaction_receipt_unmined(web3, unlocked_account) diff --git a/web3/_utils/method_formatters.py b/web3/_utils/method_formatters.py index 9bfd716a05..f3bfd97a09 100644 --- a/web3/_utils/method_formatters.py +++ b/web3/_utils/method_formatters.py @@ -278,6 +278,16 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]: TRANSACTION_POOL_INSPECT_FORMATTERS ) +FEE_HISTORY_FORMATTERS = { + 'baseFeePerGas': apply_formatter_to_array(to_integer_if_hex), + 'gasUsedRatio': apply_formatter_if(is_not_null, apply_formatter_to_array(float)), + 'oldestBlock': to_integer_if_hex, + 'reward': apply_formatter_if(is_not_null, apply_formatter_to_array( + apply_formatter_to_array(to_integer_if_hex))), +} + +fee_history_formatter = apply_formatters_to_dict(FEE_HISTORY_FORMATTERS) + STORAGE_PROOF_FORMATTERS = { 'key': HexBytes, 'value': HexBytes, @@ -382,6 +392,7 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]: PYTHONIC_REQUEST_FORMATTERS: Dict[RPCEndpoint, Callable[..., Any]] = { # Eth + RPC.eth_feeHistory: apply_formatter_at_index(to_hex_if_integer, 1), RPC.eth_getBalance: apply_formatter_at_index(to_hex_if_integer, 1), RPC.eth_getBlockByNumber: apply_formatter_at_index(to_hex_if_integer, 0), RPC.eth_getBlockTransactionCountByNumber: apply_formatter_at_index( @@ -441,6 +452,7 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]: RPC.eth_coinbase: to_checksum_address, RPC.eth_call: HexBytes, RPC.eth_estimateGas: to_integer_if_hex, + RPC.eth_feeHistory: fee_history_formatter, RPC.eth_gasPrice: to_integer_if_hex, RPC.eth_getBalance: to_integer_if_hex, RPC.eth_getBlockByHash: apply_formatter_if(is_not_null, block_formatter), diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py index 2ee8f8c8f4..e24e04be71 100644 --- a/web3/_utils/module_testing/eth_module.py +++ b/web3/_utils/module_testing/eth_module.py @@ -294,6 +294,36 @@ async def test_eth_estimate_gas( assert is_integer(gas_estimate) assert gas_estimate > 0 + @pytest.mark.asyncio + async def test_eth_fee_history(self, async_w3: "Web3") -> None: + fee_history = await async_w3.eth.fee_history(1, 'latest', [50]) # type: ignore + assert is_list_like(fee_history['baseFeePerGas']) + assert is_list_like(fee_history['gasUsedRatio']) + assert is_integer(fee_history['oldestBlock']) + assert fee_history['oldestBlock'] >= 0 + assert is_list_like(fee_history['reward']) + assert is_list_like(fee_history['reward'][0]) + + @pytest.mark.asyncio + async def test_eth_fee_history_with_integer( + self, async_w3: "Web3", empty_block: BlockData + ) -> None: + fee_history = await async_w3.eth.fee_history(1, empty_block['number'], [50]) # type: ignore + assert is_list_like(fee_history['baseFeePerGas']) + assert is_list_like(fee_history['gasUsedRatio']) + assert is_integer(fee_history['oldestBlock']) + assert fee_history['oldestBlock'] >= 0 + assert is_list_like(fee_history['reward']) + assert is_list_like(fee_history['reward'][0]) + + @pytest.mark.asyncio + async def test_eth_fee_history_no_reward_percentiles(self, async_w3: "Web3") -> None: + fee_history = await async_w3.eth.fee_history(1, 'latest') # type: ignore + assert is_list_like(fee_history['baseFeePerGas']) + assert is_list_like(fee_history['gasUsedRatio']) + assert is_integer(fee_history['oldestBlock']) + assert fee_history['oldestBlock'] >= 0 + @pytest.mark.asyncio async def test_eth_getBlockByHash( self, async_w3: "Web3", empty_block: BlockData @@ -568,6 +598,33 @@ def test_eth_chainId(self, web3: "Web3") -> None: # chain id value from geth fixture genesis file assert chain_id == 131277322940537 + def test_eth_fee_history(self, web3: "Web3") -> None: + fee_history = web3.eth.fee_history(1, 'latest', [50]) + assert is_list_like(fee_history['baseFeePerGas']) + assert is_list_like(fee_history['gasUsedRatio']) + assert is_integer(fee_history['oldestBlock']) + assert fee_history['oldestBlock'] >= 0 + assert is_list_like(fee_history['reward']) + assert is_list_like(fee_history['reward'][0]) + + def test_eth_fee_history_with_integer(self, + web3: "Web3", + empty_block: BlockData) -> None: + fee_history = web3.eth.fee_history(1, empty_block['number'], [50]) + assert is_list_like(fee_history['baseFeePerGas']) + assert is_list_like(fee_history['gasUsedRatio']) + assert is_integer(fee_history['oldestBlock']) + assert fee_history['oldestBlock'] >= 0 + assert is_list_like(fee_history['reward']) + assert is_list_like(fee_history['reward'][0]) + + def test_eth_fee_history_no_reward_percentiles(self, web3: "Web3") -> None: + fee_history = web3.eth.fee_history(1, 'latest') + assert is_list_like(fee_history['baseFeePerGas']) + assert is_list_like(fee_history['gasUsedRatio']) + assert is_integer(fee_history['oldestBlock']) + assert fee_history['oldestBlock'] >= 0 + def test_eth_gas_price(self, web3: "Web3") -> None: gas_price = web3.eth.gas_price assert is_integer(gas_price) diff --git a/web3/_utils/rpc_abi.py b/web3/_utils/rpc_abi.py index 4aa7cf60b7..4a25d27c51 100644 --- a/web3/_utils/rpc_abi.py +++ b/web3/_utils/rpc_abi.py @@ -46,6 +46,7 @@ class RPC: eth_chainId = RPCEndpoint("eth_chainId") eth_coinbase = RPCEndpoint("eth_coinbase") eth_estimateGas = RPCEndpoint("eth_estimateGas") + eth_feeHistory = RPCEndpoint("eth_feeHistory") eth_gasPrice = RPCEndpoint("eth_gasPrice") eth_getBalance = RPCEndpoint("eth_getBalance") eth_getBlockByHash = RPCEndpoint("eth_getBlockByHash") diff --git a/web3/eth.py b/web3/eth.py index 0feed47fae..82fab201ff 100644 --- a/web3/eth.py +++ b/web3/eth.py @@ -87,7 +87,9 @@ ENS, BlockData, BlockIdentifier, + BlockParams, CallOverrideParams, + FeeHistory, FilterParams, GasPriceStrategy, LogReceipt, @@ -176,6 +178,11 @@ def estimate_gas_munger( mungers=[estimate_gas_munger] ) + _fee_history: Method[Callable[..., FeeHistory]] = Method( + RPC.eth_feeHistory, + mungers=[default_root_munger] + ) + def get_block_munger( self, block_identifier: BlockIdentifier, full_transactions: bool = False ) -> Tuple[BlockIdentifier, bool]: @@ -241,6 +248,15 @@ async def gas_price(self) -> Wei: # types ignored b/c mypy conflict with BlockingEth properties return await self._gas_price() # type: ignore + async def fee_history( + self, + block_count: int, + newest_block: Union[BlockParams, BlockNumber], + reward_percentiles: Optional[List[float]] = None + ) -> FeeHistory: + return await self._fee_history( # type: ignore + block_count, newest_block, reward_percentiles) + async def send_transaction(self, transaction: TxParams) -> HexBytes: # types ignored b/c mypy conflict with BlockingEth properties return await self._send_transaction(transaction) # type: ignore @@ -698,6 +714,14 @@ def estimate_gas( ) -> Wei: return self._estimate_gas(transaction, block_identifier) + def fee_history( + self, + block_count: int, + newest_block: Union[BlockParams, BlockNumber], + reward_percentiles: Optional[List[float]] = None + ) -> FeeHistory: + return self._fee_history(block_count, newest_block, reward_percentiles) + def filter_munger( self, filter_params: Optional[Union[str, FilterParams]] = None, diff --git a/web3/providers/eth_tester/defaults.py b/web3/providers/eth_tester/defaults.py index a5af7dfb72..b026de7423 100644 --- a/web3/providers/eth_tester/defaults.py +++ b/web3/providers/eth_tester/defaults.py @@ -213,6 +213,7 @@ def personal_send_transaction(eth_tester: "EthereumTester", params: Any) -> HexS 'mining': static_return(False), 'hashrate': static_return(0), 'chainId': static_return('0x3d'), + 'feeHistory': not_implemented, 'gasPrice': static_return(1), 'accounts': call_eth_tester('get_accounts'), 'blockNumber': compose( diff --git a/web3/types.py b/web3/types.py index 7236443c54..042fdd4ab9 100644 --- a/web3/types.py +++ b/web3/types.py @@ -144,6 +144,13 @@ class FilterParams(TypedDict, total=False): topics: Sequence[Optional[Union[_Hash32, Sequence[_Hash32]]]] +class FeeHistory(TypedDict): + baseFeePerGas: List[Wei] + gasUsedRatio: List[float] + oldestBlock: BlockNumber + reward: List[List[Wei]] + + class LogReceipt(TypedDict): address: ChecksumAddress blockHash: HexBytes