Skip to content

refactor!: move contract instance and container helper utilities to chain.contracts #898

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

Merged
merged 10 commits into from
Jul 24, 2022
2 changes: 1 addition & 1 deletion src/ape/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"""The currently active project. See :class:`ape.managers.project.ProjectManager`."""

Contract = chain.contracts.instance_at
"""User-facing class for instantiating contracts. See :class:`ape.contracts.base._Contract`."""
"""User-facing class for instantiating contracts."""

convert = _ManagerAccessMixin.conversion_manager.convert
"""Conversion utility function. See :class:`ape.managers.converters.ConversionManager`."""
Expand Down
13 changes: 6 additions & 7 deletions src/ape/api/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,14 @@ def deploy(self, contract: "ContractContainer", *args, **kwargs) -> "ContractIns
if not receipt.contract_address:
raise AccountsError(f"'{receipt.txn_hash}' did not create a contract.")

address = click.style(receipt.contract_address, bold=True)
address = self.provider.network.ecosystem.decode_address(receipt.contract_address)
styled_address = click.style(receipt.contract_address, bold=True)
contract_name = contract.contract_type.name or "<Unnamed Contract>"
logger.success(f"Contract '{contract_name}' deployed to: {address}")

contract_instance = self.create_contract(
address=receipt.contract_address, # type: ignore
contract_type=contract.contract_type,
logger.success(f"Contract '{contract_name}' deployed to: {styled_address}")
contract_instance = self.chain_manager.contracts.instance_at(
address, contract.contract_type
)
self.chain_manager.contracts[contract_instance.address] = contract_instance.contract_type
self.chain_manager.contracts[address] = contract_instance.contract_type
return contract_instance

def check_signature(
Expand Down
3 changes: 2 additions & 1 deletion src/ape/api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ def __getattr__(self, contract_name: str) -> "ContractContainer":
def get(self, contract_name: str) -> Optional["ContractContainer"]:
manifest = self.extract_manifest()
if hasattr(manifest, contract_name):
return self.create_contract_container(contract_type=getattr(manifest, contract_name))
contract_type = getattr(manifest, contract_name)
return self.chain_manager.contracts.get_container(contract_type)

return None

Expand Down
12 changes: 10 additions & 2 deletions src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,17 @@ def get_balance(self, address: str) -> int:
def get_code(self, address: str) -> bytes:
return self.web3.eth.get_code(address)

def get_storage_at(self, address: str, slot: int, *args, **kwargs) -> bytes:
def get_storage_at(self, address: str, slot: int, **kwargs) -> bytes:
block_id = kwargs.pop("block_identifier", None)
return self.web3.eth.get_storage_at(address, slot, block_identifier=block_id)
try:
return self.web3.eth.get_storage_at(
address, slot, block_identifier=block_id
) # type: ignore
except ValueError as err:
if "RPC Endpoint has not been implemented" in str(err):
raise APINotImplementedError(str(err)) from err

raise # Raise original error

def send_call(self, txn: TransactionAPI, **kwargs) -> bytes:
try:
Expand Down
4 changes: 2 additions & 2 deletions src/ape/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,13 @@ def _load_contracts(ctx, param, value) -> Optional[Union[ContractType, List[Cont
# and therefore we should also return a list.
is_multiple = isinstance(value, (tuple, list))

def create_contract(contract_name: str) -> ContractType:
def get_contract(contract_name: str) -> ContractType:
if contract_name not in project.contracts:
raise ContractError(f"No contract named '{value}'")

return project.contracts[contract_name]

return [create_contract(c) for c in value] if is_multiple else create_contract(value)
return [get_contract(c) for c in value] if is_multiple else get_contract(value)


def contract_option(help=None, required=False, multiple=False):
Expand Down
7 changes: 2 additions & 5 deletions src/ape/contracts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ def deployments(self):

return self.chain_manager.contracts.get_deployments(self)

def at(self, address: str) -> ContractInstance:
def at(self, address: AddressType) -> ContractInstance:
"""
Get a contract at the given address.

Expand All @@ -729,10 +729,7 @@ def at(self, address: str) -> ContractInstance:
:class:`~ape.contracts.ContractInstance`
"""

return self.create_contract(
address=address, # type: ignore
contract_type=self.contract_type,
)
return self.chain_manager.contracts.instance_at(address, self.contract_type)

def __call__(self, *args, **kwargs) -> TransactionAPI:
args = self.conversion_manager.convert(args, tuple)
Expand Down
43 changes: 24 additions & 19 deletions src/ape/managers/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pandas as pd
from ethpm_types import ContractType

from ape.api import Address, BlockAPI, ReceiptAPI
from ape.api import BlockAPI, ReceiptAPI
from ape.api.address import BaseAddress
from ape.api.networks import LOCAL_NETWORK_NAME, NetworkAPI, ProxyInfoAPI
from ape.api.query import BlockQuery, validate_and_expand_columns
Expand Down Expand Up @@ -576,16 +576,27 @@ def get(

return contract_type

def get_container(self, contract_type: ContractType) -> ContractContainer:
"""
Get a contract container for the given contract type.

Args:
contract_type (ContractType): The contract type to wrap.

Returns:
ContractContainer: A container object you can deploy.
"""

return ContractContainer(contract_type)

def instance_at(
self, address: Union[str, "AddressType"], contract_type: Optional[ContractType] = None
) -> BaseAddress:
) -> ContractInstance:
"""
Get a contract at the given address. If the contract type of the contract is known,
either from a local deploy or a :class:`~ape.api.explorers.ExplorerAPI`, it will use that
contract type. You can also provide the contract type from which it will cache and use
next time. If the contract type is not known, returns a
:class:`~ape.api.address.BaseAddress` object; otherwise returns a
:class:`~ape.contracts.ContractInstance` (subclass).
next time.

Raises:
TypeError: When passing an invalid type for the `contract_type` arguments
Expand All @@ -598,9 +609,7 @@ def instance_at(
in case it is not already known.

Returns:
:class:`~ape.api.address.BaseAddress`: Will be a
:class:`~ape.contracts.ContractInstance` if the contract type is discovered,
which is a subclass of the ``BaseAddress`` class.
:class:`~ape.contracts.base.ContractInstance`
"""

try:
Expand All @@ -611,15 +620,15 @@ def instance_at(
address = self.provider.network.ecosystem.decode_address(address)
contract_type = self.get(address, default=contract_type)

if contract_type:
if not isinstance(contract_type, ContractType):
raise TypeError(
f"Expected type '{ContractType.__name__}' for argument 'contract_type'."
)
if not contract_type:
raise ChainError(f"Failed to get contract type for address '{address}'.")

return self.create_contract(address, contract_type)
elif not isinstance(contract_type, ContractType):
raise TypeError(
f"Expected type '{ContractType.__name__}' for argument 'contract_type'."
)

return Address(address)
return ContractInstance(address, contract_type)

def get_deployments(self, contract_container: ContractContainer) -> List[ContractInstance]:
"""
Expand Down Expand Up @@ -662,10 +671,6 @@ def get_deployments(self, contract_container: ContractContainer) -> List[Contrac
instance = self.instance_at(
contract_addresses[deployment_index], contract_container.contract_type
)

if not isinstance(instance, ContractInstance):
continue

deployments.append(instance)

return deployments
Expand Down
4 changes: 1 addition & 3 deletions src/ape/managers/project/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,7 @@ def _load_dependencies(self) -> Dict[str, Dict[str, DependencyAPI]]:

def _get_contract(self, name: str) -> Optional[ContractContainer]:
if name in self.contracts:
return self.create_contract_container(
contract_type=self.contracts[name],
)
return self.chain_manager.contracts.get_container(self.contracts[name])

return None

Expand Down
34 changes: 0 additions & 34 deletions src/ape/utils/basemodel.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
from abc import ABC
from typing import TYPE_CHECKING, ClassVar, Dict, List, cast

from ethpm_types import ContractType
from pydantic import BaseModel

from ape.exceptions import ProviderNotConnectedError
from ape.types import AddressType
from ape.utils.misc import cached_property, singledispatchmethod

if TYPE_CHECKING:
from ape.api.providers import ProviderAPI
from ape.contracts.base import ContractContainer, ContractInstance
from ape.managers.accounts import AccountManager
from ape.managers.chain import ChainManager
from ape.managers.compilers import CompilerManager
Expand Down Expand Up @@ -75,37 +72,6 @@ def provider(self) -> "ProviderAPI":
raise ProviderNotConnectedError()
return self.network_manager.active_provider

def create_contract_container(self, contract_type: ContractType) -> "ContractContainer":
"""
Helper method for creating a ``ContractContainer``.

Args:
contract_type (``ContractType``): Type of contract for the container

Returns:
:class:`~ape.contracts.ContractContainer`
"""
from ape.contracts.base import ContractContainer

return ContractContainer(contract_type=contract_type)

def create_contract(
self, address: "AddressType", contract_type: "ContractType"
) -> "ContractInstance":
"""
Helper method for creating a ``ContractInstance``.

Args:
address (``AddressType``): Address of contract
contract_type (``ContractType``): Type of contract

Returns:
:class:`~ape.contracts.ContractInstance`
"""
from ape.contracts.base import ContractInstance

return ContractInstance(address=address, contract_type=contract_type)


class BaseInterface(ManagerAccessMixin, ABC):
"""
Expand Down
2 changes: 1 addition & 1 deletion src/ape/utils/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def _dim_default_gas(call_sig: str) -> str:
method = None
contract_name = contract_type.name
if "symbol" in contract_type.view_methods:
contract = self._receipt.create_contract(address, contract_type)
contract = self._receipt.chain_manager.contracts.instance_at(address, contract_type)

try:
contract_name = contract.symbol() or contract_name
Expand Down
8 changes: 6 additions & 2 deletions src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ape.api import BlockAPI, EcosystemAPI, PluginConfig, ReceiptAPI, TransactionAPI
from ape.api.networks import LOCAL_NETWORK_NAME, ProxyInfoAPI
from ape.contracts.base import ContractCall
from ape.exceptions import DecodingError, TransactionError
from ape.exceptions import APINotImplementedError, DecodingError, TransactionError
from ape.types import AddressType, ContractLog, RawAddress, TransactionSignature
from ape.utils import (
LogInputABICollection,
Expand Down Expand Up @@ -145,7 +145,11 @@ def get_proxy_info(self, address: AddressType) -> Optional[ProxyInfo]:
ProxyType.UUPS: str_to_slot("PROXIABLE"),
}
for type, slot in slots.items():
storage = self.provider.get_storage_at(address, slot)
try:
storage = self.provider.get_storage_at(address, slot)
except APINotImplementedError:
continue

if sum(storage) == 0:
continue

Expand Down
5 changes: 2 additions & 3 deletions tests/functional/test_contract_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,7 @@ def test_poll_logs_timeout(vyper_contract_instance, eth_tester_provider, owner,
assert "Timed out waiting for new block (time_waited=1" in str(err.value)


def test_contract_two_events_with_same_name(owner, networks_connected_to_tester):
provider = networks_connected_to_tester
def test_contract_two_events_with_same_name(owner, chain, networks_connected_to_tester):
base_path = Path(__file__).parent / "data" / "contracts" / "ethereum" / "local"
interface_path = base_path / "Interface.json"
impl_path = base_path / "InterfaceImplementation.json"
Expand All @@ -238,7 +237,7 @@ def test_contract_two_events_with_same_name(owner, networks_connected_to_tester)
assert len([e for e in impl_contract_type.events if e.name == event_name]) == 2
assert len([e for e in interface_contract_type.events if e.name == event_name]) == 1

impl_container = provider.create_contract_container(impl_contract_type)
impl_container = chain.contracts.get_container(impl_contract_type)
impl_instance = owner.deploy(impl_container)

with pytest.raises(AttributeError) as err:
Expand Down
8 changes: 4 additions & 4 deletions tests/functional/test_contract_instance.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import re

import pytest
from eth_utils import is_checksum_address
from hexbytes import HexBytes

from ape import Contract
from ape.api import Address
from ape_ethereum.transactions import TransactionStatusEnum
from ape.exceptions import ChainError

from .conftest import SOLIDITY_CONTRACT_ADDRESS

MATCH_TEST_CONTRACT = re.compile(r"<TestContract((Sol)|(Vy))")


def test_init_at_unknown_address():
contract = Contract(SOLIDITY_CONTRACT_ADDRESS)
assert type(contract) == Address
assert contract.address == SOLIDITY_CONTRACT_ADDRESS
with pytest.raises(ChainError):
Contract(SOLIDITY_CONTRACT_ADDRESS)


def test_init_specify_contract_type(
Expand Down