diff --git a/newsfragments/2265.feature.rst b/newsfragments/2265.feature.rst new file mode 100644 index 0000000000..b61e66f36a --- /dev/null +++ b/newsfragments/2265.feature.rst @@ -0,0 +1 @@ +Add async ``eth.get_transaction_receipt`` and ``eth.wait_for_transaction_receipt`` methods \ No newline at end of file diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py index 9859dabc8e..482af2b8e4 100644 --- a/web3/_utils/module_testing/eth_module.py +++ b/web3/_utils/module_testing/eth_module.py @@ -47,6 +47,7 @@ InvalidAddress, InvalidTransaction, NameNotFound, + TimeExhausted, TransactionNotFound, TransactionTypeMismatch, ) @@ -704,7 +705,7 @@ async def test_async_eth_mining( ) -> None: mining = await async_w3.eth.mining # type: ignore assert is_boolean(mining) - + @pytest.mark.asyncio async def test_async_eth_get_transaction_receipt_mined( self, @@ -712,7 +713,7 @@ async def test_async_eth_get_transaction_receipt_mined( block_with_txn: BlockData, mined_txn_hash: HexStr ) -> None: - receipt = await async_w3.eth.get_transaction_receipt(mined_txn_hash) + receipt = await async_w3.eth.get_transaction_receipt(mined_txn_hash) # type: ignore assert is_dict(receipt) assert receipt['blockNumber'] == block_with_txn['number'] assert receipt['blockHash'] == block_with_txn['hash'] @@ -730,7 +731,7 @@ async def test_async_eth_get_transaction_receipt_mined( async def test_async_eth_get_transaction_receipt_unmined( self, async_w3: "Web3", unlocked_account_dual_type: ChecksumAddress ) -> None: - txn_hash = await async_w3.eth.send_transaction({ + txn_hash = await async_w3.eth.send_transaction({ # type: ignore 'from': unlocked_account_dual_type, 'to': unlocked_account_dual_type, 'value': Wei(1), @@ -739,7 +740,7 @@ async def test_async_eth_get_transaction_receipt_unmined( 'maxPriorityFeePerGas': async_w3.toWei(1, 'gwei') }) with pytest.raises(TransactionNotFound): - await async_w3.eth.get_transaction_receipt(txn_hash) + await async_w3.eth.get_transaction_receipt(txn_hash) # type: ignore @pytest.mark.asyncio async def test_async_eth_get_transaction_receipt_with_log_entry( @@ -749,7 +750,7 @@ async def test_async_eth_get_transaction_receipt_with_log_entry( emitter_contract: "Contract", txn_hash_with_log: HexStr, ) -> None: - receipt = await async_w3.eth.wait_for_transaction_receipt(txn_hash_with_log) + receipt = await async_w3.eth.wait_for_transaction_receipt(txn_hash_with_log) # type: ignore assert is_dict(receipt) assert receipt['blockNumber'] == block_with_txn_with_log['number'] assert receipt['blockHash'] == block_with_txn_with_log['hash'] @@ -773,7 +774,7 @@ async def test_async_eth_wait_for_transaction_receipt_mined( block_with_txn: BlockData, mined_txn_hash: HexStr ) -> None: - receipt = await async_w3.eth.wait_for_transaction_receipt(mined_txn_hash) + receipt = await async_w3.eth.wait_for_transaction_receipt(mined_txn_hash) # type: ignore assert is_dict(receipt) assert receipt['blockNumber'] == block_with_txn['number'] assert receipt['blockHash'] == block_with_txn['hash'] @@ -791,7 +792,7 @@ async def test_async_eth_wait_for_transaction_receipt_mined( async def test_async_eth_wait_for_transaction_receipt_unmined( self, async_w3: "Web3", unlocked_account_dual_type: ChecksumAddress ) -> None: - txn_hash = await async_w3.eth.send_transaction({ + txn_hash = await async_w3.eth.send_transaction({ # type: ignore 'from': unlocked_account_dual_type, 'to': unlocked_account_dual_type, 'value': Wei(1), @@ -799,18 +800,18 @@ async def test_async_eth_wait_for_transaction_receipt_unmined( 'maxFeePerGas': async_w3.toWei(3, 'gwei'), 'maxPriorityFeePerGas': async_w3.toWei(1, 'gwei') }) - with pytest.raises(TransactionNotFound): - await async_w3.eth.wait_for_transaction_receipt(txn_hash) + with pytest.raises(TimeExhausted): + await async_w3.eth.wait_for_transaction_receipt(txn_hash, timeout=2) # type: ignore @pytest.mark.asyncio - async def test_async_eth_get_transaction_receipt_with_log_entry( + async def test_async_eth_wait_for_transaction_receipt_with_log_entry( self, async_w3: "Web3", block_with_txn_with_log: BlockData, emitter_contract: "Contract", txn_hash_with_log: HexStr, ) -> None: - receipt = await async_w3.eth.get_transaction_receipt(txn_hash_with_log) + receipt = await async_w3.eth.wait_for_transaction_receipt(txn_hash_with_log) # type: ignore assert is_dict(receipt) assert receipt['blockNumber'] == block_with_txn_with_log['number'] assert receipt['blockHash'] == block_with_txn_with_log['hash'] @@ -827,6 +828,7 @@ async def test_async_eth_get_transaction_receipt_with_log_entry( assert log_entry['transactionIndex'] == 0 assert log_entry['transactionHash'] == HexBytes(txn_hash_with_log) + class EthModuleTest: def test_eth_protocol_version(self, web3: "Web3") -> None: with pytest.warns(DeprecationWarning, diff --git a/web3/_utils/transactions.py b/web3/_utils/transactions.py index d63ea51d42..27a285182c 100644 --- a/web3/_utils/transactions.py +++ b/web3/_utils/transactions.py @@ -21,9 +21,6 @@ from web3._utils.compat import ( Literal, ) -from web3._utils.threads import ( - Timeout, -) from web3._utils.utility_methods import ( all_in_dict, any_in_dict, @@ -31,14 +28,10 @@ from web3.constants import ( DYNAMIC_FEE_TXN_PARAMS, ) -from web3.exceptions import ( - TransactionNotFound, -) from web3.types import ( BlockIdentifier, TxData, TxParams, - TxReceipt, Wei, _Hash32, ) @@ -126,25 +119,6 @@ def fill_transaction_defaults(web3: "Web3", transaction: TxParams) -> TxParams: return merge(defaults, transaction) -def wait_for_transaction_receipt( - web3: "Web3", txn_hash: _Hash32, timeout: float, poll_latency: float -) -> TxReceipt: - with Timeout(timeout) as _timeout: - while True: - try: - txn_receipt = web3.eth.get_transaction_receipt(txn_hash) - except TransactionNotFound: - txn_receipt = None - # FIXME: The check for a null `blockHash` is due to parity's - # non-standard implementation of the JSON-RPC API and should - # be removed once the formal spec for the JSON-RPC endpoints - # has been finalized. - if txn_receipt is not None and txn_receipt['blockHash'] is not None: - break - _timeout.sleep(poll_latency) - return txn_receipt - - def get_block_gas_limit(web3: "Web3", block_identifier: Optional[BlockIdentifier] = None) -> Wei: if block_identifier is None: block_identifier = web3.eth.block_number diff --git a/web3/eth.py b/web3/eth.py index 636805798d..dfaddbe8de 100644 --- a/web3/eth.py +++ b/web3/eth.py @@ -62,7 +62,6 @@ extract_valid_transaction_params, get_required_transaction, replace_transaction, - wait_for_transaction_receipt as utils_wait_for_transaction_receipt, ) from web3.contract import ( ConciseContract, @@ -71,6 +70,7 @@ ) from web3.exceptions import ( TimeExhausted, + TransactionNotFound, ) from web3.iban import ( Iban, @@ -282,6 +282,12 @@ def call_munger( mungers=[default_root_munger] ) + # _get_transaction_receipt_awaitable: Method[Callable[[_Hash32], + # Awaitable[TxReceipt]]] = Method( + # RPC.eth_getTransactionReceipt, + # mungers=[default_root_munger] + # ) + class AsyncEth(BaseEth): is_async = True @@ -411,13 +417,30 @@ async def get_transaction_count( async def get_transaction_receipt( self, transaction_hash: _Hash32 ) -> TxReceipt: - return await self._get_transaction_receipt(transaction_hash) + return await self._get_transaction_receipt(transaction_hash) # type: ignore async def wait_for_transaction_receipt( self, transaction_hash: _Hash32, timeout: float = 120, poll_latency: float = 0.1 ) -> TxReceipt: try: - return await utils_wait_for_transaction_receipt(self.web3, transaction_hash, timeout, poll_latency) + with Timeout(timeout) as _timeout: + while True: + try: + tx_receipt = await self._get_transaction_receipt( # type: ignore + transaction_hash + ) + except TransactionNotFound: + tx_receipt = None + # FIXME: The check for a null `blockHash` is due to parity's + # non-standard implementation of the JSON-RPC API and should + # be removed once the formal spec for the JSON-RPC endpoints + # has been finalized. + # if txn_receipt is not None and txn_receipt['blockHash'] is not None: + if tx_receipt is not None: + break + _timeout.sleep(poll_latency) + return tx_receipt + except Timeout: raise TimeExhausted( "Transaction {!r} is not in the chain, after {} seconds".format( @@ -706,7 +729,22 @@ def wait_for_transaction_receipt( self, transaction_hash: _Hash32, timeout: float = 120, poll_latency: float = 0.1 ) -> TxReceipt: try: - return utils_wait_for_transaction_receipt(self.web3, transaction_hash, timeout, poll_latency) + with Timeout(timeout) as _timeout: + while True: + try: + tx_receipt = self._get_transaction_receipt(transaction_hash) + except TransactionNotFound: + tx_receipt = None + # FIXME: The check for a null `blockHash` is due to parity's + # non-standard implementation of the JSON-RPC API and should + # be removed once the formal spec for the JSON-RPC endpoints + # has been finalized. + # if txn_receipt is not None and txn_receipt['blockHash'] is not None: + if tx_receipt is not None: + break + _timeout.sleep(poll_latency) + return tx_receipt + except Timeout: raise TimeExhausted( "Transaction {!r} is not in the chain, after {} seconds".format( @@ -720,11 +758,6 @@ def get_transaction_receipt( ) -> TxReceipt: return self._get_transaction_receipt(transaction_hash) - # get_transaction_receipt: Method[Callable[[_Hash32], TxReceipt]] = Method( - # RPC.eth_getTransactionReceipt, - # mungers=[default_root_munger] - # ) - get_transaction_count: Method[Callable[..., Nonce]] = Method( RPC.eth_getTransactionCount, mungers=[BaseEth.block_id_munger], @@ -952,7 +985,7 @@ def setGasPriceStrategy(self, gas_price_strategy: GasPriceStrategy) -> None: sendRawTransaction = DeprecatedMethod(send_raw_transaction, # type: ignore 'sendRawTransaction', 'send_raw_transaction') - getTransactionReceipt = DeprecatedMethod(get_transaction_receipt, + getTransactionReceipt = DeprecatedMethod(get_transaction_receipt, # type: ignore 'getTransactionReceipt', 'get_transaction_receipt') uninstallFilter = DeprecatedMethod(uninstall_filter, 'uninstallFilter', 'uninstall_filter')