diff --git a/ape_hardhat/provider.py b/ape_hardhat/provider.py index 2722cd8..4f3c059 100644 --- a/ape_hardhat/provider.py +++ b/ape_hardhat/provider.py @@ -53,9 +53,9 @@ HARDHAT_CONFIG = """ // See https://hardhat.org/config/ for config options. module.exports = {{ + hardfork: "{hard_fork}", networks: {{ hardhat: {{ - hardfork: "{hard_fork}", // Base fee of 0 allows use of 0 gas price when testing initialBaseFeePerGas: 0, accounts: {{ @@ -234,7 +234,11 @@ def timeout(self) -> int: @property def _clean_uri(self) -> str: - return str(URL(self.uri).with_user(None).with_password(None)) + try: + return str(URL(self.uri).with_user(None).with_password(None)) + except ValueError: + # Likely isn't a real URI. + return self.uri @property def _port(self) -> Optional[int]: @@ -327,7 +331,15 @@ def is_connected(self) -> bool: @property def bin_path(self) -> Path: - return self.project_folder / "node_modules" / ".bin" / "hardhat" + suffix = Path("node_modules") / ".bin" / "hardhat" + options = (self.project_folder, Path.home()) + for base in options: + path = base / suffix + if path.exists(): + return path + + # Default to the expected path suffx (relative). + return suffix @property def hardhat_config_file(self) -> Path: @@ -368,6 +380,15 @@ 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) @@ -413,8 +434,11 @@ def connect(self): if self.is_connected: # Connects to already running process self._start() - elif self.config.manage_process: + elif self.config.manage_process and ( + "localhost" in self._host or "127.0.0.1" in self._host or self._host == "auto" + ): # Only do base-process setup if not connecting to already-running process + # and is running on localhost. super().connect() if self._host: @@ -438,9 +462,10 @@ def connect(self): except SubprocessError as exc: logger.info("Retrying Hardhat subprocess startup: %r", exc) self._host = None - else: + + elif not self.is_connected: raise HardhatProviderError( - f"Failed to connect to remote Hardhat node at {self._clean_uri}`" + f"Failed to connect to remote Hardhat node at '{self._clean_uri}'." ) def _set_web3(self): @@ -448,7 +473,14 @@ def _set_web3(self): return self._web3 = _create_web3(self.uri, self.timeout) - if not self._web3.is_connected(): + + try: + is_connected = self._web3.is_connected() + except Exception: + self._web3 = None + return + + if not is_connected: self._web3 = None return diff --git a/tests/conftest.py b/tests/conftest.py index c1bcdfc..564738b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import json +import shutil import subprocess import tempfile from contextlib import contextmanager @@ -282,3 +283,15 @@ def side_effect(cmd_ls): check_output_mock = mocker.patch("ape_hardhat.provider.check_output") check_output_mock.side_effect = side_effect return check_output_mock + + +@pytest.fixture +def no_hardhat_bin(project): + bin_path = project.path / "node_modules" / ".bin" / "hardhat" + bin_copy = project.path / "node_modules" / ".bin" / "hardhat-2" + shutil.move(bin_path, bin_copy) + + try: + yield + finally: + shutil.move(bin_copy, bin_path) diff --git a/tests/test_provider.py b/tests/test_provider.py index 0bd1356..e174dc8 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -1,3 +1,4 @@ +import shutil import tempfile from pathlib import Path @@ -12,7 +13,7 @@ from evm_trace import CallType from hexbytes import HexBytes -from ape_hardhat.exceptions import HardhatNotInstalledError, HardhatSubprocessError +from ape_hardhat.exceptions import HardhatNotInstalledError, HardhatProviderError from ape_hardhat.provider import HARDHAT_CHAIN_ID TEST_WALLET_ADDRESS = "0xD9b7fdb3FC0A0Aa3A507dCf0976bc23D49a9C7A3" @@ -262,16 +263,18 @@ def test_host(temp_config, networks, host): assert provider.uri == "https://example.com" -def test_use_different_config(temp_config, networks): +def test_use_different_config(temp_config, networks, project): data = {"hardhat": {"hardhat_config_file": "./hardhat.config.ts"}} with temp_config(data): provider = networks.ethereum.local.get_provider("hardhat") assert provider.hardhat_config_file.name == "hardhat.config.ts" assert "--config" in provider._get_command() - with pytest.raises(HardhatSubprocessError): - # This raises because Hardhat is not installed in the temp project. - provider.build_command() + actual = provider._get_command() + assert "npx" in actual[0] + # Will either be home dir hardhat if installed there + # or just the relative suffix (like in CI). + assert actual[1].endswith("node_modules/.bin/hardhat") def test_connect_when_hardhat_not_installed(networks, mock_web3, install_detection_fail): @@ -299,3 +302,30 @@ def test_get_virtual_machine_error_when_sol_panic(connected_provider): actual = connected_provider.get_virtual_machine_error(err) expected = "0x1" assert actual.revert_message == expected + + +def test_bin_path(connected_provider, project): + actual = connected_provider.bin_path + expected = project.path / "node_modules" / ".bin" / "hardhat" + assert actual == expected + + bin_cp = project.path / "node_modules" / ".bin" / "hardhat-2" + shutil.move(expected, bin_cp) + + try: + actual = connected_provider.bin_path + assert actual.as_posix().endswith("node_modules/.bin/hardhat") + + finally: + shutil.move(bin_cp, expected) + + +def test_remote_host(temp_config, networks, no_hardhat_bin, project): + data = {"hardhat": {"host": "https://example.com"}} + with temp_config(data): + with pytest.raises( + HardhatProviderError, + match=r"Failed to connect to remote Hardhat node at 'https://example.com'\.", + ): + with networks.ethereum.local.use_provider("hardhat"): + pass