Skip to content

Commit

Permalink
feat!: Handle 0.7 breaking changes and updates [APE-1562] (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
NotPeopling2day authored Dec 19, 2023
1 parent c2ee444 commit 9656422
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 75 deletions.
1 change: 0 additions & 1 deletion .mdformat.toml

This file was deleted.

2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 23.11.0
rev: 23.12.0
hooks:
- id: black
name: black
Expand Down
125 changes: 64 additions & 61 deletions ape_hardhat/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@
from subprocess import PIPE, CalledProcessError, call, check_output
from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Union, cast

from ape._pydantic_compat import root_validator
from ape.api import (
ForkedNetworkAPI,
PluginConfig,
ReceiptAPI,
SubprocessProvider,
TestProviderAPI,
TransactionAPI,
Web3Provider,
)
from ape.exceptions import (
ContractLogicError,
Expand All @@ -28,23 +26,26 @@
from ape.logging import logger
from ape.types import (
AddressType,
BlockID,
CallTreeNode,
ContractCode,
SnapshotID,
SourceTraceback,
TraceFrame,
)
from ape.utils import cached_property
from ape_test import Config as TestConfig
from ape.utils import DEFAULT_TEST_HD_PATH, cached_property
from ape_ethereum.provider import Web3Provider
from ape_test import ApeTestConfig
from chompjs import parse_js_object # type: ignore
from eth_pydantic_types import HexBytes
from eth_typing import HexStr
from eth_utils import is_0x_prefixed, is_hex, to_hex
from ethpm_types import HexBytes
from evm_trace import CallType
from evm_trace import TraceFrame as EvmTraceFrame
from evm_trace import create_trace_frames, get_calltree_from_geth_trace
from pydantic import BaseModel, Field
from semantic_version import Version # type: ignore
from packaging.version import Version
from pydantic import BaseModel, Field, model_validator
from pydantic_settings import SettingsConfigDict
from web3 import HTTPProvider, Web3
from web3.exceptions import ExtraDataLengthError
from web3.gas_strategies.rpc import rpc_gas_price_strategy
Expand Down Expand Up @@ -76,7 +77,6 @@
}},
}};
""".lstrip()
HARDHAT_HD_PATH = "m/44'/60'/0'"
DEFAULT_HARDHAT_CONFIG_FILE_NAME = "hardhat.config.js"
HARDHAT_CONFIG_FILE_NAME_OPTIONS = (DEFAULT_HARDHAT_CONFIG_FILE_NAME, "hardhat.config.ts")
HARDHAT_PLUGIN_PATTERN = re.compile(r"hardhat-[A-Za-z0-9-]+$")
Expand All @@ -92,7 +92,8 @@ def _validate_hardhat_config_file(
num_of_accounts: int,
hardhat_version: str,
hard_fork: Optional[str] = None,
):
hd_path: Optional[str] = None,
) -> Path:
if not path.is_file() and path.is_dir():
path = path / DEFAULT_HARDHAT_CONFIG_FILE_NAME

Expand All @@ -111,8 +112,9 @@ def _validate_hardhat_config_file(
else:
hard_fork = "shanghai"

hd_path = hd_path or DEFAULT_TEST_HD_PATH
content = HARDHAT_CONFIG.format(
hd_path=HARDHAT_HD_PATH,
hd_path=hd_path,
mnemonic=mnemonic,
number_of_accounts=num_of_accounts,
hard_fork=hard_fork,
Expand Down Expand Up @@ -146,7 +148,7 @@ def _validate_hardhat_config_file(
if not accounts_config or (
accounts_config.get("mnemonic") != mnemonic
or accounts_config.get("count") != num_of_accounts
or accounts_config.get("path") != HARDHAT_HD_PATH
or accounts_config.get("path") != hd_path
):
logger.warning(invalid_config_warning)

Expand All @@ -155,7 +157,7 @@ def _validate_hardhat_config_file(
content = path.read_text()
if (
mnemonic not in content
or HARDHAT_HD_PATH not in content
or hd_path not in content
or str(num_of_accounts) not in content
):
logger.warning(invalid_config_warning)
Expand All @@ -166,12 +168,14 @@ def _validate_hardhat_config_file(
f"Some features may not work as intended."
)

return path


class PackageJson(BaseModel):
name: Optional[str]
version: Optional[str]
description: Optional[str]
dependencies: Optional[Dict[str, str]]
name: Optional[str] = None
version: Optional[str] = None
description: Optional[str] = None
dependencies: Optional[Dict[str, str]] = None
dev_dependencies: Optional[Dict[str, str]] = Field(None, alias="devDependencies")


Expand Down Expand Up @@ -201,8 +205,8 @@ class HardhatForkConfig(PluginConfig):


class HardhatNetworkConfig(PluginConfig):
port: Optional[Union[int, Literal["auto"]]] = DEFAULT_PORT
"""Depreciated. Use ``host`` config."""
evm_version: Optional[str] = None
"""The EVM hardfork to use. Defaults to letting Hardhat decide."""

host: Optional[Union[str, Literal["auto"]]] = None
"""The host address or ``"auto"`` to use localhost with a random port (with attempts)."""
Expand Down Expand Up @@ -237,8 +241,7 @@ class HardhatNetworkConfig(PluginConfig):
# Mapping of ecosystem_name => network_name => HardhatForkConfig
fork: Dict[str, Dict[str, HardhatForkConfig]] = {}

class Config:
extra = "allow"
model_config = SettingsConfigDict(extra="allow")


class ForkedNetworkMetadata(BaseModel):
Expand Down Expand Up @@ -453,14 +456,18 @@ def bin_path(self) -> Path:
# Default to the expected path suffx (relative).
return suffix

@property
def _ape_managed_hardhat_config_file(self):
return self.config_manager.DATA_FOLDER / "hardhat" / DEFAULT_HARDHAT_CONFIG_FILE_NAME

@property
def hardhat_config_file(self) -> Path:
if self.settings.hardhat_config_file and self.settings.hardhat_config_file.is_dir():
path = self.settings.hardhat_config_file / DEFAULT_HARDHAT_CONFIG_FILE_NAME
elif self.settings.hardhat_config_file:
path = self.settings.hardhat_config_file
else:
path = self.config_manager.DATA_FOLDER / "hardhat" / DEFAULT_HARDHAT_CONFIG_FILE_NAME
path = self._ape_managed_hardhat_config_file

return path.expanduser().absolute()

Expand All @@ -473,11 +480,11 @@ def metadata(self) -> NetworkMetadata:
when connecting to a remote Hardhat network.
"""
metadata = self._make_request("hardhat_metadata", [])
return NetworkMetadata.parse_obj(metadata)
return NetworkMetadata.model_validate(metadata)

