diff --git a/docs/web3.eth.rst b/docs/web3.eth.rst index 6259f668ed..bdfda41eb7 100644 --- a/docs/web3.eth.rst +++ b/docs/web3.eth.rst @@ -328,6 +328,35 @@ The following methods are available on the ``web3.eth`` namespace. }) +.. py:method:: Eth.waitForTransactionReceipt(transaction_hash, timeout=120) + + Waits for the transaction specified by ``transaction_hash`` to be included in a block, then + returns its transaction receipt. + + Optionally, specify a ``timeout`` in seconds. If timeout elapses before the transaction + is added to a block, then :meth:`~Eth.waitForTransactionReceipt` returns None. + + .. code-block:: python + + >>> web3.eth.waitForTransactionReceipt('0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060') + # If transaction is not yet in a block, time passes, while the thread sleeps... + # ... + # Then when the transaction is added to a block, its receipt is returned: + AttributeDict({ + 'blockHash': '0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd', + 'blockNumber': 46147, + 'contractAddress': None, + 'cumulativeGasUsed': 21000, + 'from': '0xa1e4380a3b1f749673e270229993ee55f35663b4', + 'gasUsed': 21000, + 'logs': [], + 'root': '96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957', + 'to': '0x5df9b87991262f6ba471f09758cde1c0fc1de734', + 'transactionHash': '0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060', + 'transactionIndex': 0, + }) + + .. py:method:: Eth.getTransactionReceipt(transaction_hash) * Delegates to ``eth_getTransactionReceipt`` RPC Method diff --git a/tests/core/eth-module/test_transactions.py b/tests/core/eth-module/test_transactions.py index 4489f2f0de..67c0a82205 100644 --- a/tests/core/eth-module/test_transactions.py +++ b/tests/core/eth-module/test_transactions.py @@ -3,6 +3,9 @@ from web3.exceptions import ( ValidationError, ) +from web3.middleware.simulate_unmined_transaction import ( + unmined_receipt_simulator_middleware, +) @pytest.mark.parametrize( @@ -31,7 +34,6 @@ def test_send_transaction_with_valid_chain_id(web3, make_chain_id, expect_succes 'to': web3.eth.accounts[1], 'chainId': make_chain_id(web3), } - if expect_success: # just be happy that we didn't crash web3.eth.sendTransaction(transaction) @@ -40,3 +42,17 @@ def test_send_transaction_with_valid_chain_id(web3, make_chain_id, expect_succes web3.eth.sendTransaction(transaction) assert 'chain ID' in str(exc_info.value) + + +def test_unmined_transaction_wait_for_receipt(web3, extra_accounts): + web3.middleware_stack.add(unmined_receipt_simulator_middleware) + txn_hash = web3.eth.sendTransaction({ + 'from': web3.eth.coinbase, + 'to': '0xd3CdA913deB6f67967B99D67aCDFa1712C293601', + 'value': 123457 + }) + assert web3.eth.getTransactionReceipt(txn_hash) is None + + txn_receipt = web3.eth.waitForTransactionReceipt(txn_hash) + assert txn_receipt['transactionHash'] == txn_hash + assert txn_receipt['blockHash'] is not None diff --git a/web3/eth.py b/web3/eth.py index 2ebf2739c0..90a01baeed 100644 --- a/web3/eth.py +++ b/web3/eth.py @@ -43,6 +43,7 @@ get_buffered_gas_estimate, get_required_transaction, replace_transaction, + wait_for_transaction_receipt, ) @@ -202,6 +203,9 @@ def getTransactionFromBlock(self, block_identifier, transaction_index): [block_identifier, transaction_index], ) + def waitForTransactionReceipt(self, transaction_hash, timeout=120): + return wait_for_transaction_receipt(self.web3, transaction_hash, timeout) + def getTransactionReceipt(self, transaction_hash): return self.web3.manager.request_blocking( "eth_getTransactionReceipt", diff --git a/web3/middleware/simulate_unmined_transaction.py b/web3/middleware/simulate_unmined_transaction.py new file mode 100644 index 0000000000..21b8321880 --- /dev/null +++ b/web3/middleware/simulate_unmined_transaction.py @@ -0,0 +1,21 @@ +import collections +import itertools + +counter = itertools.count() + +INVOCATIONS_BEFORE_RESULT = 5 + + +def unmined_receipt_simulator_middleware(make_request, web3): + receipt_counters = collections.defaultdict(itertools.count) + + def middleware(method, params): + if method == 'eth_getTransactionReceipt': + txn_hash = params[0] + if next(receipt_counters[txn_hash]) < INVOCATIONS_BEFORE_RESULT: + return {'result': None} + else: + return make_request(method, params) + else: + return make_request(method, params) + return middleware diff --git a/web3/utils/transactions.py b/web3/utils/transactions.py index bffc4a4615..b32587442f 100644 --- a/web3/utils/transactions.py +++ b/web3/utils/transactions.py @@ -1,5 +1,4 @@ import math -import random from cytoolz import ( assoc, @@ -50,13 +49,13 @@ def fill_transaction_defaults(web3, transaction): return merge(defaults, transaction) -def wait_for_transaction_receipt(web3, txn_hash, timeout=120): +def wait_for_transaction_receipt(web3, txn_hash, timeout=120, poll_latency=0.1): with Timeout(timeout) as _timeout: while True: txn_receipt = web3.eth.getTransactionReceipt(txn_hash) if txn_receipt is not None: break - _timeout.sleep(random.random()) + _timeout.sleep(poll_latency) return txn_receipt