diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d7770d4..a921fc9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,12 +16,12 @@ repos: name: black - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.11.0 hooks: - id: mypy additional_dependencies: [types-PyYAML, types-requests, types-setuptools, pydantic] diff --git a/ape_hardhat/provider.py b/ape_hardhat/provider.py index 0fb13a2..37b84bd 100644 --- a/ape_hardhat/provider.py +++ b/ape_hardhat/provider.py @@ -19,6 +19,7 @@ ContractLogicError, OutOfGasError, RPCTimeoutError, + SignatureError, SubprocessError, TransactionError, VirtualMachineError, @@ -28,6 +29,7 @@ from ape.utils import DEFAULT_TEST_HD_PATH, cached_property from ape_ethereum.provider import Web3Provider from ape_ethereum.trace import TraceApproach, TransactionTrace +from ape_ethereum.transactions import TransactionStatusEnum from ape_test import ApeTestConfig from chompjs import parse_js_object # type: ignore from eth_pydantic_types import HexBytes @@ -761,7 +763,6 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: Creates a new message call transaction or a contract creation for signed transactions. """ - sender = txn.sender if sender: sender = self.conversion_manager.convert(txn.sender, AddressType) @@ -775,6 +776,7 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: txn_dict["type"] = HexBytes(txn_dict["type"]).hex() txn_params = cast(TxParams, txn_dict) + vm_err = None try: txn_hash = self.web3.eth.send_transaction(txn_params) except ValueError as err: @@ -795,43 +797,42 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: else: tx = txn - raise self.get_virtual_machine_error(err, txn=tx) from err + vm_err = self.get_virtual_machine_error(err, txn=tx) + if txn.raise_on_revert: + raise vm_err from err - receipt = self.get_receipt( - txn_hash.hex(), required_confirmations=txn.required_confirmations or 0, txn=txn_dict - ) - receipt.raise_for_status() + try: + txn_hash = txn.txn_hash + except SignatureError: + txn_hash = None + + required_confirmations = txn.required_confirmations or 0 + if txn_hash is not None: + receipt = self.get_receipt( + txn_hash.hex(), required_confirmations=required_confirmations, txn=txn_dict + ) + if vm_err: + receipt.error = vm_err + if txn.raise_on_revert: + receipt.raise_for_status() + + else: + # If we get here, likely was a failed (but allowed-fail) + # impersonated-sender receipt. + receipt = self._create_receipt( + block_number=-1, # Not in a block. + error=vm_err, + required_confirmations=required_confirmations, + status=TransactionStatusEnum.FAILING, + txn_hash="", # No hash exists, likely from impersonated sender. + **txn_dict, + ) else: receipt = super().send_transaction(txn) return receipt - # def get_receipt( - # self, - # txn_hash: str, - # required_confirmations: int = 0, - # timeout: Optional[int] = None, - # **kwargs, - # ) -> ReceiptAPI: - # try: - # # Try once without waiting first. - # # NOTE: This is required for txn sent with an impersonated account. - # receipt_data = dict(self.web3.eth.get_transaction_receipt(HexStr(txn_hash))) - # except Exception: - # return super().get_receipt( - # txn_hash, required_confirmations=required_confirmations, timeout=timeout - # ) - # - # txn = kwargs.get("txn", dict(self.web3.eth.get_transaction(HexStr(txn_hash)))) - # data: dict = {"txn_hash": txn_hash, **receipt_data, **txn} - # if "gas_price" not in data: - # data["gas_price"] = self.gas_price - # - # receipt = self.network.ecosystem.decode_receipt(data) - # self.chain_manager.history.append(receipt) - # return receipt - def get_transaction_trace(self, transaction_hash: str, **kwargs) -> TraceAPI: if "debug_trace_transaction_parameters" not in kwargs: kwargs["debug_trace_transaction_parameters"] = {} diff --git a/setup.py b/setup.py index 7a3fec9..8c4cabe 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ ], "lint": [ "black>=24.4.2,<25", # Auto-formatter and linter - "mypy>=1.10.0,<2", # Static type analyzer + "mypy>=1.11.0,<2", # Static type analyzer "types-setuptools", # Needed for mypy type shed "types-requests", # Needed for mypy type shed "types-PyYAML", # Needed for mypy type shed - "flake8>=7.0.0,<8", # Style linter + "flake8>=7.1.0,<8", # Style linter "flake8-breakpoint>=1.1.0,<2", # Detect breakpoints left in code "flake8-print>=5.0.0,<6", # Detect print statements left in code "isort>=5.13.2,<6", # Import sorting linter @@ -76,7 +76,7 @@ url="https://github.com/ApeWorX/ape-hardhat", include_package_data=True, install_requires=[ - "eth-ape>=0.8.9,<0.9", + "eth-ape>=0.8.10,<0.9", "ethpm-types", # Use same version as eth-ape "evm-trace", # Use same version as eth-ape "web3", # Use same version as eth-ape diff --git a/tests/test_provider.py b/tests/test_provider.py index 8d9860b..8711570 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -221,6 +221,21 @@ def test_revert_error_from_impersonated_account(error_contract, accounts): # because the account is impersonated. assert err.value.txn.txn_hash.startswith("0x") + # Show we can "allow" reverts using impersonated accounts. + # NOTE: This is extra because of the lack of tx-hash available. + receipt = error_contract.withdraw(sender=account, raise_on_revert=False) + assert receipt.failed + + +def test_revert_allow(error_contract, not_owner, contract_instance): + # 'sender' is not the owner so it will revert (with a message) + receipt = error_contract.withdraw(sender=not_owner, raise_on_revert=False) + assert receipt.error is not None + assert isinstance(receipt.error, error_contract.Unauthorized) + + # Ensure this also works for calls. + contract_instance.setNumber.call(5, raise_on_revert=False) + @pytest.mark.parametrize("host", ("https://example.com", "example.com")) def test_host(project, networks, host):