@cached_property
def _test_config(self) -> TestConfig:
return cast(TestConfig, self.config_manager.get_config("test"))
def _test_config(self) -> ApeTestConfig:
return cast(ApeTestConfig, self.config_manager.get_config("test"))

@cached_property
def _package_json(self) -> PackageJson:
Expand All @@ -486,7 +493,7 @@ def _package_json(self) -> PackageJson:
if not json_path.is_file():
return PackageJson()

return PackageJson.parse_file(json_path)
return PackageJson.model_validate_json(json_path.read_text())

@cached_property
def _hardhat_plugins(self) -> List[str]:
Expand All @@ -511,38 +518,8 @@ def connect(self):
Start the hardhat process and verify it's up and accepting connections.
"""

_validate_hardhat_config_file(
self.hardhat_config_file,
self.mnemonic,
self.number_of_accounts,
self.hardhat_version,
)

# NOTE: Must set port before calling 'super().connect()'.
warning = "`port` setting is depreciated. Please use `host` key that includes the port."

if "port" in self.provider_settings:
# TODO: Can remove after 0.7.
logger.warning(warning)
self._host = f"http://127.0.0.1:{self.provider_settings['port']}"

elif self.settings.port != DEFAULT_PORT and self.config_host is not None:
raise HardhatProviderError(
"Cannot use depreciated `port` field with `host`."
"Place `port` at end of `host` instead."
)

elif self.settings.port != DEFAULT_PORT:
# We only get here if the user configured a port without a host,
# the old way of doing it. TODO: Can remove after 0.7.
logger.warning(warning)
if self.settings.port not in (None, "auto"):
self._host = f"http://127.0.0.1:{self.settings.port}"
else:
# This will trigger selecting a random port on localhost and trying.
self._host = "auto"

elif "host" in self.provider_settings:
if "host" in self.provider_settings:
self._host = self.provider_settings["host"]

elif self._host is None:
Expand Down Expand Up @@ -682,6 +659,20 @@ def build_command(self) -> List[str]:
return self._get_command()

def _get_command(self) -> List[str]:
if self.hardhat_config_file == self._ape_managed_hardhat_config_file:
# If we are using the Ape managed file. regenerated before launch.
self._ape_managed_hardhat_config_file.unlink(missing_ok=True)

# Validate (and create if needed) the user-given path.
hh_config_path = _validate_hardhat_config_file(
self.hardhat_config_file,
self.mnemonic,
self.number_of_accounts,
self.hardhat_version,
hard_fork=self.config.evm_version,
hd_path=self.test_config.hd_path or DEFAULT_TEST_HD_PATH,
)

return [
self.node_bin,
str(self.bin_path),
Expand All @@ -691,7 +682,7 @@ def _get_command(self) -> List[str]:
"--port",
f"{self._port or DEFAULT_PORT}",
"--config",
str(self.hardhat_config_file),
str(hh_config_path),
]

def set_block_gas_limit(self, gas_limit: int) -> bool:
Expand Down Expand Up @@ -726,8 +717,19 @@ def revert(self, snapshot_id: SnapshotID) -> bool:
def unlock_account(self, address: AddressType) -> bool:
return self._make_request("hardhat_impersonateAccount", [address])

def send_call(self, txn: TransactionAPI, **kwargs: Any) -> bytes:
def send_call(
self,
txn: TransactionAPI,
block_id: Optional[BlockID] = None,
state: Optional[Dict] = None,
**kwargs,
) -> HexBytes:
skip_trace = kwargs.pop("skip_trace", False)
if block_id is not None:
kwargs["block_identifier"] = block_id
if state is not None:
kwargs["state_override"] = state

arguments = self._prepare_call(txn, **kwargs)
if skip_trace:
return self._send_call_legacy(txn, **kwargs)
Expand Down Expand Up @@ -812,7 +814,7 @@ def _trace_call(self, arguments: List[Any]) -> Tuple[Dict, Iterator[EvmTraceFram
trace_data = result.get("structLogs", [])
return result, create_trace_frames(trace_data)

def _send_call_legacy(self, txn, **kwargs) -> bytes:
def _send_call_legacy(self, txn, **kwargs) -> HexBytes:
result = super().send_call(txn, **kwargs)

# Older versions of Hardhat do not support call tracing.
Expand Down Expand Up @@ -864,7 +866,7 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI:
if sender_address in self.unlocked_accounts:
# Allow for an unsigned transaction
txn = self.prepare_transaction(txn)
txn_dict = txn.dict()
txn_dict = txn.model_dump(by_alias=True, mode="json")
if isinstance(txn_dict.get("type"), int):
txn_dict["type"] = HexBytes(txn_dict["type"]).hex()

Expand Down Expand Up @@ -1052,7 +1054,8 @@ class HardhatForkProvider(HardhatProvider):
to use as your archive node.
"""

