diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index dd282ad0f4..926c26a50e 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -325,6 +325,14 @@ def failed(self) -> bool: """ return False + @property + def confirmed(self) -> bool: + """ + ``True`` when the number of confirmations is equal or greater + to the required amount of confirmations. + """ + return self._confirmations_occurred == self.required_confirmations + @property @abstractmethod def total_fees_paid(self) -> int: @@ -417,40 +425,39 @@ def await_confirmations(self) -> "ReceiptAPI": Returns: :class:`~ape.api.ReceiptAPI`: The receipt that is now confirmed. """ - # perf: avoid *everything* if required_confirmations is 0, as this is likely a - # dev environment or the user doesn't care. - if self.required_confirmations == 0: - # The transaction might not yet be confirmed but - # the user is aware of this. Or, this is a development environment. + # NOTE: Even when required_confirmations is `0`, we want to wait for the nonce to + # increment. Otherwise, users may end up with invalid nonce errors in tests. + self._await_sender_nonce_increment() + if self.required_confirmations == 0 or self._check_error_status() or self.confirmed: return self - try: - self.raise_for_status() - except TransactionError: - # Skip waiting for confirmations when the transaction has failed. - return self + # Confirming now. + self._log_submission() + self._await_confirmations() + return self + + def _await_sender_nonce_increment(self): + if not self.sender: + return iterations_timeout = 20 iteration = 0 - # Wait for nonce from provider to increment. - if self.sender: + sender_nonce = self.provider.get_nonce(self.sender) + while sender_nonce == self.nonce: + time.sleep(1) sender_nonce = self.provider.get_nonce(self.sender) + iteration += 1 + if iteration != iterations_timeout: + continue + + tx_err = TransactionError("Timeout waiting for sender's nonce to increase.") + self.error = tx_err + if self.transaction.raise_on_revert: + raise tx_err + else: + break - while sender_nonce == self.nonce: - time.sleep(1) - sender_nonce = self.provider.get_nonce(self.sender) - iteration += 1 - if iteration == iterations_timeout: - tx_err = TransactionError("Timeout waiting for sender's nonce to increase.") - self.error = tx_err - if self.transaction.raise_on_revert: - raise tx_err - - confirmations_occurred = self._confirmations_occurred - if self.required_confirmations and confirmations_occurred >= self.required_confirmations: - return self - - # If we get here, that means the transaction has been recently submitted. + def _log_submission(self): if explorer_url := self._explorer and self._explorer.get_transaction_url(self.txn_hash): log_message = f"Submitted {explorer_url}" else: @@ -458,26 +465,34 @@ def await_confirmations(self) -> "ReceiptAPI": logger.info(log_message) - if self.required_confirmations: - with ConfirmationsProgressBar(self.required_confirmations) as progress_bar: - while confirmations_occurred < self.required_confirmations: - confirmations_occurred = self._confirmations_occurred - progress_bar.confs = confirmations_occurred + def _check_error_status(self) -> bool: + try: + self.raise_for_status() + except TransactionError: + # Skip waiting for confirmations when the transaction has failed. + return True + + return False - if confirmations_occurred == self.required_confirmations: - break + def _await_confirmations(self): + if self.required_confirmations <= 0: + return - time_to_sleep = int(self._block_time / 2) - time.sleep(time_to_sleep) + with ConfirmationsProgressBar(self.required_confirmations) as progress_bar: + while not self.confirmed: + confirmations_occurred = self._confirmations_occurred + if confirmations_occurred >= self.required_confirmations: + break - return self + progress_bar.confs = confirmations_occurred + time_to_sleep = int(self._block_time / 2) + time.sleep(time_to_sleep) @property def method_called(self) -> Optional[MethodABI]: """ The method ABI of the method called to produce this receipt. """ - return None @property diff --git a/tests/functional/geth/test_receipt.py b/tests/functional/geth/test_receipt.py index 0c267065e0..5a4a53a7bf 100644 --- a/tests/functional/geth/test_receipt.py +++ b/tests/functional/geth/test_receipt.py @@ -52,3 +52,24 @@ def test_track_gas(mocker, geth_account, geth_contract, gas_tracker): contract_name = geth_contract.contract_type.name assert contract_name in report assert "getNestedStructWithTuple1" in report[contract_name] + + +@geth_process_test +def test_await_confirmations(geth_account, geth_contract): + tx = geth_contract.setNumber(235921972943759, sender=geth_account) + tx.await_confirmations() + assert tx.confirmed + + +@geth_process_test +def test_await_confirmations_zero_confirmations(mocker, geth_account, geth_contract): + """ + We still need to wait for the nonce to increase when required confirmations is 0. + Otherwise, we sometimes ran into nonce-issues when transacting too fast with + the same account. + """ + tx = geth_contract.setNumber(545921972923759, sender=geth_account, required_confirmations=0) + spy = mocker.spy(tx, "_await_sender_nonce_increment") + tx.await_confirmations() + assert tx.confirmed + assert spy.call_count == 1