Skip to content

Commit

Permalink
fix: 'nonce too low' errors during testing (#2270)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Sep 9, 2024
1 parent d0b6d38 commit 047b853
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 38 deletions.
91 changes: 53 additions & 38 deletions src/ape/api/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -417,67 +425,74 @@ 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:
log_message = f"Submitted {self.txn_hash}"

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
Expand Down
21 changes: 21 additions & 0 deletions tests/functional/geth/test_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 047b853

Please sign in to comment.