@root_validator()
@model_validator(mode="before")
@classmethod
def set_upstream_provider(cls, value):
network = value["network"]
adhoc_settings = value.get("provider_settings", {}).get("fork", {})
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ force_grid_wrap = 0
include_trailing_comma = true
multi_line_output = 3
use_parentheses = true

[tool.mdformat]
number = true
24 changes: 16 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,27 @@
"rich", # Needed for trace tests
],
"lint": [
"black>=23.11.0,<24", # auto-formatter and linter
"black>=23.12.0,<24", # Auto-formatter and linter
"mypy>=1.7.1,<2", # Static type analyzer
"types-PyYAML", # Needed due to mypy typeshed
"types-setuptools", # Needed for mypy typeshed
"types-requests", # Needed due to mypy typeshed
"types-setuptools", # Needed for mypy type shed
"types-requests", # Needed for mypy type shed
"types-PyYAML", # Needed for mypy type shed
"flake8>=6.1.0,<7", # Style linter
"flake8-breakpoint>=1.1.0,<2", # Detect breakpoints left in code
"flake8-print>=5.0.0,<6", # Detect print statements left in code
"isort>=5.10.1,<6", # Import sorting linter
"mdformat>=0.7.17", # Auto-formatter for markdown
"mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown
"mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates
"pydantic<2", # Needed for successful type-check
"mdformat-pyproject>=0.0.1", # Allows configuring in pyproject.toml
],
"doc": [
"myst-parser>=1.0.0,<2", # Parse markdown docs
"sphinx-click>=4.4.0,<5", # For documenting CLI
"Sphinx>=6.1.3,<7", # Documentation generator
"sphinx_rtd_theme>=1.2.0,<2", # Readthedocs.org theme
"towncrier>=19.2.0, <20", # Generate release notes
"sphinxcontrib-napoleon>=0.7", # Allow Google-style documentation
"sphinx-plausible>=0.1.2,<0.2",
],
"release": [ # `release` GitHub Action job uses this
"setuptools", # Installation tool
Expand Down Expand Up @@ -72,11 +77,14 @@
url="https://github.com/ApeWorX/ape-hardhat",
include_package_data=True,
install_requires=[
"eth-ape>=0.6.12,<0.7",
"eth-ape>=0.7.0,<0.8",
"ethpm-types", # Use same version as eth-ape
"evm-trace", # Use same version as eth-ape
"hexbytes", # Use same version as eth-ape
"web3", # Use same version as eth-ape
"chompjs>=1.1.9,<2", # To help parse hardhat files
"requests", # Use same version as eth-ape
"hexbytes", # Use same version as eth-ape
"packaging", # Use same version as eth-ape
"yarl>=1.9.2,<2",
],
python_requires=">=3.8,<4",
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ def contract_type(request, get_contract_type) -> ContractType:
@pytest.fixture
def get_contract_type():
def fn(name: str):
return ContractType.parse_file(LOCAL_CONTRACTS_PATH / f"{name}.json")
path = LOCAL_CONTRACTS_PATH / f"{name}.json"
return ContractType.model_validate_json(path.read_text())

return fn

Expand Down
6 changes: 3 additions & 3 deletions tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def test_get_call_tree(connected_provider, sender, receiver):
call_tree = connected_provider.get_call_tree(transfer.txn_hash)
assert isinstance(call_tree, CallTreeNode)
assert call_tree.call_type == CallType.CALL.value
assert repr(call_tree) == "0xc89D42189f0450C2b2c3c61f58Ec5d628176A1E7.0x()"
assert repr(call_tree) == "0x70997970C51812dc3A010C7d01b50e0d17dc79C8.0x()"


def test_request_timeout(connected_provider, config):
Expand Down Expand Up @@ -313,7 +313,7 @@ def test_remote_host(temp_config, networks, no_hardhat_bin, project):


def test_hardfork(temp_config, networks):
data = {"hardhat": {"hardfork": "london"}}
data = {"hardhat": {"evm_version": "london"}}
with temp_config(data):
with networks.ethereum.local.use_provider("hardhat") as provider:
assert provider.config.hardfork == "london"
assert provider.config.evm_version == "london"

0 comments on commit 9656422

Please sign in to comment.