From 489394726d2718325ce6ff1d3ff57764b9495086 Mon Sep 17 00:00:00 2001 From: Moras-del <55106176+Moras-del@users.noreply.github.com> Date: Wed, 14 Jul 2021 19:42:57 +0200 Subject: [PATCH] Add eth_getRawTransactionByHash (#2051) --- docs/web3.eth.rst | 16 ++++++++++- newsfragments/2039.feature.rst | 1 + .../core/manager/test_response_formatters.py | 27 +++++++++++++++++++ tests/integration/test_ethereum_tester.py | 4 +++ web3/_utils/method_formatters.py | 2 ++ web3/_utils/module_testing/eth_module.py | 26 ++++++++++++++++++ web3/_utils/rpc_abi.py | 2 ++ web3/eth.py | 12 +++++++++ web3/manager.py | 8 +++++- web3/middleware/cache.py | 1 + web3/middleware/exception_retry_request.py | 1 + 11 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 newsfragments/2039.feature.rst diff --git a/docs/web3.eth.rst b/docs/web3.eth.rst index 314639b234..0c86bdb555 100644 --- a/docs/web3.eth.rst +++ b/docs/web3.eth.rst @@ -576,6 +576,20 @@ The following methods are available on the ``web3.eth`` namespace. :attr:`~web3.eth.Eth.get_transaction` +.. py:method:: Eth.get_raw_transaction(transaction_hash) + + * Delegates to ``eth_getRawTransactionByHash`` RPC Method + + Returns the raw form of transaction specified by ``transaction_hash``. + + If no transaction is found, ``TransactionNotFound`` is raised. + + .. code-block:: python + + >>> web3.eth.get_raw_transaction('0x86fbfe56cce542ff0a2a2716c31675a0c9c43701725c4a751d20ee2ddf8a733d') + HexBytes('0xf86907843b9aca0082520894dc544d1aa88ff8bbd2f2aec754b1f1e99e1812fd018086eecac466e115a0f9db4e25484b28f486b247a372708d4cd0643fc63e604133afac577f4cc1eab8a044841d84e799d4dc18ba146816a937e8a0be8bc296bd8bb8aea126de5e627e06') + + .. py:method:: Eth.getTransactionFromBlock(block_identifier, transaction_index) .. note:: This method is deprecated in EIP 1474. @@ -748,7 +762,7 @@ The following methods are available on the ``web3.eth`` namespace. that goes to the miner * ``gasPrice``: ``integer`` - Integer of the gasPrice used for each paid gas **LEGACY** - unless you have good reason to, use ``maxFeePerGas`` - and ``maxPriorityFeePerGas`` instead. + and ``maxPriorityFeePerGas`` instead. * ``value``: ``integer`` - (optional) Integer of the value send with this transaction * ``data``: ``bytes or text`` - The compiled code of a contract OR the hash diff --git a/newsfragments/2039.feature.rst b/newsfragments/2039.feature.rst new file mode 100644 index 0000000000..2691db33e6 --- /dev/null +++ b/newsfragments/2039.feature.rst @@ -0,0 +1 @@ +Add support for eth_getRawTransactionByHash RPC method \ No newline at end of file diff --git a/tests/core/manager/test_response_formatters.py b/tests/core/manager/test_response_formatters.py index 3c23a98282..ec8cbf47ce 100644 --- a/tests/core/manager/test_response_formatters.py +++ b/tests/core/manager/test_response_formatters.py @@ -7,10 +7,12 @@ from web3._utils.method_formatters import ( raise_block_not_found, raise_block_not_found_for_uncle_at_index, + raise_transaction_not_found, ) from web3.exceptions import ( BlockNotFound, ContractLogicError, + TransactionNotFound, ) ERROR_RESPONSE = { @@ -24,6 +26,7 @@ NONE_RESPONSE = {"jsonrpc": "2.0", "id": 1, "result": None} +ZERO_X_RESPONSE = {"jsonrpc": "2.0", "id": 1, "result": '0x'} def raise_contract_logic_error(response): @@ -89,6 +92,14 @@ def raise_contract_logic_error(response): raise_block_not_found_for_uncle_at_index, BlockNotFound, ), + ( + # 0x response gets handled the same as a None response + ZERO_X_RESPONSE, + ('0x03'), + identity, + raise_transaction_not_found, + TransactionNotFound, + ), ], ) def test_formatted_response_raises_errors(web3, @@ -123,6 +134,14 @@ def test_formatted_response_raises_errors(web3, BlockNotFound, "Uncle at index: 0 of block with id: '0x01' not found." ), + ( + ZERO_X_RESPONSE, + ('0x01',), + identity, + raise_transaction_not_found, + TransactionNotFound, + "Transaction with hash: '0x01' not found." + ), ], ) def test_formatted_response_raises_correct_error_message(response, @@ -148,6 +167,14 @@ def test_formatted_response_raises_correct_error_message(response, identity, NONE_RESPONSE['result'], ), + ( + # Response with a result of 0x doesn't raise if there is no null result formatter + ZERO_X_RESPONSE, + ('0x03'), + identity, + identity, + ZERO_X_RESPONSE['result'], + ), ], ) def test_formatted_response(response, diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py index a74baa67df..80d80eb37a 100644 --- a/tests/integration/test_ethereum_tester.py +++ b/tests/integration/test_ethereum_tester.py @@ -264,6 +264,10 @@ class TestEthereumTesterEthModule(EthModuleTest): test_eth_submitWork_deprecated = not_implemented( EthModuleTest.test_eth_submitWork_deprecated, ValueError) test_eth_submit_work = not_implemented(EthModuleTest.test_eth_submit_work, ValueError) + test_eth_get_raw_transaction = not_implemented( + EthModuleTest.test_eth_get_raw_transaction, ValueError) + test_eth_get_raw_transaction_raises_error = not_implemented( + EthModuleTest.test_eth_get_raw_transaction, ValueError) def test_eth_getBlockByHash_pending( self, web3: "Web3" diff --git a/web3/_utils/method_formatters.py b/web3/_utils/method_formatters.py index 298dc76f15..e489b324e3 100644 --- a/web3/_utils/method_formatters.py +++ b/web3/_utils/method_formatters.py @@ -450,6 +450,7 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]: RPC.eth_getFilterLogs: filter_result_formatter, RPC.eth_getLogs: filter_result_formatter, RPC.eth_getProof: apply_formatter_if(is_not_null, proof_formatter), + RPC.eth_getRawTransactionByHash: HexBytes, RPC.eth_getStorageAt: HexBytes, RPC.eth_getTransactionByBlockHashAndIndex: apply_formatter_if( is_not_null, @@ -657,6 +658,7 @@ def raise_transaction_not_found_with_index(params: Tuple[BlockIdentifier, int]) RPC.eth_getTransactionByBlockHashAndIndex: raise_transaction_not_found_with_index, RPC.eth_getTransactionByBlockNumberAndIndex: raise_transaction_not_found_with_index, RPC.eth_getTransactionReceipt: raise_transaction_not_found, + RPC.eth_getRawTransactionByHash: raise_transaction_not_found, } diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py index b495a68592..8cf6a06360 100644 --- a/web3/_utils/module_testing/eth_module.py +++ b/web3/_utils/module_testing/eth_module.py @@ -360,6 +360,20 @@ async def test_eth_getBlockByNumber_full_transactions( transaction = block['transactions'][0] assert transaction['hash'] == block_with_txn['transactions'][0] + @pytest.mark.asyncio + async def test_eth_get_raw_transaction( + self, async_w3: "Web3", mined_txn_hash: HexStr + ) -> None: + raw_transaction = await async_w3.eth.get_raw_transaction(mined_txn_hash) # type: ignore + assert is_bytes(raw_transaction) + + @pytest.mark.asyncio + async def test_eth_get_raw_transaction_raises_error( + self, web3: "Web3", mined_txn_hash: HexStr + ) -> None: + with pytest.raises(TransactionNotFound, match=f"Transaction with hash: '{UNKNOWN_HASH}'"): + web3.eth.get_raw_transaction(UNKNOWN_HASH) + class EthModuleTest: def test_eth_protocol_version(self, web3: "Web3") -> None: @@ -2168,3 +2182,15 @@ def test_eth_submitWork_deprecated(self, web3: "Web3") -> None: match="submitWork is deprecated in favor of submit_work"): result = web3.eth.submitWork(nonce, pow_hash, mix_digest) assert result is False + + def test_eth_get_raw_transaction( + self, web3: "Web3", mined_txn_hash: HexStr + ) -> None: + raw_transaction = web3.eth.get_raw_transaction(mined_txn_hash) + assert is_bytes(raw_transaction) + + def test_eth_get_raw_transaction_raises_error( + self, web3: "Web3", mined_txn_hash: HexStr + ) -> None: + with pytest.raises(TransactionNotFound, match=f"Transaction with hash: '{UNKNOWN_HASH}'"): + web3.eth.get_raw_transaction(UNKNOWN_HASH) diff --git a/web3/_utils/rpc_abi.py b/web3/_utils/rpc_abi.py index 8383ea076a..4aa7cf60b7 100644 --- a/web3/_utils/rpc_abi.py +++ b/web3/_utils/rpc_abi.py @@ -57,6 +57,7 @@ class RPC: eth_getFilterLogs = RPCEndpoint("eth_getFilterLogs") eth_getLogs = RPCEndpoint("eth_getLogs") eth_getProof = RPCEndpoint("eth_getProof") + eth_getRawTransactionByHash = RPCEndpoint("eth_getRawTransactionByHash") eth_getStorageAt = RPCEndpoint("eth_getStorageAt") eth_getTransactionByBlockHashAndIndex = RPCEndpoint("eth_getTransactionByBlockHashAndIndex") eth_getTransactionByBlockNumberAndIndex = RPCEndpoint("eth_getTransactionByBlockNumberAndIndex") @@ -175,6 +176,7 @@ class RPC: 'eth_getBlockTransactionCountByHash': ['bytes32'], 'eth_getCode': ['address', None], 'eth_getLogs': FILTER_PARAMS_ABIS, + 'eth_getRawTransactionByHash': ['bytes32'], 'eth_getStorageAt': ['address', 'uint', None], 'eth_getProof': ['address', 'uint[]', None], 'eth_getTransactionByBlockHashAndIndex': ['bytes32', 'uint'], diff --git a/web3/eth.py b/web3/eth.py index 4022013f15..59e137dfe0 100644 --- a/web3/eth.py +++ b/web3/eth.py @@ -132,6 +132,11 @@ def send_transaction_munger(self, transaction: TxParams) -> Tuple[TxParams]: mungers=[default_root_munger] ) + _get_raw_transaction: Method[Callable[[_Hash32], HexBytes]] = Method( + RPC.eth_getRawTransactionByHash, + mungers=[default_root_munger] + ) + def _generate_gas_price(self, transaction_params: Optional[TxParams] = None) -> Optional[Wei]: if self.gasPriceStrategy: return self.gasPriceStrategy(self.web3, transaction_params) @@ -205,6 +210,10 @@ async def get_transaction(self, transaction_hash: _Hash32) -> TxData: # types ignored b/c mypy conflict with BlockingEth properties return await self._get_transaction(transaction_hash) # type: ignore + async def get_raw_transaction(self, transaction_hash: _Hash32) -> TxData: + # types ignored b/c mypy conflict with BlockingEth properties + return await self._get_raw_transaction(transaction_hash) # type: ignore + async def generate_gas_price( self, transaction_params: Optional[TxParams] = None ) -> Optional[Wei]: @@ -501,6 +510,9 @@ def get_block( def get_transaction(self, transaction_hash: _Hash32) -> TxData: return self._get_transaction(transaction_hash) + def get_raw_transaction(self, transaction_hash: _Hash32) -> _Hash32: + return self._get_raw_transaction(transaction_hash) + def getTransactionFromBlock( self, block_identifier: BlockIdentifier, transaction_index: int ) -> NoReturn: diff --git a/web3/manager.py b/web3/manager.py index 9234b864ff..d7713c462a 100644 --- a/web3/manager.py +++ b/web3/manager.py @@ -17,6 +17,9 @@ from eth_utils.toolz import ( pipe, ) +from hexbytes import ( + HexBytes, +) from web3._utils.decorators import ( deprecated_for, @@ -53,6 +56,9 @@ from web3 import Web3 # noqa: F401 +NULL_RESPONSES = [None, HexBytes('0x'), '0x'] + + def apply_error_formatters( error_formatters: Callable[..., Any], response: RPCResponse, @@ -160,7 +166,7 @@ def formatted_response( if "error" in response: apply_error_formatters(error_formatters, response) raise ValueError(response["error"]) - elif response['result'] is None: + elif response['result'] in NULL_RESPONSES: # null_result_formatters raise either a BlockNotFound # or a TransactionNotFound error, depending on the method called apply_null_result_formatters(null_result_formatters, response, params) diff --git a/web3/middleware/cache.py b/web3/middleware/cache.py index f68f626b4c..e81761b0a3 100644 --- a/web3/middleware/cache.py +++ b/web3/middleware/cache.py @@ -65,6 +65,7 @@ 'eth_getTransactionByBlockHashAndIndex', # 'eth_getTransactionByBlockNumberAndIndex', # 'eth_getTransactionReceipt', + 'eth_getRawTransactionByHash', 'eth_getUncleByBlockHashAndIndex', # 'eth_getUncleByBlockNumberAndIndex', # 'eth_getCompilers', diff --git a/web3/middleware/exception_retry_request.py b/web3/middleware/exception_retry_request.py index 887b45c68d..3a7192abf7 100644 --- a/web3/middleware/exception_retry_request.py +++ b/web3/middleware/exception_retry_request.py @@ -51,6 +51,7 @@ 'eth_getTransactionByBlockNumberAndIndex', 'eth_getTransactionReceipt', 'eth_getTransactionCount', + 'eth_getRawTransactionByHash', 'eth_call', 'eth_estimateGas', 'eth_newBlockFilter',