Skip to content
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

fix: properly detect, specify, and handle connecting forked network to non-fork [APE-1393] #156

Merged
merged 10 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
rev: v4.4.0
hooks:
- id: check-yaml

Expand All @@ -10,24 +10,24 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.9.1
hooks:
- id: black
name: black

- repo: https://github.com/pycqa/flake8
rev: 6.0.0
rev: 6.1.0
hooks:
- id: flake8

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
rev: v1.5.1
hooks:
- id: mypy
additional_dependencies: [types-PyYAML, types-requests, types-setuptools, pydantic]

- repo: https://github.com/executablebooks/mdformat
rev: 0.7.16
rev: 0.7.17
hooks:
- id: mdformat
additional_dependencies: [mdformat-gfm, mdformat-frontmatter]
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Quick Start

Hardhat network provider plugin for Ape. Hardhat is a development framework written in Node.js for Ethereum that includes a local network implementation.
This is a Hardhat network provider plugin for Ape.
Hardhat is a development framework written in Node.js for Ethereum that includes a local network implementation.
Use this plugin to manage a Hardhat node process or connect to an existing one.

## Dependencies

Expand Down Expand Up @@ -42,16 +44,18 @@ This network provider takes additional Hardhat-specific configuration options. T

```yaml
hardhat:
port: 8555
host: 127.0.0.1:8555
```

To select a random port, use a value of "auto":

```yaml
hardhat:
port: auto
host: auto
```

**NOTE**: If you plan on running multiple Hardhat nodes of any kind, you likely will want to use `auto`.

This is useful for multiprocessing and starting up multiple providers.

You can also adjust the request timeout setting:
Expand Down
104 changes: 70 additions & 34 deletions ape_hardhat/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from eth_utils import is_0x_prefixed, is_hex, to_hex
from evm_trace import CallType
from evm_trace import TraceFrame as EvmTraceFrame
from evm_trace import get_calltree_from_geth_trace
from evm_trace import create_trace_frames, get_calltree_from_geth_trace
from hexbytes import HexBytes
from pydantic import BaseModel, Field
from semantic_version import Version # type: ignore
Expand Down Expand Up @@ -207,6 +207,54 @@ class Config:
extra = "allow"


class ForkedNetworkMetadata(BaseModel):
"""
Metadata from the RPC ``hardhat_metadata``.
"""

chain_id: int = Field(alias="chainId")
"""
The chain ID of the network being forked.
"""

fork_block_number: int = Field(alias="forkBlockNumber")
"""
The number of the block that the network forked from.
"""

fork_block_hash: str = Field(alias="forkBlockHash")
"""
The hash of the block that the network forked from.
"""


class NetworkMetadata(BaseModel):
"""
Metadata from the RPC ``hardhat_metadata``.
"""

client_version: str = Field(alias="clientVersion")
"""
A string identifying the version of Hardhat, for debugging purposes,
not meant to be displayed to users.
"""

instance_id: str = Field(alias="instanceId")
"""
A 0x-prefixed hex-encoded 32 bytes id which uniquely identifies an
instance/run of Hardhat Network. Running Hardhat Network more than
once (even with the same version and parameters) will always result
in different instanceIds. Running hardhat_reset will change the
instanceId of an existing Hardhat Network.
"""

forked_network: Optional[ForkedNetworkMetadata] = Field(None, alias="forkedNetwork")
"""
An object with information about the forked network. This field is
only present when Hardhat Network is forking another chain.
"""


def _call(*args):
return call([*args], stderr=PIPE, stdout=PIPE, stdin=PIPE)

Expand Down Expand Up @@ -368,6 +416,17 @@ def hardhat_config_file(self) -> Path:

return path.expanduser().absolute()

@property
def metadata(self) -> NetworkMetadata:
"""
Get network metadata, including an object about forked-metadata.
If the network is not a fork, it will be ``None``.
This is a helpful way of determing if a Hardhat node is a fork or not
when connecting to a remote Hardhat network.
"""
metadata = self._make_request("hardhat_metadata", [])
return NetworkMetadata.parse_obj(metadata)

@cached_property
def _test_config(self) -> TestConfig:
return cast(TestConfig, self.config_manager.get_config("test"))
Expand Down Expand Up @@ -396,15 +455,6 @@ def package_is_plugin(package: str) -> bool:

return plugins

@property
def gas_price(self) -> int:
# TODO: Remove this once Ape > 0.6.13
result = super().gas_price
if isinstance(result, str) and is_0x_prefixed(result):
return int(result, 16)

return result

def _has_hardhat_plugin(self, plugin_name: str) -> bool:
return next((True for plugin in self._hardhat_plugins if plugin == plugin_name), False)

Expand Down Expand Up @@ -749,8 +799,7 @@ def get_transaction_trace(self, txn_hash: str) -> Iterator[TraceFrame]:
def _get_transaction_trace(self, txn_hash: str) -> Iterator[EvmTraceFrame]:
result = self._make_request("debug_traceTransaction", [txn_hash])
frames = result.get("structLogs", [])
for frame in frames:
yield EvmTraceFrame(**frame)
yield from create_trace_frames(frames)

