Skip to content

Commit

Permalink
fix: issue using default call-tree approach when call fails (#2128)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Jun 11, 2024
1 parent 7ec8842 commit e0e57d4
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 94 deletions.
13 changes: 6 additions & 7 deletions src/ape/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,6 @@ def address(self) -> Optional["AddressType"]:

return receiver

return (
self.contract_address
or getattr(self.txn, "receiver", None)
or getattr(self.txn, "contract_address", None)
)

@cached_property
def contract_type(self) -> Optional[ContractType]:
if not (address := self.address):
Expand Down Expand Up @@ -821,7 +815,12 @@ def __repr__(self) -> str:
def _get_ape_traceback_from_tx(txn: FailedTxn) -> Optional["SourceTraceback"]:
from ape.api.transactions import ReceiptAPI

receipt: "ReceiptAPI" = txn if isinstance(txn, ReceiptAPI) else txn.receipt # type: ignore
try:
receipt: "ReceiptAPI" = txn if isinstance(txn, ReceiptAPI) else txn.receipt # type: ignore
except Exception:
# Receipt not real enough, maybe was a re-played call.
return None

if not receipt:
return None

Expand Down
12 changes: 9 additions & 3 deletions src/ape_ethereum/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from rich.tree import Tree

from ape.api import EcosystemAPI, TraceAPI, TransactionAPI
from ape.exceptions import ProviderError, TransactionNotFoundError
from ape.exceptions import ContractLogicError, ProviderError, TransactionNotFoundError
from ape.logging import logger
from ape.types import AddressType, ContractFunctionPath, GasReport
from ape.utils import ZERO_ADDRESS, is_evm_precompile, is_zero_hex
Expand Down Expand Up @@ -433,8 +433,14 @@ def _get_basic_calltree(self) -> CallTreeNode:

# Figure out the 'returndata' using 'eth_call' RPC.
tx = receipt.transaction.model_copy(update={"nonce": None})
return_value = self.provider.send_call(tx, block_id=receipt.block_number)
init_kwargs["returndata"] = return_value

try:
return_value = self.provider.send_call(tx, block_id=receipt.block_number)
except ContractLogicError:
# Unable to get the return value because even as a call, it fails.
pass
else:
init_kwargs["returndata"] = return_value

return CallTreeNode(**init_kwargs)

Expand Down
185 changes: 102 additions & 83 deletions tests/functional/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,105 @@

from ape.api import ReceiptAPI
from ape.exceptions import Abort, NetworkNotFoundError, TransactionError
from ape_ethereum.transactions import Receipt


def test_abort():
expected = re.compile(r"Operation aborted in test_exceptions.py::test_abort on line \d+\.")
assert expected.match(str(Abort()))


def test_transaction_error_when_receipt_is_subclass(vyper_contract_instance, owner):
"""
Ensure TransactionError knows subclass Receipts are still receipts.
(There was a bug once when it didn't, and that caused internal AttributeErrors).
"""

class SubclassReceipt(Receipt):
pass

receipt = vyper_contract_instance.setNumber(123, sender=owner)
receipt_data = {**receipt.model_dump(), "transaction": receipt.transaction}
sub_receipt = SubclassReceipt.model_validate(receipt_data)

err = TransactionError(txn=sub_receipt)
assert isinstance(err.txn, ReceiptAPI) # Same check used.


def test_network_not_found_error_close_match():
net = "sepolai"
error = NetworkNotFoundError(net, ecosystem="ethereum", options=("sepolia",))
actual = str(error)
expected = f"No network in 'ethereum' named '{net}'. Did you mean 'sepolia'?"
assert actual == expected


def test_network_not_found_error_no_close_matches():
net = "madeup"
error = NetworkNotFoundError(net, ecosystem="ethereum", options=("sepolia",))
actual = str(error)
expected = f"No network in 'ethereum' named '{net}'. Options:\nsepolia"
assert actual == expected


def test_network_with_ecosystem_not_found_no_options():
net = "madeup"
error = NetworkNotFoundError(net, ecosystem="ethereum", options=())
actual = str(error)
expected = "'ethereum' has no networks."
assert actual == expected


def test_network_without_ecosystem_not_found_no_options():
net = "madeup"
error = NetworkNotFoundError(net, options=())
actual = str(error)
expected = "No networks found."
assert actual == expected


def test_transaction_error_address(owner):
err = TransactionError(contract_address=owner.address)
assert err.address == owner.address


def test_transaction_error_receiver_as_address(owner):
tx = owner.transfer(owner, "1 wei")
err = TransactionError(txn=tx)
assert err.address == owner.address


def test_transaction_error_deploy_address_as_address(
owner, ethereum, vyper_contract_container, zero_address
):
contract = vyper_contract_container.deploy(629, sender=owner)

receipt = contract.creation_metadata.receipt
data = receipt.model_dump(exclude=("transaction",))
# Show when receier is zero_address, it still picks contract address.
data["transaction"] = ethereum.create_transaction(receiver=zero_address)

tx = Receipt.model_validate(data)
assert tx.receiver == zero_address, "setup failed"

err = TransactionError(txn=tx)
assert err.address == contract.address
from ape_ethereum.transactions import DynamicFeeTransaction, Receipt


class TestAbort:
def test_shows_line_number(self):
actual = str(Abort())
expected = re.compile(r"Operation aborted in [\w<>.]*::[\w<>]* on line \d+\.")
assert expected.match(actual)


class TestTransactionError:
def test_receipt_is_subclass(self, vyper_contract_instance, owner):
"""
Ensure TransactionError knows subclass Receipts are still receipts.
(There was a bug once when it didn't, and that caused internal AttributeErrors).
"""

class SubclassReceipt(Receipt):
pass

receipt = vyper_contract_instance.setNumber(123, sender=owner)
receipt_data = {**receipt.model_dump(), "transaction": receipt.transaction}
sub_receipt = SubclassReceipt.model_validate(receipt_data)

err = TransactionError(txn=sub_receipt)
assert isinstance(err.txn, ReceiptAPI) # Same check used.

def test_address(self, owner):
err = TransactionError(contract_address=owner.address)
assert err.address == owner.address

def test_receiver_as_address(self, owner):
tx = owner.transfer(owner, "1 wei")
err = TransactionError(txn=tx)
assert err.address == owner.address

def test_deploy_address_as_address(
self, owner, ethereum, vyper_contract_container, zero_address
):
contract = vyper_contract_container.deploy(629, sender=owner)

receipt = contract.creation_metadata.receipt
data = receipt.model_dump(exclude=("transaction",))
# Show when receiver is zero_address, it still picks contract address.
data["transaction"] = ethereum.create_transaction(receiver=zero_address)

tx = Receipt.model_validate(data)
assert tx.receiver == zero_address, "setup failed"

err = TransactionError(txn=tx)
assert err.address == contract.address

def test_call(self):
"""
Simulating a failing-call, making sure it doesn't
blow up if it doesn't get a source-tb.
"""
data = {
"chainId": 1337,
"to": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"gas": 30029122,
"value": 0,
"data": "0xce50aa7d00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000000000000000000001", # noqa: E501
"type": 2,
"maxFeePerGas": 875000000,
"maxPriorityFeePerGas": 0,
"accessList": [],
}
failing_call = DynamicFeeTransaction.model_validate(data)
err = TransactionError(txn=failing_call)
assert err.source_traceback is None


class TestNetworkNotFoundError:
def test_close_match(self):
net = "sepolai"
error = NetworkNotFoundError(net, ecosystem="ethereum", options=("sepolia",))
actual = str(error)
expected = f"No network in 'ethereum' named '{net}'. Did you mean 'sepolia'?"
assert actual == expected

def test_no_close_matches(self):
net = "madeup"
error = NetworkNotFoundError(net, ecosystem="ethereum", options=("sepolia",))
actual = str(error)
expected = f"No network in 'ethereum' named '{net}'. Options:\nsepolia"
assert actual == expected

def test_ecosystem_no_network_options(self):
net = "madeup"
error = NetworkNotFoundError(net, ecosystem="ethereum", options=())
actual = str(error)
expected = "'ethereum' has no networks."
assert actual == expected

def test_no_options(self):
net = "madeup"
error = NetworkNotFoundError(net, options=())
actual = str(error)
expected = "No networks found."
assert actual == expected
31 changes: 30 additions & 1 deletion tests/functional/test_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from evm_trace import CallTreeNode, CallType
from hexbytes import HexBytes

from ape_ethereum.trace import CallTrace, Trace, TransactionTrace, parse_rich_tree
from ape.exceptions import ContractLogicError
from ape_ethereum.trace import CallTrace, Trace, TraceApproach, TransactionTrace, parse_rich_tree
from tests.functional.data.python import (
TRACE_MISSING_GAS,
TRACE_WITH_CUSTOM_ERROR,
Expand Down Expand Up @@ -158,6 +159,34 @@ def test_transaction_trace_list_of_lists(vyper_contract_instance, owner):
assert re.match(expected.strip(), actual.strip())


def test_transaction_trace_basic_approach_on_failed_call(chain, vyper_contract_instance, not_owner):
"""
Show we can use the basic approach for failed calls.
"""
# Get a failed tx
tx = None
try:
vyper_contract_instance.setNumber(0, sender=not_owner)
except ContractLogicError as err:
tx = err.txn

assert tx is not None, "Setup failed - could not get a failed txn."

trace = TransactionTrace.model_validate(
{
"call_trace_approach": None,
"debug_trace_transaction_parameters": {"enableMemory": True},
"transaction_hash": tx.txn_hash,
"transaction": tx,
}
)
trace.call_trace_approach = TraceApproach.BASIC
actual = trace.get_calltree()
# Mostly just checking that it did not fail!
assert actual is not None
assert isinstance(actual, CallTreeNode)


def test_call_trace_debug_trace_call_not_supported(owner, vyper_contract_instance):
"""
When using EthTester, we can still see the top-level trace of a call.
Expand Down

0 comments on commit e0e57d4

Please sign in to comment.