diff --git a/brownie/network/account.py b/brownie/network/account.py index b32e3c6da..d09f4a364 100644 --- a/brownie/network/account.py +++ b/brownie/network/account.py @@ -4,6 +4,7 @@ import sys import threading import time +from collections import deque from collections.abc import Iterator from getpass import getpass from pathlib import Path @@ -20,7 +21,7 @@ from eth_utils.applicators import apply_formatters_to_dict from hexbytes import HexBytes from web3 import HTTPProvider, IPCProvider -from web3.exceptions import InvalidTransaction +from web3.exceptions import InvalidTransaction, TransactionNotFound from brownie._config import CONFIG, _get_data_folder from brownie._singleton import _Singleton @@ -43,6 +44,7 @@ rpc = Rpc() eth_account.Account.enable_unaudited_hdwallet_features() +_marker = deque("-/|\\-/|\\") class Accounts(metaclass=_Singleton): @@ -736,25 +738,41 @@ def _make_transaction( if to: tx["to"] = to_address(str(to)) tx = _apply_fee_to_tx(tx, gas_price, max_fee, priority_fee) - try: - txid = self._transact(tx, allow_revert) # type: ignore - exc, revert_data = None, None - except ValueError as e: - exc = VirtualMachineError(e) - if not hasattr(exc, "txid"): - raise exc from None - txid = exc.txid - revert_data = (exc.revert_msg, exc.pc, exc.revert_type) - - receipt = TransactionReceipt( - txid, - self, - silent=silent, - required_confs=required_confs, - is_blocking=False, - name=fn_name, - revert_data=revert_data, - ) + txid = None + while True: + try: + response = self._transact(tx, allow_revert) # type: ignore + exc, revert_data = None, None + if txid is None: + txid = HexBytes(response).hex() + if not silent: + print(f"\rTransaction sent: {color('bright blue')}{txid}{color}") + except ValueError as e: + if txid is None: + exc = VirtualMachineError(e) + if not hasattr(exc, "txid"): + raise exc from None + txid = exc.txid + print(f"\rTransaction sent: {color('bright blue')}{txid}{color}") + revert_data = (exc.revert_msg, exc.pc, exc.revert_type) + try: + receipt = TransactionReceipt( + txid, + self, + silent=silent, + required_confs=required_confs, + is_blocking=False, + name=fn_name, + revert_data=revert_data, + ) # type: ignore + break + except (TransactionNotFound, ValueError): + if not silent: + sys.stdout.write(f" Awaiting transaction in the mempool... {_marker[0]}\r") + sys.stdout.flush() + _marker.rotate(1) + time.sleep(1) + receipt = self._await_confirmation(receipt, required_confs, gas_strategy, gas_iter) return receipt, exc diff --git a/brownie/network/middlewares/caching.py b/brownie/network/middlewares/caching.py index fbcec5958..002da555e 100644 --- a/brownie/network/middlewares/caching.py +++ b/brownie/network/middlewares/caching.py @@ -190,11 +190,14 @@ def process_request(self, make_request: Callable, method: str, params: List) -> "eth_uninstallFilter", # used to check connectivity "web3_clientVersion", - # caching these causes weirdness with transaction replacement + # caching these causes weirdness with transaction broadcasting and replacement "eth_sendTransaction", "eth_sendRawTransaction", "eth_sign", "eth_signTransaction", + "eth_getTransactionByHash", + "eth_getTransactionReceipt", + "eth_chainId", ): return make_request(method, params) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 9737fdf1d..985331b70 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -158,8 +158,6 @@ def __init__( if isinstance(txid, bytes): txid = HexBytes(txid).hex() - if not self._silent: - print(f"\rTransaction sent: {color('bright blue')}{txid}{color}") # this event is set once the transaction is confirmed or dropped # it is used to waiting during blocking transaction actions @@ -197,7 +195,34 @@ def __init__( if self._revert_pc is not None: self._dev_revert_msg = build._get_dev_revert(self._revert_pc) or None - self._await_transaction(required_confs, is_blocking) + tx: Dict = web3.eth.get_transaction(HexBytes(self.txid)) + self._set_from_tx(tx) + + if not self._silent: + output_str = "" + if self.type == 2: + max_gas = tx["maxFeePerGas"] / 10 ** 9 + priority_gas = tx["maxPriorityFeePerGas"] / 10 ** 9 + output_str = ( + f" Max fee: {color('bright blue')}{max_gas}{color} gwei" + f" Priority fee: {color('bright blue')}{priority_gas}{color} gwei" + ) + elif self.gas_price is not None: + gas_price = self.gas_price / 10 ** 9 + output_str = f" Gas price: {color('bright blue')}{gas_price}{color} gwei" + print( + f"{output_str} Gas limit: {color('bright blue')}{self.gas_limit}{color}" + f" Nonce: {color('bright blue')}{self.nonce}{color}" + ) + + # await confirmation of tx in a separate thread which is blocking if + # required_confs > 0 or tx has already confirmed (`blockNumber` != None) + confirm_thread = threading.Thread( + target=self._await_confirmation, args=(tx["blockNumber"], required_confs), daemon=True + ) + confirm_thread.start() + if is_blocking and (required_confs > 0 or tx["blockNumber"]): + confirm_thread.join() def __repr__(self) -> str: color_str = {-2: "dark white", -1: "bright yellow", 0: "bright red", 1: ""}[self.status] @@ -422,56 +447,6 @@ def _raise_if_reverted(self, exc: Any) -> None: source=source, revert_msg=self._revert_msg, dev_revert_msg=self._dev_revert_msg ) - def _await_transaction(self, required_confs: int, is_blocking: bool) -> None: - # await tx showing in mempool - while True: - try: - tx: Dict = web3.eth.get_transaction(HexBytes(self.txid)) - break - except (TransactionNotFound, ValueError): - if self.sender is None: - # if sender was not explicitly set, this transaction was - # not broadcasted locally and so likely doesn't exist - raise - if self.nonce is not None: - sender_nonce = web3.eth.get_transaction_count(str(self.sender)) - if sender_nonce > self.nonce: - self.status = Status(-2) - return - if not self._silent: - sys.stdout.write(f" Awaiting transaction in the mempool... {_marker[0]}\r") - sys.stdout.flush() - _marker.rotate(1) - time.sleep(1) - - self._set_from_tx(tx) - - if not self._silent: - output_str = "" - if self.type == 2: - max_gas = tx["maxFeePerGas"] / 10 ** 9 - priority_gas = tx["maxPriorityFeePerGas"] / 10 ** 9 - output_str = ( - f" Max fee: {color('bright blue')}{max_gas}{color} gwei" - f" Priority fee: {color('bright blue')}{priority_gas}{color} gwei" - ) - elif self.gas_price is not None: - gas_price = self.gas_price / 10 ** 9 - output_str = f" Gas price: {color('bright blue')}{gas_price}{color} gwei" - print( - f"{output_str} Gas limit: {color('bright blue')}{self.gas_limit}{color}" - f" Nonce: {color('bright blue')}{self.nonce}{color}" - ) - - # await confirmation of tx in a separate thread which is blocking if - # required_confs > 0 or tx has already confirmed (`blockNumber` != None) - confirm_thread = threading.Thread( - target=self._await_confirmation, args=(tx["blockNumber"], required_confs), daemon=True - ) - confirm_thread.start() - if is_blocking and (required_confs > 0 or tx["blockNumber"]): - confirm_thread.join() - def _await_confirmation(self, block_number: int = None, required_confs: int = 1) -> None: # await first confirmation block_number = block_number or self.block_number @@ -512,7 +487,8 @@ def _await_confirmation(self, block_number: int = None, required_confs: int = 1) ) _marker.rotate(1) sys.stdout.flush() - time.sleep(1) + + time.sleep(1) # silence other dropped tx's immediately after confirmation to avoid output weirdness for dropped_tx in state.TxHistory().filter( diff --git a/tests/network/transaction/test_confirmation.py b/tests/network/transaction/test_confirmation.py deleted file mode 100644 index 83f8769c9..000000000 --- a/tests/network/transaction/test_confirmation.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/python3 - - -def test_await_conf_simple_xfer(accounts): - tx = accounts[0].transfer(accounts[1], "1 ether") - assert tx.status == 1 - tx._await_transaction(1, True) - - -def test_await_conf_successful_contract_call(accounts, tester): - tx = tester.revertStrings(6, {"from": accounts[1]}) - assert tx.status == 1 - tx._await_transaction(1, True) - - -def test_await_conf_failed_contract_call(accounts, tester, console_mode): - tx = tester.revertStrings(1, {"from": accounts[1]}) - assert tx.status == 0 - tx._await_transaction(1, True) - - -def test_await_conf_successful_contract_deploy(accounts, BrownieTester): - tx = BrownieTester.deploy(True, {"from": accounts[0]}).tx - assert tx.status == 1 - tx._await_transaction(1, True) - - -def test_await_conf_failed_contract_deploy(accounts, BrownieTester, console_mode): - tx = BrownieTester.deploy(False, {"from": accounts[0]}) - assert tx.status == 0 - tx._await_transaction(1, True) - - -def test_transaction_confirmations(accounts, chain): - tx = accounts[0].transfer(accounts[1], "1 ether") - assert tx.confirmations == 1 - chain.mine() - assert tx.confirmations == 2