From d9785a7016525475f0c5e14404589cdc026eafe4 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 02:44:25 +0400 Subject: [PATCH 01/17] feat: hardhat rpc backend --- brownie/network/rpc/__init__.py | 8 ++-- brownie/network/rpc/hardhat.py | 75 +++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 brownie/network/rpc/hardhat.py diff --git a/brownie/network/rpc/__init__.py b/brownie/network/rpc/__init__.py index d79789fb1..9b9bbffa7 100644 --- a/brownie/network/rpc/__init__.py +++ b/brownie/network/rpc/__init__.py @@ -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, } diff --git a/brownie/network/rpc/hardhat.py b/brownie/network/rpc/hardhat.py new file mode 100644 index 000000000..9fb0ddd98 --- /dev/null +++ b/brownie/network/rpc/hardhat.py @@ -0,0 +1,75 @@ +#!/usr/bin/python3 + +import sys +import warnings +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"} + + +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 + + 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 From 70b835705796e57f12703412a3d9e5a28160e14f Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 02:45:13 +0400 Subject: [PATCH 02/17] feat: hardhat middleware --- brownie/network/middlewares/hardhat.py | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 brownie/network/middlewares/hardhat.py diff --git a/brownie/network/middlewares/hardhat.py b/brownie/network/middlewares/hardhat.py new file mode 100644 index 000000000..617c538bd --- /dev/null +++ b/brownie/network/middlewares/hardhat.py @@ -0,0 +1,29 @@ +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 == "eth_sendTransaction" and "error" in result: + txid = self.w3.eth.getBlock("latest")["transactions"][0] + data: Dict = {} + result["error"]["data"] = {txid.hex(): data} + message = result["error"]["message"].split(": ", maxsplit=1)[1] + if message.startswith("revert"): + data.update(error="revert", reason=message[7:]) + else: + data["error"] = message + return result From 01e506fa7dac8abce2de7ed755be054a037f25ed Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 02:45:46 +0400 Subject: [PATCH 03/17] fix: do not expect `pc` in error string --- brownie/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brownie/exceptions.py b/brownie/exceptions.py index f1c3b80bc..fc8d953e6 100644 --- a/brownie/exceptions.py +++ b/brownie/exceptions.py @@ -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") From 18a3d84bd3b9ed619512fca0c02fd169891ad3f9 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 02:46:01 +0400 Subject: [PATCH 04/17] feat: add `hardhat` and `hardhat-fork` networks --- brownie/data/network-config.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/brownie/data/network-config.yaml b/brownie/data/network-config.yaml index edacea6ba..3c730ca75 100644 --- a/brownie/data/network-config.yaml +++ b/brownie/data/network-config.yaml @@ -111,6 +111,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 From 0786c0c5f64a60653aebcda2dccad7faabd29b7b Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 03:20:36 +0400 Subject: [PATCH 05/17] fix: update failing test --- tests/cli/test_cli_networks.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/cli/test_cli_networks.py b/tests/cli/test_cli_networks.py index 64b46aaa1..d8524b18e 100644 --- a/tests/cli/test_cli_networks.py +++ b/tests/cli/test_cli_networks.py @@ -1,3 +1,5 @@ +import copy + import pytest import yaml @@ -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) @@ -151,17 +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", - "polygon-main-fork", - "xdai-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) From 017083174aaf52cca8cd7a70c378328d3d570d7c Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 03:36:50 +0400 Subject: [PATCH 06/17] fix: handle non-VM exceptions --- brownie/network/middlewares/hardhat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/brownie/network/middlewares/hardhat.py b/brownie/network/middlewares/hardhat.py index 617c538bd..9eaac42b7 100644 --- a/brownie/network/middlewares/hardhat.py +++ b/brownie/network/middlewares/hardhat.py @@ -18,10 +18,13 @@ def process_request(self, make_request: Callable, method: str, params: List) -> # modify Hardhat transaction error to mimick the format that Ganache uses if method == "eth_sendTransaction" and "error" in result: + message = result["error"]["message"] + if not message.startswith("VM Exception"): + return result txid = self.w3.eth.getBlock("latest")["transactions"][0] data: Dict = {} result["error"]["data"] = {txid.hex(): data} - message = result["error"]["message"].split(": ", maxsplit=1)[1] + message = message.split(": ", maxsplit=1)[-1] if message.startswith("revert"): data.update(error="revert", reason=message[7:]) else: From 2003bea3ad0641108d9f47eed6b3a96e995caccd Mon Sep 17 00:00:00 2001 From: banteg Date: Wed, 7 Apr 2021 06:54:59 +0300 Subject: [PATCH 07/17] fix: cast rpc sleep response to int --- brownie/network/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brownie/network/state.py b/brownie/network/state.py index 1b38bfa71..c5f0727a3 100644 --- a/brownie/network/state.py +++ b/brownie/network/state.py @@ -354,7 +354,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() From 6f82d26736a8436d509d0f1ec45d831c62874598 Mon Sep 17 00:00:00 2001 From: banteg Date: Wed, 7 Apr 2021 06:56:13 +0300 Subject: [PATCH 08/17] feat: capture hardhat reverts --- brownie/network/middlewares/hardhat.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/brownie/network/middlewares/hardhat.py b/brownie/network/middlewares/hardhat.py index 9eaac42b7..fd1368310 100644 --- a/brownie/network/middlewares/hardhat.py +++ b/brownie/network/middlewares/hardhat.py @@ -19,14 +19,15 @@ def process_request(self, make_request: Callable, method: str, params: List) -> # modify Hardhat transaction error to mimick the format that Ganache uses if method == "eth_sendTransaction" and "error" in result: message = result["error"]["message"] - if not message.startswith("VM Exception"): - return result - txid = self.w3.eth.getBlock("latest")["transactions"][0] - data: Dict = {} - result["error"]["data"] = {txid.hex(): data} - message = message.split(": ", maxsplit=1)[-1] - if message.startswith("revert"): - data.update(error="revert", reason=message[7:]) - else: - data["error"] = message + if message.startswith("VM Exception") or message.startswith("Transaction reverted"): + 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 From 9d06356ed4cfea3075151e3432a0de1ff5e53322 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 14:22:27 +0400 Subject: [PATCH 09/17] fix: handle non-default network use with xdist --- brownie/test/managers/master.py | 9 +++++++++ brownie/test/managers/runner.py | 8 +++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/brownie/test/managers/master.py b/brownie/test/managers/master.py index 301805af5..9d77ae9d1 100644 --- a/brownie/test/managers/master.py +++ b/brownie/test/managers/master.py @@ -18,6 +18,15 @@ class PytestBrownieMaster(PytestBrownieBase): Hooks in this class are loaded by the master process when using xdist. """ + def pytest_configure_node(self, node): + """ + Configure node information before it gets instantiated. + + Here we can pass arbitrary information to xdist workers via the + `workerinput` dict. + """ + node.workerinput["network"] = CONFIG.argv["network"] + def pytest_xdist_make_scheduler(self, config, log): """ Return a node scheduler implementation. diff --git a/brownie/test/managers/runner.py b/brownie/test/managers/runner.py index c78ce4e07..c636775eb 100644 --- a/brownie/test/managers/runner.py +++ b/brownie/test/managers/runner.py @@ -531,9 +531,11 @@ class PytestBrownieXdistRunner(PytestBrownieRunner): def __init__(self, config, project): self.workerid = int("".join(i for i in config.workerinput["workerid"] if i.isdigit())) - # TODO fix me - key = CONFIG.argv["network"] or CONFIG.settings["networks"]["default"] - CONFIG.networks[key]["cmd_settings"]["port"] += self.workerid + + # network ID is passed to the worker via `pytest_configure_node` in the master + network_id = config.workerinput["network"] or CONFIG.settings["networks"]["default"] + CONFIG.networks[network_id]["cmd_settings"]["port"] += self.workerid + super().__init__(config, project) def pytest_collection_modifyitems(self, items): From 2c3ade13af9b1b56b213735a2ca8b045e685881c Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 15:02:52 +0400 Subject: [PATCH 10/17] fix: ignore chain id --- brownie/network/rpc/hardhat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/brownie/network/rpc/hardhat.py b/brownie/network/rpc/hardhat.py index 9fb0ddd98..ee1f1eb66 100644 --- a/brownie/network/rpc/hardhat.py +++ b/brownie/network/rpc/hardhat.py @@ -12,6 +12,7 @@ from brownie.network.web3 import web3 CLI_FLAGS = {"port": "--port", "fork": "--fork", "fork_block": "--fork-block-number"} +IGNORED_SETTINGS = ["chain_id"] def launch(cmd: str, **kwargs: Dict) -> None: @@ -25,7 +26,7 @@ def launch(cmd: str, **kwargs: Dict) -> None: # else: # cmd += ".cmd" cmd_list = cmd.split(" ") - for key, value in [(k, v) for k, v in kwargs.items() if v]: + for key, value in [(k, v) for k, v in kwargs.items() if v and k not in IGNORED_SETTINGS]: try: cmd_list.extend([CLI_FLAGS[key], str(value)]) except KeyError: From 40449e55fe01a8d5905b8019c2f650ac1d22582b Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 15:04:47 +0400 Subject: [PATCH 11/17] fix: set block gas limit upon connection --- brownie/network/rpc/hardhat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/brownie/network/rpc/hardhat.py b/brownie/network/rpc/hardhat.py index ee1f1eb66..a7cad98aa 100644 --- a/brownie/network/rpc/hardhat.py +++ b/brownie/network/rpc/hardhat.py @@ -42,7 +42,8 @@ def launch(cmd: str, **kwargs: Dict) -> None: def on_connection() -> None: - pass + gas_limit = web3.eth.getBlock("latest").gasLimit + web3.provider.make_request("evm_setBlockGasLimit", [hex(gas_limit)]) # type: ignore def _request(method: str, args: List) -> int: From 236df2952f01bb5e2eebe9be5d66f1c95a25bc5b Mon Sep 17 00:00:00 2001 From: banteg Date: Wed, 7 Apr 2021 06:57:36 +0300 Subject: [PATCH 12/17] feat: default hardhat config --- brownie/network/rpc/hardhat.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/brownie/network/rpc/hardhat.py b/brownie/network/rpc/hardhat.py index a7cad98aa..771b0bed0 100644 --- a/brownie/network/rpc/hardhat.py +++ b/brownie/network/rpc/hardhat.py @@ -2,6 +2,7 @@ import sys import warnings +from pathlib import Path from subprocess import DEVNULL, PIPE from typing import Dict, List, Optional @@ -38,6 +39,12 @@ def launch(cmd: str, **kwargs: Dict) -> None: print(f"\nLaunching '{' '.join(cmd_list)}'...") out = DEVNULL if sys.platform == "win32" else PIPE + # required so hardhat considers the folder to be a hardhat project + # once hardhat network releases, we should be able to remove + hardhat_config = Path("hardhat.config.js") + if not hardhat_config.exists(): + hardhat_config.touch() + return psutil.Popen(cmd_list, stdin=DEVNULL, stdout=out, stderr=out) From 546916f2f812ed39db2773df395c6ea10aa9cb3b Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Wed, 7 Apr 2021 16:16:25 +0400 Subject: [PATCH 13/17] fix: adjust time offset after transactions --- brownie/network/state.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/brownie/network/state.py b/brownie/network/state.py index c5f0727a3..bb61f3c3e 100644 --- a/brownie/network/state.py +++ b/brownie/network/state.py @@ -312,6 +312,8 @@ def _add_to_undo_buffer(self, tx: Any, fn: Any, args: Tuple, kwargs: Dict) -> No else: self._redo_buffer.clear() self._current_id = rpc.Rpc().snapshot() + # ensure the local time offset is correct, in case it was modified by the transaction + self.sleep(0) def _network_connected(self) -> None: self._reset_id = None From f9c296c0a846f7a014cafa7760df6eb7e415c76f Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Fri, 9 Apr 2021 02:31:32 +0400 Subject: [PATCH 14/17] fix: formatting for `eth_call` --- brownie/network/middlewares/hardhat.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/brownie/network/middlewares/hardhat.py b/brownie/network/middlewares/hardhat.py index fd1368310..47ed860f4 100644 --- a/brownie/network/middlewares/hardhat.py +++ b/brownie/network/middlewares/hardhat.py @@ -17,12 +17,17 @@ def process_request(self, make_request: Callable, method: str, params: List) -> result = make_request(method, params) # modify Hardhat transaction error to mimick the format that Ganache uses - if method == "eth_sendTransaction" and "error" in result: + 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"): - txid = self.w3.eth.getBlock("latest")["transactions"][0] + if method == "eth_call": + # ganache returns a txid even on a failed eth_call, which is weird, + # but we still mimick it here for the sake of consistency + txid = "0x" + else: + txid = self.w3.eth.getBlock("latest")["transactions"][0].hex() data: Dict = {} - result["error"]["data"] = {txid.hex(): data} + result["error"]["data"] = {txid: data} message = message.split(": ", maxsplit=1)[-1] if message == "Transaction reverted without a reason": data.update({"error": "revert", "reason": None}) From eb08ff38730f9ee3356d5a1c1a157875e4e345f0 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sat, 7 Aug 2021 03:22:21 +0000 Subject: [PATCH 15/17] fix: autogenerate correct config file --- brownie/network/rpc/hardhat.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/brownie/network/rpc/hardhat.py b/brownie/network/rpc/hardhat.py index 771b0bed0..fbea885fb 100644 --- a/brownie/network/rpc/hardhat.py +++ b/brownie/network/rpc/hardhat.py @@ -15,6 +15,19 @@ CLI_FLAGS = {"port": "--port", "fork": "--fork", "fork_block": "--fork-block-number"} IGNORED_SETTINGS = ["chain_id"] +HARDHAT_CONFIG = """ +// autogenerated by brownie +// do not modify the existing settings +module.exports = { + networks: { + hardhat: { + hardfork: "berlin", + throwOnTransactionFailures: true, + throwOnCallFailures: true + } + } +}""" + def launch(cmd: str, **kwargs: Dict) -> None: """Launches the RPC client. @@ -43,7 +56,8 @@ def launch(cmd: str, **kwargs: Dict) -> None: # once hardhat network releases, we should be able to remove hardhat_config = Path("hardhat.config.js") if not hardhat_config.exists(): - hardhat_config.touch() + with hardhat_config.open("w") as fp: + fp.write(HARDHAT_CONFIG) return psutil.Popen(cmd_list, stdin=DEVNULL, stdout=out, stderr=out) From 190a2165080c6ff99709c7b4a13b332b51af1f83 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sat, 7 Aug 2021 03:22:43 +0000 Subject: [PATCH 16/17] fix: update hardhat middleware --- brownie/network/middlewares/hardhat.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/brownie/network/middlewares/hardhat.py b/brownie/network/middlewares/hardhat.py index 47ed860f4..c57eb943d 100644 --- a/brownie/network/middlewares/hardhat.py +++ b/brownie/network/middlewares/hardhat.py @@ -19,13 +19,15 @@ def process_request(self, make_request: Callable, method: str, params: List) -> # 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"): + if message.startswith("Error: VM Exception") or message.startswith( + "Error: Transaction reverted" + ): if method == "eth_call": # ganache returns a txid even on a failed eth_call, which is weird, # but we still mimick it here for the sake of consistency txid = "0x" else: - txid = self.w3.eth.getBlock("latest")["transactions"][0].hex() + txid = result["error"]["data"]["txHash"] data: Dict = {} result["error"]["data"] = {txid: data} message = message.split(": ", maxsplit=1)[-1] From 6eb8b40bb7c0f1b1cb0971f01a722d5ce8ae313b Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Sat, 7 Aug 2021 14:12:16 +0000 Subject: [PATCH 17/17] fix: check for config file in parent folders --- brownie/network/rpc/hardhat.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/brownie/network/rpc/hardhat.py b/brownie/network/rpc/hardhat.py index fbea885fb..4b454657b 100644 --- a/brownie/network/rpc/hardhat.py +++ b/brownie/network/rpc/hardhat.py @@ -52,11 +52,15 @@ def launch(cmd: str, **kwargs: Dict) -> None: print(f"\nLaunching '{' '.join(cmd_list)}'...") out = DEVNULL if sys.platform == "win32" else PIPE - # required so hardhat considers the folder to be a hardhat project - # once hardhat network releases, we should be able to remove - hardhat_config = Path("hardhat.config.js") - if not hardhat_config.exists(): - with hardhat_config.open("w") as fp: + # check parent folders for existence of a hardhat config, so this folder is + # considered a hardhat project. if none is found, create one. + config_exists = False + for path in Path("hardhat.config.js").absolute().parents: + if path.joinpath("hardhat.config.js").exists(): + config_exists = True + break + if not config_exists: + with Path("hardhat.config.js").open("w") as fp: fp.write(HARDHAT_CONFIG) return psutil.Popen(cmd_list, stdin=DEVNULL, stdout=out, stderr=out)