diff --git a/src/ape/exceptions.py b/src/ape/exceptions.py index c26ee6eca5..d3af388a67 100644 --- a/src/ape/exceptions.py +++ b/src/ape/exceptions.py @@ -587,18 +587,28 @@ class ContractNotFoundError(ChainError): Raised when a contract is not found at an address. """ - def __init__(self, address: "AddressType", has_explorer: bool, network_name: str): + # 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): msg = f"Failed to get contract type for address '{address}'." - msg += ( - " Contract may need verification." - if has_explorer - else ( - f" Current network '{network_name}' has no associated " + + # 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 + + if has_explorer: + msg += " Contract may need verification." + 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 " "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) diff --git a/tests/functional/test_contract_call_handler.py b/tests/functional/test_contract_call_handler.py index f59b6f03ae..6c53aff569 100644 --- a/tests/functional/test_contract_call_handler.py +++ b/tests/functional/test_contract_call_handler.py @@ -2,6 +2,7 @@ from ape.contracts.base import ContractCallHandler from ape.exceptions import ContractNotFoundError +from ape.utils import ZERO_ADDRESS def test_struct_input( @@ -14,10 +15,11 @@ def test_struct_input( def test_call_contract_not_found(mocker, method_abi_with_struct_input, networks): (networks.ethereum.local.__dict__ or {}).pop("explorer", None) contract = mocker.MagicMock() + contract.address = ZERO_ADDRESS contract.is_contract = False method = method_abi_with_struct_input handler = ContractCallHandler(contract=contract, abis=[method]) - expected = ".*Current network 'ethereum:local:test'.*" + expected = f"Failed to get contract type for address '{ZERO_ADDRESS}'." with pytest.raises(ContractNotFoundError, match=expected): handler() @@ -25,9 +27,10 @@ def test_call_contract_not_found(mocker, method_abi_with_struct_input, networks) def test_transact_contract_not_found(mocker, owner, method_abi_with_struct_input, networks): (networks.ethereum.local.__dict__ or {}).pop("explorer", None) contract = mocker.MagicMock() + contract.address = ZERO_ADDRESS contract.is_contract = False method = method_abi_with_struct_input handler = ContractCallHandler(contract=contract, abis=[method]) - expected = ".*Current network 'ethereum:local:test'.*" + expected = rf"Failed to get contract type for address '{ZERO_ADDRESS}'\." with pytest.raises(ContractNotFoundError, match=expected): handler.transact(sender=owner) diff --git a/tests/functional/test_contracts_cache.py b/tests/functional/test_contracts_cache.py index 9e586a944d..f6f542743a 100644 --- a/tests/functional/test_contracts_cache.py +++ b/tests/functional/test_contracts_cache.py @@ -75,19 +75,34 @@ def fn(addr, default=None): @explorer_test -def test_instance_at_contract_type_not_found(chain, eth_tester_provider): +def test_instance_at_contract_type_not_found_local_network(chain, eth_tester_provider): eth_tester_provider.network.__dict__["explorer"] = None new_address = "0x4a986a6dca6dbF99Bc3D17F8d71aFB0D60E740F9" - expected = ( - rf"Failed to get contract type for address '{new_address}'. " - r"Current network 'ethereum:local:test' has no associated explorer plugin. " - "Try installing an explorer plugin using .*ape plugins install etherscan.*, " - r"or using a network with explorer support\." - ) + expected = rf"Failed to get contract type for address '{new_address}'." with pytest.raises(ContractNotFoundError, match=expected): chain.contracts.instance_at(new_address) +@explorer_test +def test_instance_at_contract_type_not_found_live_network(chain, eth_tester_provider): + eth_tester_provider.network.__dict__["explorer"] = None + real_name = eth_tester_provider.network.name + eth_tester_provider.network.name = "sepolia" + try: + new_address = "0x4a986a6dca6dbF99Bc3D17F8d71aFB0D60E740F9" + expected = ( + rf"Failed to get contract type for address '{new_address}'. " + r"Current network 'ethereum:sepolia:test' has no associated explorer plugin. " + "Try installing an explorer plugin using .*ape plugins install etherscan.*, " + r"or using a network with explorer support\." + ) + with pytest.raises(ContractNotFoundError, match=expected): + chain.contracts.instance_at(new_address) + + finally: + eth_tester_provider.network.name = real_name + + def test_instance_at_use_abi(chain, solidity_fallback_contract, owner): new_instance = owner.deploy(solidity_fallback_contract.contract_type) del chain.contracts[new_instance.address] @@ -161,14 +176,20 @@ def test_cache_default_contract_type_when_used(solidity_contract_instance, chain def test_contracts_getitem_contract_not_found(chain, eth_tester_provider): eth_tester_provider.network.__dict__["explorer"] = None new_address = "0x4a986a6dca6dbF99Bc3D17F8d71aFB0D60E740F9" - expected = ( - rf"Failed to get contract type for address '{new_address}'. " - r"Current network 'ethereum:local:test' has no associated explorer plugin. " - "Try installing an explorer plugin using .*ape plugins install etherscan.*, " - r"or using a network with explorer support\." - ) - with pytest.raises(KeyError, match=expected): - _ = chain.contracts[new_address] + real_name = eth_tester_provider.network.name + eth_tester_provider.network.name = "sepolia" + try: + expected = ( + rf"Failed to get contract type for address '{new_address}'. " + r"Current network 'ethereum:sepolia:test' has no associated explorer plugin. " + "Try installing an explorer plugin using .*ape plugins install etherscan.*, " + r"or using a network with explorer support\." + ) + with pytest.raises(KeyError, match=expected): + _ = chain.contracts[new_address] + + finally: + eth_tester_provider.network.name = real_name def test_deployments_mapping_cache_location(chain): diff --git a/tests/functional/test_exceptions.py b/tests/functional/test_exceptions.py index 55f3ca9d36..cda512b95f 100644 --- a/tests/functional/test_exceptions.py +++ b/tests/functional/test_exceptions.py @@ -5,14 +5,17 @@ import pytest from ape.api import ReceiptAPI +from ape.api.networks import LOCAL_NETWORK_NAME from ape.exceptions import ( Abort, ContractLogicError, + ContractNotFoundError, NetworkNotFoundError, TransactionError, handle_ape_exception, ) from ape.types import SourceTraceback +from ape.utils import ZERO_ADDRESS from ape_ethereum.transactions import DynamicFeeTransaction, Receipt @@ -224,3 +227,23 @@ def revert_type(self) -> Optional[str]: actual = error.message expected = "CUSTOM_ERROR" assert actual == expected + + +class TestContractNotFoundError: + def test_local_network(self): + """ + 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}'." + + def test_fork_network(self): + err = ContractNotFoundError(ZERO_ADDRESS, False, "ethereum:sepolia-fork:test") + assert str(err) == ( + f"Failed to get contract type for address '{ZERO_ADDRESS}'. " + "Current network 'ethereum:sepolia-fork:test' 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." + )