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

Hardhat compatibility fixes #1044

Closed
wants to merge 11 commits into from
13 changes: 13 additions & 0 deletions brownie/data/network-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ development:
host: http://127.0.0.1
cmd_settings:
port: 8545
- name: Hardhat
id: hardhat
cmd: npx hardhat node
host: http://127.0.0.1
cmd_settings:
port: 8545
- name: Hardhat (Mainnet Fork)
id: hardhat-fork
cmd: npx hardhat node
host: http://127.0.0.1
cmd_settings:
port: 8545
fork: mainnet
- name: Ganache-CLI (Mainnet Fork)
id: mainnet-fork
cmd: ganache-cli
Expand Down
2 changes: 1 addition & 1 deletion brownie/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def __init__(self, exc: ValueError) -> None:
self.source: str = ""
self.revert_type: str = data["error"]
self.pc: Optional[str] = data.get("program_counter")
if self.revert_type == "revert":
if self.pc and self.revert_type == "revert":
self.pc -= 1

self.revert_msg: Optional[str] = data.get("reason")
Expand Down
34 changes: 34 additions & 0 deletions brownie/network/middlewares/hardhat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Callable, Dict, List, Optional

from web3 import Web3

from brownie.network.middlewares import BrownieMiddlewareABC


class HardhatMiddleWare(BrownieMiddlewareABC):
@classmethod
def get_layer(cls, w3: Web3, network_type: str) -> Optional[int]:
if w3.clientVersion.lower().startswith("hardhat"):
return -100
else:
return None

def process_request(self, make_request: Callable, method: str, params: List) -> Dict:
result = make_request(method, params)

# modify Hardhat transaction error to mimick the format that Ganache uses
if method in ("eth_call", "eth_sendTransaction") and "error" in result:
message = result["error"]["message"]
if message.startswith("VM Exception") or message.startswith("Transaction reverted"):
# FIXME: this doesn't play well with parallel tests
txid = self.w3.eth.getBlock("latest")["transactions"][0]
data: Dict = {}
result["error"]["data"] = {txid.hex(): data}
message = message.split(": ", maxsplit=1)[-1]
if message == "Transaction reverted without a reason":
data.update({"error": "revert", "reason": None})
elif message.startswith("revert"):
data.update({"error": "revert", "reason": message[7:]})
else:
data["error"] = message
return result
8 changes: 3 additions & 5 deletions brownie/network/rpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,17 @@
from brownie.network.state import Chain
from brownie.network.web3 import web3

from . import ganache, geth
from . import ganache, geth, hardhat

chain = Chain()

ATTACH_BACKENDS = {
"ethereumjs testrpc": ganache,
"geth": geth,
}
ATTACH_BACKENDS = {"ethereumjs testrpc": ganache, "geth": geth, "hardhat": hardhat}

LAUNCH_BACKENDS = {
"ganache": ganache,
"ethnode": geth,
"geth": geth,
"npx hardhat": hardhat,
}


Expand Down
90 changes: 90 additions & 0 deletions brownie/network/rpc/hardhat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/python3

import sys
import warnings
from pathlib import Path
from subprocess import DEVNULL, PIPE
from typing import Dict, List, Optional

import psutil
from requests.exceptions import ConnectionError as RequestsConnectionError

from brownie.exceptions import InvalidArgumentWarning, RPCRequestError
from brownie.network.web3 import web3

CLI_FLAGS = {"port": "--port", "fork": "--fork", "fork_block": "--fork-block-number"}

DEFAULT_HARDHAT_CONFIG = """\
module.exports = {
networks: {
hardhat: {
blockGasLimit: 15000000
}
}
}
"""


def launch(cmd: str, **kwargs: Dict) -> None:
"""Launches the RPC client.

Args:
cmd: command string to execute as subprocess"""
# if sys.platform == "win32" and not cmd.split(" ")[0].endswith(".cmd"):
# if " " in cmd:
# cmd = cmd.replace(" ", ".cmd ", 1)
# else:
# cmd += ".cmd"
cmd_list = cmd.split(" ")
for key, value in [(k, v) for k, v in kwargs.items() if v]:
try:
cmd_list.extend([CLI_FLAGS[key], str(value)])
except KeyError:
warnings.warn(
f"Ignoring invalid commandline setting for hardhat: "
f'"{key}" with value "{value}".',
InvalidArgumentWarning,
)
print(f"\nLaunching '{' '.join(cmd_list)}'...")
out = DEVNULL if sys.platform == "win32" else PIPE

hardhat_config = Path("hardhat.config.js")
if not hardhat_config.exists():
hardhat_config.write_text(DEFAULT_HARDHAT_CONFIG)

return psutil.Popen(cmd_list, stdin=DEVNULL, stdout=out, stderr=out)


def on_connection() -> None:
pass


def _request(method: str, args: List) -> int:
try:
response = web3.provider.make_request(method, args) # type: ignore
if "result" in response:
return response["result"]
except (AttributeError, RequestsConnectionError):
raise RPCRequestError("Web3 is not connected.")
raise RPCRequestError(response["error"]["message"])


def sleep(seconds: int) -> int:
return _request("evm_increaseTime", [seconds])


def mine(timestamp: Optional[int] = None) -> None:
params = [timestamp] if timestamp else []
_request("evm_mine", params)


def snapshot() -> int:
return _request("evm_snapshot", [])


def revert(snapshot_id: int) -> None:
_request("evm_revert", [snapshot_id])


def unlock_account(address: str) -> None:
web3.provider.make_request("hardhat_impersonateAccount", [address]) # type: ignore
2 changes: 1 addition & 1 deletion brownie/network/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ def sleep(self, seconds: int) -> None:
"""
if not isinstance(seconds, int):
raise TypeError("seconds must be an integer value")
self._time_offset = rpc.Rpc().sleep(seconds)
self._time_offset = int(rpc.Rpc().sleep(seconds))

if seconds:
self._redo_buffer.clear()
Expand Down
18 changes: 7 additions & 11 deletions tests/cli/test_cli_networks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import copy

import pytest
import yaml

Expand All @@ -6,10 +8,10 @@


@pytest.fixture(autouse=True)
def isolation():
def networks_yaml():
with _get_data_folder().joinpath("network-config.yaml").open() as fp:
networks = yaml.safe_load(fp)
yield
yield copy.deepcopy(networks)
with _get_data_folder().joinpath("network-config.yaml").open("w") as fp:
networks = yaml.dump(networks, fp)

Expand Down Expand Up @@ -151,15 +153,9 @@ def test_delete_live():
assert "mainnet" not in [i["id"] for i in networks["live"][0]["networks"]]


def test_delete_development():
for network_name in (
"development",
"mainnet-fork",
"bsc-main-fork",
"ftm-main-fork",
"geth-dev",
):
cli_networks._delete(network_name)
def test_delete_development(networks_yaml):
for network_name in networks_yaml["development"]:
cli_networks._delete(network_name["id"])

with _get_data_folder().joinpath("network-config.yaml").open() as fp:
networks = yaml.safe_load(fp)
Expand Down