def get_call_tree(self, txn_hash: str) -> CallTreeNode:
receipt = self.chain_manager.get_receipt(txn_hash)
Expand Down Expand Up @@ -918,28 +967,15 @@ def _upstream_provider(self) -> ProviderAPI:
def connect(self):
super().connect()

# Verify that we're connected to a Hardhat node with mainnet-fork mode.
self._upstream_provider.connect()

try:
upstream_genesis_block_hash = self._upstream_provider.get_block(0).hash
except ExtraDataLengthError as err:
if isinstance(self._upstream_provider, Web3Provider):
logger.error(
f"Upstream provider '{self._upstream_provider.name}' "
f"missing Geth PoA middleware."
)
self._upstream_provider.web3.middleware_onion.inject(geth_poa_middleware, layer=0)
upstream_genesis_block_hash = self._upstream_provider.get_block(0).hash
else:
raise HardhatProviderError(f"Unable to get genesis block: {err}.") from err

self._upstream_provider.disconnect()

if self.get_block(0).hash != upstream_genesis_block_hash:
logger.warning(
"Upstream network has mismatching genesis block. "
"This could be an issue with hardhat."
if not self.metadata.forked_network:
# This will fail when trying to connect hardhat-fork to
# a non-forked network.
raise HardhatProviderError(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we had an issue with the genesis block on certain networks, which is why we had to switch to logging as an error instead, with hopes to someday having a better way. Now, hardhat has a metadata RPC that we can use to deduce whether the network is a fork, so we can put back this error.

See this comment for historical reference: #32 (comment)

"Network is no a fork. "
"Hardhat is likely already running on the local network. "
"Try using config:\n\n(ape-config.yaml)\n```\nhardhat:\n "
"host: auto\n```\n\nso that multiple processes can automatically "
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this message should prevent the user from being misled so much, and even has a suggestion for how to fix.
this is the part that fixes ApeWorX/ape#1467

"use different ports."
)

def build_command(self) -> List[str]:
Expand Down
8 changes: 4 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
"rich", # Needed for trace tests
],
"lint": [
"black>=23.3.0,<24", # auto-formatter and linter
"mypy>=0.991,<1", # Static type analyzer
"black>=23.9.1,<24", # auto-formatter and linter
"mypy>=1.5.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
"flake8>=6.0.0,<7", # Style linter
"flake8>=6.1.0,<7", # Style linter
"isort>=5.10.1,<6", # Import sorting linter
"mdformat>=0.7.16", # Auto-formatter for markdown
"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
],
Expand Down
1 change: 0 additions & 1 deletion tests/data/contracts/ethereum/local/reverts_contract.json

This file was deleted.

This file was deleted.

59 changes: 0 additions & 59 deletions tests/data/sources/RevertsContractVy.vy

This file was deleted.

4 changes: 0 additions & 4 deletions tests/data/sources/SubRevertsVy.vy

This file was deleted.

23 changes: 1 addition & 22 deletions tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from ape.api.accounts import ImpersonatedAccount
from ape.contracts import ContractContainer
from ape.exceptions import ContractLogicError
from ape.pytest.contextmanagers import RevertsContextManager as reverts
from ape.types import CallTreeNode, TraceFrame
from evm_trace import CallType
from hexbytes import HexBytes
Expand Down Expand Up @@ -176,26 +175,6 @@ def test_contract_revert_custom_exception(owner, get_contract_type, accounts):
assert err.value.inputs == {"addr": accounts[7].address, "counter": 123}


def test_contract_dev_message(owner, get_contract_type):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately, we can longer run this test without installing ape-vyper, maybe we will find a way to have a less integration-y test

sub_ct = get_contract_type("sub_reverts_contract")
sub = owner.deploy(ContractContainer(sub_ct))
container = ContractContainer(get_contract_type("reverts_contract"))
contract = owner.deploy(container, sub)

with reverts(dev_message="dev: one"):
contract.revertStrings(1, sender=owner)
with reverts(dev_message="dev: error"):
contract.revertStrings(2, sender=owner)
with reverts(dev_message="dev: such modifiable, wow"):
contract.revertStrings(4, sender=owner)
with reverts(dev_message="dev: foobarbaz"):
contract.revertStrings(13, sender=owner)
with reverts(dev_message="dev: great job"):
contract.revertStrings(31337, sender=owner)
with reverts(dev_message="dev: sub-zero"):
contract.subRevertStrings(0, sender=owner)


def test_transaction_contract_as_sender(contract_instance, connected_provider):
# Set balance so test wouldn't normally fail from lack of funds
connected_provider.set_balance(contract_instance.address, "1000 ETH")
Expand All @@ -216,7 +195,7 @@ def test_set_balance(connected_provider, owner, convert, amount):
def test_set_code(connected_provider, contract_instance):
provider = connected_provider
code = provider.get_code(contract_instance.address)
assert type(code) == HexBytes
assert type(code) is HexBytes
assert provider.set_code(contract_instance.address, "0x00") is True
assert provider.get_code(contract_instance.address) != code
assert provider.set_code(contract_instance.address, code) is True
Expand Down