Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support raise_on_revert=False for sending calls and transactions #177

Merged
merged 3 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
63 changes: 32 additions & 31 deletions ape_hardhat/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ContractLogicError,
OutOfGasError,
RPCTimeoutError,
SignatureError,
SubprocessError,
TransactionError,
VirtualMachineError,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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"] = {}
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading