diff --git a/setup.py b/setup.py index c59895aa4b..8ae85ec8d5 100644 --- a/setup.py +++ b/setup.py @@ -121,6 +121,7 @@ # ** Dependencies maintained by Ethereum Foundation ** "eth-abi>=5.1.0,<6", "eth-account>=0.13.4,<0.14", + "eth-tester>=0.12.0b2,<0.13", # Peer: stricter pin needed for [tester]. "eth-typing>=5.0.1,<6", "eth-utils>=5.1.0,<6", "hexbytes>=1.2.1,<2", diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py index 62fe4f7108..99aedca7c1 100644 --- a/src/ape/contracts/base.py +++ b/src/ape/contracts/base.py @@ -257,11 +257,7 @@ def decode_input(self, calldata: bytes) -> tuple[str, dict[str, Any]]: def _validate_is_contract(self): if not self.contract.is_contract: - raise ContractNotFoundError( - self.contract.address, - self.provider.network.explorer is not None, - self.provider.network_choice, - ) + raise ContractNotFoundError(self.contract.address, provider=self.provider) class ContractCallHandler(ContractMethodHandler): diff --git a/src/ape/exceptions.py b/src/ape/exceptions.py index aabdc51ae3..c181ced3ba 100644 --- a/src/ape/exceptions.py +++ b/src/ape/exceptions.py @@ -22,7 +22,7 @@ from ethpm_types.contract_type import ContractType from ape.api.networks import NetworkAPI - from ape.api.providers import SubprocessProvider + from ape.api.providers import ProviderAPI, SubprocessProvider from ape.api.trace import TraceAPI from ape.api.transactions import ReceiptAPI, TransactionAPI from ape.managers.project import ProjectManager @@ -590,29 +590,39 @@ class ContractNotFoundError(ChainError): Raised when a contract is not found at an address. """ - # TODO: In 0.9, pass in provider object directly (instead of network choice + name) - def __init__(self, address: "AddressType", has_explorer: bool, network_choice: str): + def __init__(self, address: "AddressType", provider: Optional["ProviderAPI"] = None): + try: + msg = self._create_message(address, provider=provider) + except Exception as err: + # Don't let errors occurring within exception handling to + # ruin the exception completely. + logger.error(f"Failed to create proper error message because of: {err}") + msg = f"Failed to get contract type for address '{address}'." + + super().__init__(msg) + + @classmethod + def _create_message( + cls, address: "AddressType", provider: Optional["ProviderAPI"] = None + ) -> str: msg = f"Failed to get contract type for address '{address}'." + if not provider: + return msg - # NOTE: Network name is optional to avoid breaking change. - choice_parts = network_choice.split(":") - if len(choice_parts) > 1: - network_name = network_choice.split(":")[1] - else: - network_name = network_choice + network_name = provider.network_choice.split(":")[1] - if has_explorer: - msg += " Contract may need verification." + if provider.network.explorer: + msg += " Contract may need verification (if relying on an explorer)." elif network_name != "local": # Only bother mentioning explorer plugins if we are not the local network. msg += ( - f" Current network '{network_choice}' has no associated " + f" Current network '{provider.network_choice}' has no associated " "explorer plugin. Try installing an explorer plugin using " f"{click.style(text='ape plugins install etherscan', fg='green')}, " "or using a network with explorer support." ) - super().__init__(msg) + return msg class UnknownSnapshotError(ChainError): diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index be87fb5ed4..b344329607 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -972,9 +972,7 @@ def __getitem__(self, address: AddressType) -> ContractType: contract_type = self.get(address) if not contract_type: # Create error message from custom exception cls. - err = ContractNotFoundError( - address, self.provider.network.explorer is not None, self.provider.network_choice - ) + err = ContractNotFoundError(address, provider=self.provider) # Must raise KeyError. raise KeyError(str(err)) @@ -1242,11 +1240,7 @@ def instance_at( ) if not contract_type: - raise ContractNotFoundError( - contract_address, - self.provider.network.explorer is not None, - self.provider.network_choice, - ) + raise ContractNotFoundError(contract_address, provider=self.provider) elif not isinstance(contract_type, ContractType): raise TypeError( diff --git a/src/ape_ethereum/provider.py b/src/ape_ethereum/provider.py index 6279c813f7..6dccf3cf49 100644 --- a/src/ape_ethereum/provider.py +++ b/src/ape_ethereum/provider.py @@ -1593,8 +1593,9 @@ def ots_get_contract_creator(self, address: "AddressType") -> Optional[dict]: result = self.make_request("ots_getContractCreator", [address]) if result is None: - # NOTE: Skip the explorer part of the error message via `has_explorer=True`. - raise ContractNotFoundError(address, True, self.network_choice) + # Don't pass provider so the error message is simplifer in this case + # (avoids mentioning explorer plugins). + raise ContractNotFoundError(address) return result diff --git a/tests/functional/test_contract_container.py b/tests/functional/test_contract_container.py index 45e74087bc..3ebe70325e 100644 --- a/tests/functional/test_contract_container.py +++ b/tests/functional/test_contract_container.py @@ -10,6 +10,7 @@ ProjectError, ) from ape_ethereum.ecosystem import ProxyType +from tests.conftest import explorer_test def test_deploy( @@ -166,6 +167,7 @@ def test_at(vyper_contract_instance, vyper_contract_container): assert instance == vyper_contract_instance +@explorer_test def test_at_fetch_from_explorer_false( project_with_contract, mock_explorer, eth_tester_provider, owner ): @@ -188,3 +190,4 @@ def test_at_fetch_from_explorer_false( # Clean up test. eth_tester_provider.network.explorer = None + assert eth_tester_provider.network.explorer is None diff --git a/tests/functional/test_exceptions.py b/tests/functional/test_exceptions.py index d6ccf200eb..55a17ddcb5 100644 --- a/tests/functional/test_exceptions.py +++ b/tests/functional/test_exceptions.py @@ -15,7 +15,7 @@ handle_ape_exception, ) from ape.types.trace import SourceTraceback -from ape.utils.misc import LOCAL_NETWORK_NAME, ZERO_ADDRESS +from ape.utils.misc import ZERO_ADDRESS from ape_ethereum.transactions import DynamicFeeTransaction, Receipt @@ -230,20 +230,27 @@ def revert_type(self) -> Optional[str]: class TestContractNotFoundError: - def test_local_network(self): + def test_local_network(self, eth_tester_provider): """ Testing we are NOT mentioning explorer plugins for the local-network, as 99.9% of the time it is confusing. """ - err = ContractNotFoundError(ZERO_ADDRESS, False, f"ethereum:{LOCAL_NETWORK_NAME}:test") - assert str(err) == f"Failed to get contract type for address '{ZERO_ADDRESS}'." + eth_tester_provider.network.explorer = None # Ensure no explorer is set. + err = ContractNotFoundError(ZERO_ADDRESS, provider=eth_tester_provider) + actual = f"{err}" + expected = f"Failed to get contract type for address '{ZERO_ADDRESS}'." + assert actual == expected - def test_fork_network(self): - err = ContractNotFoundError(ZERO_ADDRESS, False, "ethereum:sepolia-fork:test") + def test_fork_network(self, mocker, mock_sepolia): + provider = mocker.MagicMock() + provider.network = mock_sepolia + mock_sepolia.explorer = None + provider.network_choice = "ethereum:sepolia:node" + err = ContractNotFoundError(ZERO_ADDRESS, provider=provider) assert str(err) == ( f"Failed to get contract type for address '{ZERO_ADDRESS}'. " - "Current network 'ethereum:sepolia-fork:test' has no associated explorer plugin. " + "Current network 'ethereum:sepolia:node' has no associated explorer plugin. " "Try installing an explorer plugin using \x1b[32mape plugins install etherscan" "\x1b[0m, or using a network with explorer support." )