diff --git a/docs/userguides/contracts.md b/docs/userguides/contracts.md index c13861aea3..c06c0f4f4c 100644 --- a/docs/userguides/contracts.md +++ b/docs/userguides/contracts.md @@ -111,7 +111,18 @@ from ape import Contract contract = Contract("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45") ``` -It will fetch the `contract-type` using the explorer plugin from the active network, such as [ape-etherscan](https://github.com/ApeWorX/ape-etherscan). +If the contract ABI and/or code is cached on disk or in memory (such as from a previous deploy or retrieval), it will use it. +Otherwise, it will fetch the `ContractType` using the explorer plugin from the active network, such as [ape-etherscan](https://github.com/ApeWorX/ape-etherscan). + +To avoid fetching the contract from an explorer such as Etherscan, use `fetch_from_explorer=False`. + +```python +from ape import Contract + +contract = Contract("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", fetch_from_explorer=False) +``` + +This also avoids checking for an updated `ContractType` and forces Ape to only use types cached to disk or in memory. If you have the [ENS plugin](https://github.com/ApeWorX/ape-ens) installed, you can use `.eth` domain names as the argument: diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py index 3e0b27145b..62fe4f7108 100644 --- a/src/ape/contracts/base.py +++ b/src/ape/contracts/base.py @@ -1407,7 +1407,10 @@ def deployments(self): return self.chain_manager.contracts.get_deployments(self) def at( - self, address: "AddressType", txn_hash: Optional[Union[str, HexBytes]] = None + self, + address: "AddressType", + txn_hash: Optional[Union[str, HexBytes]] = None, + fetch_from_explorer: bool = True, ) -> ContractInstance: """ Get a contract at the given address. @@ -1425,13 +1428,16 @@ def at( a different ABI than :attr:`~ape.contracts.ContractContainer.contract_type`. txn_hash (Union[str, HexBytes]): The hash of the transaction that deployed the contract, if available. Defaults to ``None``. + fetch_from_explorer (bool): Set to ``False`` to avoid fetching from an explorer. Returns: :class:`~ape.contracts.ContractInstance` """ - return self.chain_manager.contracts.instance_at( - address, self.contract_type, txn_hash=txn_hash + address, + self.contract_type, + txn_hash=txn_hash, + fetch_from_explorer=fetch_from_explorer, ) @cached_property diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index e5e99a087d..24acd401f3 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -1029,7 +1029,10 @@ def get_contract_type(addr: AddressType): @nonreentrant(key_fn=lambda *args, **kwargs: args[1]) def get( - self, address: AddressType, default: Optional[ContractType] = None + self, + address: AddressType, + default: Optional[ContractType] = None, + fetch_from_explorer: bool = True, ) -> Optional[ContractType]: """ Get a contract type by address. @@ -1041,6 +1044,9 @@ def get( address (AddressType): The address of the contract. default (Optional[ContractType]): A default contract when none is found. Defaults to ``None``. + fetch_from_explorer (bool): Set to ``False`` to avoid fetching from an + explorer. Defaults to ``True``. Only fetches if it needs to (uses disk + & memory caching otherwise). Returns: Optional[ContractType]: The contract type if it was able to get one, @@ -1095,7 +1101,8 @@ def get( return default # Also gets cached to disk for faster lookup next time. - contract_type = self._get_contract_type_from_explorer(address_key) + if fetch_from_explorer: + contract_type = self._get_contract_type_from_explorer(address_key) # Cache locally for faster in-session look-up. if contract_type: @@ -1136,6 +1143,7 @@ def instance_at( contract_type: Optional[ContractType] = None, txn_hash: Optional[Union[str, "HexBytes"]] = None, abi: Optional[Union[list[ABI], dict, str, Path]] = None, + fetch_from_explorer: bool = True, ) -> ContractInstance: """ Get a contract at the given address. If the contract type of the contract is known, @@ -1157,6 +1165,9 @@ def instance_at( deploying the contract, if known. Useful for publishing. Defaults to ``None``. abi (Optional[Union[list[ABI], dict, str, Path]]): Use an ABI str, dict, path, or ethpm models to create a contract instance class. + fetch_from_explorer (bool): Set to ``False`` to avoid fetching from the explorer. + Defaults to ``True``. Won't fetch unless it needs to (uses disk & memory caching + first). Returns: :class:`~ape.contracts.base.ContractInstance` @@ -1172,7 +1183,9 @@ def instance_at( try: # Always attempt to get an existing contract type to update caches - contract_type = self.get(contract_address, default=contract_type) + contract_type = self.get( + contract_address, default=contract_type, fetch_from_explorer=fetch_from_explorer + ) except Exception as err: if contract_type or abi: # If a default contract type was provided, don't error and use it. diff --git a/tests/functional/test_contract_container.py b/tests/functional/test_contract_container.py index 29fbde0252..45e74087bc 100644 --- a/tests/functional/test_contract_container.py +++ b/tests/functional/test_contract_container.py @@ -16,8 +16,6 @@ def test_deploy( not_owner, contract_container, networks_connected_to_tester, - project, - chain, clean_contracts_cache, ): contract = contract_container.deploy(4, sender=not_owner, something_else="IGNORED") @@ -35,8 +33,6 @@ def test_deploy_wrong_number_of_arguments( not_owner, contract_container, networks_connected_to_tester, - project, - chain, clean_contracts_cache, ): expected = ( @@ -163,3 +159,32 @@ def test_source_id(contract_container): expected = contract_container.contract_type.source_id # Is just a pass-through (via extras-model), but making sure it works. assert actual == expected + + +def test_at(vyper_contract_instance, vyper_contract_container): + instance = vyper_contract_container.at(vyper_contract_instance.address) + assert instance == vyper_contract_instance + + +def test_at_fetch_from_explorer_false( + project_with_contract, mock_explorer, eth_tester_provider, owner +): + # Grab the container - note: this always compiles! + container = project_with_contract.Contract + instance = container.deploy(sender=owner) + + # Hack off the fact that it was compiled. + project_with_contract.clean() + + # Simulate having an explorer plugin installed (e.g. ape-etherscan). + eth_tester_provider.network.explorer = mock_explorer + + # Attempt to create an instance. It should NOT use the explorer at all! + instance2 = container.at(instance.address, fetch_from_explorer=False) + + assert instance == instance2 + # Ensure explorer was not used at all. + assert mock_explorer.get_contract_type.call_count == 0 + + # Clean up test. + eth_tester_provider.network.explorer = None diff --git a/tests/functional/test_contracts_cache.py b/tests/functional/test_contracts_cache.py index f6f542743a..307668289b 100644 --- a/tests/functional/test_contracts_cache.py +++ b/tests/functional/test_contracts_cache.py @@ -55,7 +55,7 @@ def test_instance_at_uses_given_contract_type_when_retrieval_fails(mocker, chain expected_fail_message = "LOOK_FOR_THIS_FAIL_MESSAGE" existing_fn = chain.contracts.get - def fn(addr, default=None): + def fn(addr, default=None, **kwargs): if addr == new_address: raise ValueError(expected_fail_message)