diff --git a/README.md b/README.md index a5827a580..4659ca701 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Brownie is a Python-based development and testing framework for smart contracts ## Features -* Full support for [Solidity](https://github.com/ethereum/solidity) (`>=0.4.22`) and [Vyper](https://github.com/vyperlang/vyper) (`0.2.x`) +* Full support for [Solidity](https://github.com/ethereum/solidity) (`>=0.4.22`) and [Vyper](https://github.com/vyperlang/vyper) (`>=0.1.0-beta.16`) * Contract testing via [`pytest`](https://github.com/pytest-dev/pytest), including trace-based coverage evaluation * Property-based and stateful testing via [`hypothesis`](https://github.com/HypothesisWorks/hypothesis/tree/master/hypothesis-python) * Powerful debugging tools, including python-style tracebacks and custom error strings diff --git a/brownie/data/default-config.yaml b/brownie/data/default-config.yaml index b80d544e0..7a10f86a3 100644 --- a/brownie/data/default-config.yaml +++ b/brownie/data/default-config.yaml @@ -35,6 +35,8 @@ compiler: enabled: true runs: 200 remappings: null + vyper: + version: null console: show_colors: true diff --git a/brownie/exceptions.py b/brownie/exceptions.py index 10df237f2..e38379b76 100644 --- a/brownie/exceptions.py +++ b/brownie/exceptions.py @@ -159,15 +159,22 @@ class ProjectNotFound(Exception): class CompilerError(Exception): - def __init__(self, e: Type[psutil.Popen]) -> None: - err = [i["formattedMessage"] for i in yaml.safe_load(e.stdout_data)["errors"]] - super().__init__("Compiler returned the following errors:\n\n" + "\n".join(err)) + def __init__(self, e: Type[psutil.Popen], compiler: str = "Compiler") -> None: + self.compiler = compiler + + err_json = yaml.safe_load(e.stdout_data) + err = [i.get("formattedMessage") or i["message"] for i in err_json["errors"]] + super().__init__(f"{compiler} returned the following errors:\n\n" + "\n".join(err)) class IncompatibleSolcVersion(Exception): pass +class IncompatibleVyperVersion(Exception): + pass + + class PragmaError(Exception): pass diff --git a/brownie/network/contract.py b/brownie/network/contract.py index c63d2caf0..76704ac6e 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -16,6 +16,8 @@ from eth_utils import remove_0x_prefix from hexbytes import HexBytes from semantic_version import Version +from vvm import get_installable_vyper_versions +from vvm.utils.convert import to_vyper_version from brownie._config import CONFIG, REQUEST_HEADERS from brownie.convert.datatypes import Wei @@ -670,19 +672,30 @@ def from_explorer( if not is_verified: return cls.from_abi(name, address, abi, owner) - try: - version = Version(data["result"][0]["CompilerVersion"].lstrip("v")).truncate() - except Exception: - version = Version("0.0.0") - if version < Version("0.4.22") or ( - # special case for OSX because installing 0.4.x versions is problematic - sys.platform == "darwin" - and version < Version("0.5.0") - and f"v{version}" not in solcx.get_installed_solc_versions() - ): + compiler_str = data["result"][0]["CompilerVersion"] + if compiler_str.startswith("vyper:"): + try: + version = to_vyper_version(compiler_str[6:]) + is_compilable = version in get_installable_vyper_versions() + except Exception: + is_compilable = False + else: + try: + version = Version(compiler_str.lstrip("v")).truncate() + if sys.platform == "darwin": + is_compilable = ( + version >= Version("0.5.0") + or f"v{version}" in solcx.get_installed_solc_versions() + ) + else: + is_compilable = f"v{version}" in solcx.get_available_solc_versions() + except Exception: + is_compilable = False + + if not is_compilable: if not silent: warnings.warn( - f"{address}: target compiler '{data['result'][0]['CompilerVersion']}' is " + f"{address}: target compiler '{compiler_str}' is " "unsupported by Brownie. Some functionality will not be available.", BrownieCompilerWarning, ) @@ -696,9 +709,10 @@ def from_explorer( if evm_version == "Default": evm_version = None - if data["result"][0]["SourceCode"].startswith("{"): + source_str = "\n".join(data["result"][0]["SourceCode"].splitlines()) + if source_str.startswith("{"): # source was verified using compiler standard JSON - input_json = json.loads(data["result"][0]["SourceCode"][1:-1]) + input_json = json.loads(source_str[1:-1]) sources = {k: v["content"] for k, v in input_json["sources"].items()} evm_version = input_json["settings"].get("evmVersion", evm_version) @@ -709,10 +723,19 @@ def from_explorer( output_json = compiler.compile_from_input_json(input_json) build_json = compiler.generate_build_json(input_json, output_json) else: - # source was submitted as a single flattened file - sources = {f"{name}-flattened.sol": data["result"][0]["SourceCode"]} + # source was submitted as a single file + if compiler_str.startswith("vyper"): + path_str = f"{name}.vy" + else: + path_str = f"{name}-flattened.sol" + + sources = {path_str: source_str} build_json = compiler.compile_and_format( - sources, solc_version=str(version), optimizer=optimizer, evm_version=evm_version + sources, + solc_version=str(version), + vyper_version=str(version), + optimizer=optimizer, + evm_version=evm_version, ) build_json = build_json[name] diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py index 42c78181a..2608d1429 100644 --- a/brownie/project/compiler/__init__.py +++ b/brownie/project/compiler/__init__.py @@ -19,6 +19,7 @@ set_solc_version, ) from brownie.project.compiler.utils import merge_natspec +from brownie.project.compiler.vyper import find_vyper_versions, set_vyper_version from brownie.utils import notify from . import solidity, vyper @@ -47,6 +48,7 @@ def compile_and_format( contract_sources: Dict[str, str], solc_version: Optional[str] = None, + vyper_version: Optional[str] = None, optimize: bool = True, runs: int = 200, evm_version: Optional[str] = None, @@ -86,10 +88,15 @@ def compile_and_format( build_json: Dict = {} compiler_targets = {} - vyper_paths = [i for i in contract_sources if Path(i).suffix == ".vy"] - if vyper_paths: - compiler_targets["vyper"] = vyper_paths - + vyper_sources = {k: v for k, v in contract_sources.items() if Path(k).suffix == ".vy"} + if vyper_sources: + # TODO add `vyper_version` input arg to manually specify, support in config file + if vyper_version is None: + compiler_targets.update( + find_vyper_versions(vyper_sources, install_needed=True, silent=silent) + ) + else: + compiler_targets[vyper_version] = list(vyper_sources) solc_sources = {k: v for k, v in contract_sources.items() if Path(k).suffix == ".sol"} if solc_sources: if solc_version is None: @@ -104,7 +111,8 @@ def compile_and_format( for version, path_list in compiler_targets.items(): compiler_data: Dict = {} - if version == "vyper": + if path_list[0].endswith(".vy"): + set_vyper_version(version) language = "Vyper" compiler_data["version"] = str(vyper.get_version()) interfaces = {k: v for k, v in interface_sources.items() if Path(k).suffix != ".sol"} @@ -287,8 +295,8 @@ def generate_build_json( abi = output_json["contracts"][path_str][contract_name]["abi"] natspec = merge_natspec( - output_json["contracts"][path_str][contract_name]["devdoc"], - output_json["contracts"][path_str][contract_name]["userdoc"], + output_json["contracts"][path_str][contract_name].get("devdoc", {}), + output_json["contracts"][path_str][contract_name].get("userdoc", {}), ) output_evm = output_json["contracts"][path_str][contract_name]["evm"] diff --git a/brownie/project/compiler/solidity.py b/brownie/project/compiler/solidity.py index 491d3a757..78b5fe058 100644 --- a/brownie/project/compiler/solidity.py +++ b/brownie/project/compiler/solidity.py @@ -69,7 +69,7 @@ def compile_from_input_json( allow_paths=allow_paths, ) except solcx.exceptions.SolcError as e: - raise CompilerError(e) + raise CompilerError(e, "solc") def set_solc_version(version: str) -> str: diff --git a/brownie/project/compiler/vyper.py b/brownie/project/compiler/vyper.py index 8218a30d9..4554940cd 100644 --- a/brownie/project/compiler/vyper.py +++ b/brownie/project/compiler/vyper.py @@ -1,20 +1,40 @@ #!/usr/bin/python3 +import logging from collections import deque from hashlib import sha1 -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Union -import vyper +import vvm from semantic_version import Version -from vyper.cli import vyper_json -from vyper.exceptions import VyperException +from brownie.exceptions import CompilerError, IncompatibleVyperVersion +from brownie.project import sources from brownie.project.compiler.utils import expand_source_map from brownie.project.sources import is_inside_offset +vvm_logger = logging.getLogger("vvm") +vvm_logger.setLevel(10) +sh = logging.StreamHandler() +sh.setLevel(10) +sh.setFormatter(logging.Formatter("%(message)s")) +vvm_logger.addHandler(sh) + +AVAILABLE_VYPER_VERSIONS = None + def get_version() -> Version: - return Version.coerce(vyper.__version__) + return vvm.get_vyper_version() + + +def set_vyper_version(version: str) -> str: + """Sets the vyper version. If not available it will be installed.""" + try: + vvm.set_vyper_version(version, silent=True) + except vvm.exceptions.VyperNotInstalled: + install_vyper(version) + vvm.set_vyper_version(version, silent=True) + return str(vvm.get_vyper_version()) def get_abi(contract_source: str, name: str) -> Dict: @@ -23,7 +43,7 @@ def get_abi(contract_source: str, name: str) -> Dict: This function is deprecated in favor of `brownie.project.compiler.get_abi` """ - compiled = vyper_json.compile_json( + compiled = vvm.compile_standard( { "language": "Vyper", "sources": {name: {"content": contract_source}}, @@ -33,6 +53,138 @@ def get_abi(contract_source: str, name: str) -> Dict: return {name: compiled["contracts"][name][name]["abi"]} +def _get_vyper_version_list() -> Tuple[List, List]: + global AVAILABLE_VYPER_VERSIONS + installed_versions = vvm.get_installed_vyper_versions() + if AVAILABLE_VYPER_VERSIONS is None: + try: + AVAILABLE_VYPER_VERSIONS = vvm.get_installable_vyper_versions() + except ConnectionError: + if not installed_versions: + raise ConnectionError("Vyper not installed and cannot connect to GitHub") + AVAILABLE_VYPER_VERSIONS = installed_versions + return AVAILABLE_VYPER_VERSIONS, installed_versions + + +def install_vyper(*versions: str) -> None: + """Installs vyper versions.""" + for version in versions: + vvm.install_vyper(version, show_progress=True) + + +def find_vyper_versions( + contract_sources: Dict[str, str], + install_needed: bool = False, + install_latest: bool = False, + silent: bool = True, +) -> Dict: + + """ + Analyzes contract pragmas and determines which vyper version(s) to use. + + Args: + contract_sources: a dictionary in the form of {'path': "source code"} + install_needed: if True, will install when no installed version matches + the contract pragma + install_latest: if True, will install when a newer version is available + than the installed one + silent: set to False to enable verbose reporting + + Returns: dictionary of {'version': ['path', 'path', ..]} + """ + + available_versions, installed_versions = _get_vyper_version_list() + + pragma_specs: Dict = {} + to_install = set() + new_versions = set() + + for path, source in contract_sources.items(): + pragma_specs[path] = sources.get_vyper_pragma_spec(source, path) + version = pragma_specs[path].select(installed_versions) + + if not version and not (install_needed or install_latest): + raise IncompatibleVyperVersion( + f"No installed vyper version matching '{pragma_specs[path]}' in '{path}'" + ) + + # if no installed version of vyper matches the pragma, find the latest available version + latest = pragma_specs[path].select(available_versions) + + if not version and not latest: + raise IncompatibleVyperVersion( + f"No installable vyper version matching '{pragma_specs[path]}' in '{path}'" + ) + + if not version or (install_latest and latest > version): + to_install.add(latest) + elif latest and latest > version: + new_versions.add(str(version)) + + # install new versions if needed + if to_install: + install_vyper(*to_install) + installed_versions = vvm.get_installed_vyper_versions() + elif new_versions and not silent: + print( + f"New compatible vyper version{'s' if len(new_versions) > 1 else ''}" + f" available: {', '.join(new_versions)}" + ) + + # organize source paths by latest available vyper version + compiler_versions: Dict = {} + for path, spec in pragma_specs.items(): + version = spec.select(installed_versions) + compiler_versions.setdefault(str(version), []).append(path) + + return compiler_versions + + +def find_best_vyper_version( + contract_sources: Dict[str, str], + install_needed: bool = False, + install_latest: bool = False, + silent: bool = True, +) -> str: + + """ + Analyze contract pragma and find the best compatible version across multiple sources. + + Args: + contract_sources: a dictionary in the form of {'path': "source code"} + install_needed: if True, will install when no installed version matches + the contract pragma + install_latest: if True, will install when a newer version is available + than the installed one + silent: set to False to enable verbose reporting + + Returns: version string + """ + + available_versions, installed_versions = _get_vyper_version_list() + + for path, source in contract_sources.items(): + + pragma_spec = sources.get_vyper_pragma_spec(source, path) + installed_versions = [i for i in installed_versions if i in pragma_spec] + available_versions = [i for i in available_versions if i in pragma_spec] + + if not available_versions: + raise IncompatibleVyperVersion("No installable vyper version compatible across all sources") + + if not installed_versions and not (install_needed or install_latest): + raise IncompatibleVyperVersion("No installed vyper version compatible across all sources") + + if max(available_versions) > max(installed_versions, default=Version("0.0.0")): + if install_latest or (install_needed and not installed_versions): + install_vyper(max(available_versions)) + return str(max(available_versions)) + if not silent: + print(f"New compatible vyper version available: {max(available_versions)}") + + return str(max(installed_versions)) + + def compile_from_input_json( input_json: Dict, silent: bool = True, allow_paths: Optional[str] = None ) -> Dict: @@ -41,7 +193,7 @@ def compile_from_input_json( Compiles contracts from a standard input json. Args: - input_json: solc input json + input_json: vyper input json silent: verbose reporting allow_paths: compiler allowed filesystem import path @@ -51,27 +203,38 @@ def compile_from_input_json( if not silent: print("Compiling contracts...") print(f" Vyper version: {get_version()}") + if get_version() < Version("0.1.0-beta.17"): + outputs = input_json["settings"]["outputSelection"]["*"]["*"] + outputs.remove("userdoc") + outputs.remove("devdoc") try: - return vyper_json.compile_json(input_json, root_path=allow_paths) - except VyperException as exc: - raise exc.with_traceback(None) + return vvm.compile_standard(input_json, base_path=allow_paths) + except vvm.exceptions.VyperError as exc: + raise CompilerError(exc, "vyper") def _get_unique_build_json( - output_evm: Dict, path_str: str, contract_name: str, ast_json: Dict, offset: Tuple + output_evm: Dict, path_str: str, contract_name: str, ast_json: Union[Dict, List], offset: Tuple ) -> Dict: + + ast: List + if isinstance(ast_json, dict): + ast = ast_json["body"] + else: + ast = ast_json + pc_map, statement_map, branch_map = _generate_coverage_data( output_evm["deployedBytecode"]["sourceMap"], output_evm["deployedBytecode"]["opcodes"], contract_name, - ast_json["body"], + ast, ) return { "allSourcePaths": {"0": path_str}, "bytecode": output_evm["bytecode"]["object"], "bytecodeSha1": sha1(output_evm["bytecode"]["object"].encode()).hexdigest(), "coverageMap": {"statements": statement_map, "branches": branch_map}, - "dependencies": _get_dependencies(ast_json["body"]), + "dependencies": _get_dependencies(ast), "offset": offset, "pcMap": pc_map, "type": "contract", diff --git a/brownie/project/main.py b/brownie/project/main.py index 791f3352e..7e6f26952 100644 --- a/brownie/project/main.py +++ b/brownie/project/main.py @@ -32,6 +32,7 @@ from brownie.exceptions import ( BrownieEnvironmentWarning, InvalidPackage, + PragmaError, ProjectAlreadyLoaded, ProjectNotFound, ) @@ -87,6 +88,7 @@ def _compile(self, contract_sources: Dict, compiler_config: Dict, silent: bool) build_json = compiler.compile_and_format( contract_sources, solc_version=compiler_config["solc"].get("version", None), + vyper_version=compiler_config["vyper"].get("version", None), optimize=compiler_config["solc"].get("optimize", None), runs=compiler_config["solc"].get("runs", None), evm_version=compiler_config["evm_version"], @@ -585,6 +587,7 @@ def from_ethpm(uri: str) -> "TempProject": compiler_config = { "evm_version": None, "solc": {"version": None, "optimize": True, "runs": 200}, + "vyper": {"version": None}, } project = TempProject(manifest["package_name"], manifest["sources"], compiler_config) if web3.isConnected(): @@ -597,20 +600,59 @@ def from_ethpm(uri: str) -> "TempProject": def compile_source( source: str, solc_version: Optional[str] = None, + vyper_version: Optional[str] = None, optimize: bool = True, runs: Optional[int] = 200, evm_version: Optional[str] = None, ) -> "TempProject": - """Compiles the given source code string and returns a TempProject container with - the ContractContainer instances.""" + """ + Compile the given source code string and return a TempProject container with + the ContractContainer instances. + """ + compiler_config: Dict = {"evm_version": evm_version, "solc": {}, "vyper": {}} - compiler_config: Dict = {"evm_version": evm_version} + # if no compiler version was given, first try to find a Solidity pragma + if solc_version is None and vyper_version is None: + try: + solc_version = compiler.solidity.find_best_solc_version( + {"": source}, install_needed=True, silent=False + ) + except PragmaError: + pass - if solc_version is not None or source.lstrip().startswith("pragma"): - compiler_config["solc"] = {"version": solc_version, "optimize": optimize, "runs": runs} - return TempProject("TempSolcProject", {".sol": source}, compiler_config) + if vyper_version is None: + # if no vyper compiler version is given, try to compile using solidity + compiler_config["solc"] = { + "version": solc_version or str(compiler.solidity.get_version()), + "optimize": optimize, + "runs": runs, + } + try: + return TempProject("TempSolcProject", {".sol": source}, compiler_config) + except Exception as exc: + # if compilation fails, raise when a solc version was given or we found a pragma + if solc_version is not None: + raise exc + + if vyper_version is None: + # if no vyper compiler version was given, try to find a pragma + try: + vyper_version = compiler.vyper.find_best_vyper_version( + {"": source}, install_needed=True, silent=False + ) + except PragmaError: + pass - return TempProject("TempVyperProject", {".vy": source}, compiler_config) + compiler_config["vyper"] = {"version": vyper_version or compiler.vyper.get_version()} + try: + return TempProject("TempVyperProject", {".vy": source}, compiler_config) + except Exception as exc: + if solc_version is None and vyper_version is None: + raise PragmaError( + "No compiler version specified, no pragma statement in the source, " + "and compilation failed with both solc and vyper" + ) from None + raise exc def load(project_path: Union[Path, str, None] = None, name: Optional[str] = None) -> "Project": diff --git a/brownie/project/sources.py b/brownie/project/sources.py index d57d42695..253ec69b2 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -7,6 +7,7 @@ from typing import Dict, List, Optional, Tuple from semantic_version import NpmSpec +from vvm.utils.convert import to_vyper_version from brownie.exceptions import NamespaceCollision, PragmaError from brownie.utils import color @@ -203,3 +204,36 @@ def get_pragma_spec(source: str, path: Optional[str] = None) -> NpmSpec: if path: raise PragmaError(f"No version pragma in '{path}'") raise PragmaError("String does not contain a version pragma") + + +def get_vyper_pragma_spec(source: str, path: Optional[str] = None) -> NpmSpec: + """ + Extracts pragma information from Vyper source code. + + Args: + source: Vyper source code + path: Optional path to the source (only used for error reporting) + + Returns: NpmSpec object + """ + pragma_match = next(re.finditer(r"(?:\n|^)\s*#\s*@version\s*([^\n]*)", source), None) + if pragma_match is None: + if path: + raise PragmaError(f"No version pragma in '{path}'") + raise PragmaError("String does not contain a version pragma") + + pragma_string = pragma_match.groups()[0] + pragma_string = " ".join(pragma_string.split()) + try: + return NpmSpec(pragma_string) + except ValueError: + pass + try: + # special case for Vyper 0.1.0-beta.X + version = to_vyper_version(pragma_string) + return NpmSpec(str(version)) + except Exception: + pass + + path = "" if path is None else f"{path}: " + raise PragmaError(f"{path}Cannot parse Vyper version from pragma: {pragma_string}") diff --git a/brownie/utils/color.py b/brownie/utils/color.py index 65cad4f08..cb8c0be5f 100755 --- a/brownie/utils/color.py +++ b/brownie/utils/color.py @@ -9,7 +9,6 @@ from pygments.formatters import get_formatter_by_name from pygments.lexers import PythonLexer from pygments_lexer_solidity import SolidityLexer -from vyper.exceptions import VyperException from brownie._config import CONFIG @@ -149,18 +148,16 @@ def format_tb( if code: tb[i] += f"\n{code}" - msg = str(exc) - if isinstance(exc, VyperException): - # apply syntax highlight and remove traceback on vyper exceptions - msg = self.highlight(msg) - if not CONFIG.argv["tb"]: - tb.clear() - from brownie.exceptions import CompilerError + msg = str(exc) + if isinstance(exc, CompilerError): # apply syntax highlighting on solc exceptions - msg = self.highlight(msg, SolidityLexer()) + if exc.compiler == "solc": + msg = self.highlight(msg, SolidityLexer()) + else: + msg = self.highlight(msg) if not CONFIG.argv["tb"]: tb.clear() diff --git a/docs/account-management.rst b/docs/account-management.rst index d2d150652..7d6832a4f 100644 --- a/docs/account-management.rst +++ b/docs/account-management.rst @@ -63,7 +63,7 @@ Unlocking Accounts In order to access a local account from a script or console, you must first unlock it. This is done via the :func:`Accounts.load ` method: -.. code-block:: +.. code-block:: python >>> accounts [] @@ -92,4 +92,4 @@ To do so, add the account to the ``unlock`` setting in a project's :ref:`configu - 0x0063046686E46Dc6F15918b61AE2B121458534a5 The unlocked accounts are automatically added to the :func:`Accounts ` container. -Note that you might need to fund the unlocked accounts manually. \ No newline at end of file +Note that you might need to fund the unlocked accounts manually. diff --git a/docs/compile.rst b/docs/compile.rst index 5d09a2fb2..f6b2298bc 100644 --- a/docs/compile.rst +++ b/docs/compile.rst @@ -17,7 +17,7 @@ If one or more contracts are unable to compile, Brownie raises an exception with Supported Languages =================== -Brownie supports Solidity (``>=0.4.22``) and Vyper (``0.2.x``). The file extension determines which compiler is used: +Brownie supports Solidity (``>=0.4.22``) and Vyper (``>=0.1.0-beta.16``). The file extension determines which compiler is used: * Solidity: ``.sol`` * Vyper: ``.vy`` @@ -50,6 +50,8 @@ Compiler settings may be declared in the :ref:`configuration file ` of a optimizer: enabled: true runs: 200 + vyper: + version: null Modifying any compiler settings will result in a full recompile of the project. @@ -58,7 +60,7 @@ Setting the Compiler Version .. note:: - Brownie supports Solidity versions ``>=0.4.22`` and Vyper version ``0.2.x``. + Brownie supports Solidity versions ``>=0.4.22`` and Vyper versions ``>=0.1.0-beta.16``. If a compiler version is set in the configuration file, all contracts in the project are compiled using that version. The compiler is installed automatically if not already present. The version should be given as a string in the format ``0.x.x``. @@ -155,9 +157,14 @@ With the ``OpenZeppelin/openzeppelin-contracts@3.0.0`` package installed, and th Installing the Compiler ======================= -If you wish to manually install a different version of ``solc``: +If you wish to manually install a different version of ``solc`` or ``vyper``: .. code-block:: python >>> from brownie.project.compiler import install_solc >>> install_solc("0.5.10") + +.. code-block:: python + + >>> from brownie.project.compiler import install_vyper + >>> install_vyper("0.2.4") diff --git a/docs/config.rst b/docs/config.rst index 2ddabd1b9..f8c085ba8 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -178,6 +178,16 @@ Compiler settings. See :ref:`compiler settings` for more infor - zeppelin=/usr/local/lib/open-zeppelin/contracts/ - github.com/ethereum/dapp-bin/=/usr/local/lib/dapp-bin/ +.. py:attribute:: compiler.vyper + + Settings specific to the Vyper compiler. + + .. py:attribute:: version + + The version of vyper to use. Should be given as a string in the format ``0.x.x``. If set to ``null``, the version is set based on the contract pragma. Brownie supports vyper versions ``>=0.1.0-beta.16``. + + default value: ``null`` + Console ------- diff --git a/requirements.txt b/requirements.txt index 8af478734..bfc71fce6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,5 @@ pyyaml>=5.3.0,<6.0.0 requests>=2.23.0,<3.0.0 semantic-version==2.8.5 tqdm==4.48.0 -vyper>=0.2.3,<0.3.0 +vvm>=0.0.2,<0.1.0 web3==5.11.1 diff --git a/setup.cfg b/setup.cfg index aafbcbc15..fea2ccb20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ force_grid_wrap = 0 include_trailing_comma = True known_standard_library = tkinter known_first_party = brownie -known_third_party = _pytest,black,ens,eth_abi,eth_account,eth_event,eth_hash,eth_keys,eth_utils,ethpm,hexbytes,hypothesis,mythx_models,prompt_toolkit,psutil,py,pygments,pygments_lexer_solidity,pytest,pythx,requests,semantic_version,setuptools,solcast,solcx,tqdm,vyper,web3,xdist,yaml +known_third_party = _pytest,black,ens,eth_abi,eth_account,eth_event,eth_hash,eth_keys,eth_utils,ethpm,hexbytes,hypothesis,mythx_models,prompt_toolkit,psutil,py,pygments,pygments_lexer_solidity,pytest,pythx,requests,semantic_version,setuptools,solcast,solcx,tqdm,vvm,web3,xdist,yaml line_length = 100 multi_line_output = 3 use_parentheses = True @@ -28,7 +28,7 @@ follow_imports = silent follow_imports = skip [tool:pytest] -addopts = +addopts = -p no:pytest-brownie --cov brownie/ --cov-report term diff --git a/tests/data/brownie-test-project/contracts/VyperTester.vy b/tests/data/brownie-test-project/contracts/VyperTester.vy index 3ceba82d4..804e9b863 100644 --- a/tests/data/brownie-test-project/contracts/VyperTester.vy +++ b/tests/data/brownie-test-project/contracts/VyperTester.vy @@ -1,3 +1,5 @@ +# @version ^0.2.0 + stuff: public(uint256[4]) diff --git a/tests/data/ipfs-cache-mock/testipfs-vyper b/tests/data/ipfs-cache-mock/testipfs-vyper index 4d0603704..811531b83 100644 --- a/tests/data/ipfs-cache-mock/testipfs-vyper +++ b/tests/data/ipfs-cache-mock/testipfs-vyper @@ -1 +1 @@ -{"contract_types":{"Bar":{"abi":[{"constant":false,"inputs":[],"name":"baz","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}],"source_path":"interfaces/Bar.json"}},"manifest_version":"2","package_name":"vyper","sources":{"contracts/foo.vy":"import interfaces.Bar as Bar\n@external\ndef foo() -> bool: return True"},"version":"1.0.0"} +{"contract_types":{"Bar":{"abi":[{"constant":false,"inputs":[],"name":"baz","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}],"source_path":"interfaces/Bar.json"}},"manifest_version":"2","package_name":"vyper","sources":{"contracts/foo.vy":"# @version 0.2.4\nimport interfaces.Bar as Bar\n@external\ndef foo() -> bool: return True"},"version":"1.0.0"} diff --git a/tests/network/contract/test_contract.py b/tests/network/contract/test_contract.py index 2083366bb..391a482b3 100644 --- a/tests/network/contract/test_contract.py +++ b/tests/network/contract/test_contract.py @@ -126,9 +126,9 @@ def test_deprecated_init_ethpm(ipfs_mock, network): def test_from_explorer(network): network.connect("mainnet") - contract = Contract.from_explorer("0x2af5d2ad76741191d15dfe7bf6ac92d4bd912ca3") + contract = Contract.from_explorer("0x973e52691176d36453868d9d86572788d27041a9") - assert contract._name == "LEO" + assert contract._name == "DxToken" assert "pcMap" in contract._build assert len(contract._sources) == 1 @@ -165,9 +165,30 @@ def test_from_explorer_osx_pre_050(network, monkeypatch): assert "pcMap" not in contract._build -def test_from_explorer_vyper(network): +def test_from_explorer_vyper_supported_020(network): + network.connect("mainnet") + + # curve yCRV gauge - `0.2.4` + contract = Contract.from_explorer("0xFA712EE4788C042e2B7BB55E6cb8ec569C4530c1") + + assert contract._name == "Vyper_contract" + assert "pcMap" in contract._build + + +def test_from_explorer_vyper_supported_010(network): + network.connect("mainnet") + + # curve cDAI/cUSDC - `0.1.0-beta.16` + contract = Contract.from_explorer("0x845838DF265Dcd2c412A1Dc9e959c7d08537f8a2") + + assert contract._name == "Vyper_contract" + assert "pcMap" in contract._build + + +def test_from_explorer_vyper_old_version(network): network.connect("mainnet") with pytest.warns(BrownieCompilerWarning): + # uniswap v1 - `0.1.0-beta.4` contract = Contract.from_explorer("0x2157a7894439191e520825fe9399ab8655e0f708") assert contract._name == "Vyper_contract" diff --git a/tests/network/contract/test_contractcall.py b/tests/network/contract/test_contractcall.py index 0ee7c9b8c..57fee0a80 100644 --- a/tests/network/contract/test_contractcall.py +++ b/tests/network/contract/test_contractcall.py @@ -27,6 +27,7 @@ def test_transact(accounts, tester): def test_block_identifier(accounts, history): contract = compile_source( """ +# @version 0.2.4 foo: public(int128) @external diff --git a/tests/network/transaction/test_internal_transfer.py b/tests/network/transaction/test_internal_transfer.py index 8d64ac858..497c8cf1f 100755 --- a/tests/network/transaction/test_internal_transfer.py +++ b/tests/network/transaction/test_internal_transfer.py @@ -7,6 +7,8 @@ def test_to_eoa(accounts): container = compile_source( """ +# @version 0.2.4 + @external @payable def send_ether(receivers: address[3]) -> bool: @@ -28,6 +30,8 @@ def send_ether(receivers: address[3]) -> bool: def test_to_contract(accounts): container = compile_source( """ +# @version 0.2.4 + @external @payable def send_ether(receiver: address) -> bool: @@ -50,6 +54,8 @@ def __default__(): def test_types(accounts): container = compile_source( """ +# @version 0.2.4 + @external @payable def send_ether(receiver: address) -> bool: @@ -67,6 +73,8 @@ def send_ether(receiver: address) -> bool: def test_via_create_vyper(accounts): container = compile_source( """ +# @version 0.2.4 + @external @payable def send_ether() -> bool: diff --git a/tests/network/transaction/test_new_contracts.py b/tests/network/transaction/test_new_contracts.py index 608b6520a..da844df2b 100644 --- a/tests/network/transaction/test_new_contracts.py +++ b/tests/network/transaction/test_new_contracts.py @@ -72,14 +72,14 @@ def test_solidity_reverts(solcproject, console_mode): vyper_forwarder_source = """ - +# @version 0.2.4 @external def create_new(_target: address) -> address: return create_forwarder_to(_target) """ vyper_factory_source = """ - +# @version 0.2.4 @external @view def foo() -> uint256: diff --git a/tests/network/transaction/test_revert_msg.py b/tests/network/transaction/test_revert_msg.py index 519044d5a..02f2688bf 100644 --- a/tests/network/transaction/test_revert_msg.py +++ b/tests/network/transaction/test_revert_msg.py @@ -146,6 +146,12 @@ def test_vyper_revert_reasons(vypertester, console_mode): def test_deployment_size_limit(accounts, console_mode): - code = f"@external\ndef baz():\n assert msg.sender != ZERO_ADDRESS, '{'blah'*10000}'" - tx = compile_source(code).Vyper.deploy({"from": accounts[0]}) + code = f""" +# @version 0.2.4 + +@external +def baz(): + assert msg.sender != ZERO_ADDRESS, '{'blah'*10000}' + """ + tx = compile_source(code, vyper_version="").Vyper.deploy({"from": accounts[0]}) assert tx.revert_msg == "exceeds EIP-170 size limit" diff --git a/tests/project/compiler/test_main_compiler.py b/tests/project/compiler/test_main_compiler.py index 9471d4e6d..330ca3489 100644 --- a/tests/project/compiler/test_main_compiler.py +++ b/tests/project/compiler/test_main_compiler.py @@ -11,6 +11,7 @@ def test_multiple_compilers(solc4source, vysource): { "solc4.sol": solc4source, "vyper.vy": vysource, + "vyperold.vy": "# @version 0.1.0b16\n", "solc6.sol": "pragma solidity 0.6.2; contract Foo {}", } ) diff --git a/tests/project/compiler/test_vyper.py b/tests/project/compiler/test_vyper.py index 4c85b0e78..67dfd4493 100644 --- a/tests/project/compiler/test_vyper.py +++ b/tests/project/compiler/test_vyper.py @@ -3,23 +3,18 @@ import functools import pytest -import vyper -from semantic_version import Version -from vyper.exceptions import StructureException +from brownie.exceptions import CompilerError from brownie.project import build, compiler @pytest.fixture def vyjson(vysource): + compiler.vyper.set_vyper_version("0.2.4") input_json = compiler.generate_input_json({"path.vy": vysource}, language="Vyper") yield compiler.compile_from_input_json(input_json) -def test_version(): - assert compiler.vyper.get_version() == Version.coerce(vyper.__version__) - - def test_generate_input_json(vysource): input_json = compiler.generate_input_json({"path.vy": vysource}, language="Vyper") assert "optimizer" not in input_json["settings"] @@ -37,9 +32,11 @@ def test_compile_input_json(vyjson): assert "path" in vyjson["contracts"]["path.vy"] -def test_compile_input_json_raises(): +@pytest.mark.parametrize("vyper_version", ["0.1.0-beta.16", "0.2.4"]) +def test_compile_input_json_raises(vyper_version): + compiler.vyper.set_vyper_version(vyper_version) input_json = compiler.generate_input_json({"path.vy": "potato"}, language="Vyper") - with pytest.raises(StructureException): + with pytest.raises(CompilerError): compiler.compile_from_input_json(input_json) @@ -50,6 +47,7 @@ def test_build_json_keys(vysource): def test_dependencies(vysource): code = """ +# @version 0.2.4 import path as foo from vyper.interfaces import ERC20 from foo import bar @@ -62,7 +60,7 @@ def test_dependencies(vysource): def test_compile_empty(): - compiler.compile_and_format({"empty.vy": ""}) + compiler.compile_and_format({"empty.vy": ""}, vyper_version="0.2.4") def test_get_abi(): @@ -83,5 +81,5 @@ def test_get_abi(): def test_size_limit(capfd): code = f"@external\ndef baz():\n assert msg.sender != ZERO_ADDRESS, '{'blah'*10000}'" - compiler.compile_and_format({"foo.vy": code}) + compiler.compile_and_format({"foo.vy": code}, vyper_version="0.2.4") assert "exceeds EIP-170 limit of 24577" in capfd.readouterr()[0] diff --git a/tests/project/conftest.py b/tests/project/conftest.py index a5b6d9679..362fbe015 100644 --- a/tests/project/conftest.py +++ b/tests/project/conftest.py @@ -54,6 +54,7 @@ def solc4source(): def vysource(): return """ # comments are totally kickass +# @version 0.2.4 @external def test() -> bool: return True diff --git a/tests/project/main/test_contract_syntaxes.py b/tests/project/main/test_contract_syntaxes.py index 9141cfab6..6c7bb30ea 100644 --- a/tests/project/main/test_contract_syntaxes.py +++ b/tests/project/main/test_contract_syntaxes.py @@ -28,6 +28,7 @@ def test_only_events(newproject, minor): def test_vyper_external_call(newproject): source = """ +# @version 0.2.4 from vyper.interfaces import ERC721 @external def interfaceTest(): diff --git a/tests/project/main/test_interfaces_vyper.py b/tests/project/main/test_interfaces_vyper.py index 87d32b66b..21b8ae064 100644 --- a/tests/project/main/test_interfaces_vyper.py +++ b/tests/project/main/test_interfaces_vyper.py @@ -1,11 +1,13 @@ import json INTERFACE = """ +# @version 0.2.4 @external def baz() -> bool: pass """ CONTRACT = """ +# @version 0.2.4 import interfaces.Bar as Bar implements: Bar diff --git a/tests/project/main/test_main_project.py b/tests/project/main/test_main_project.py index 1995b8b06..0fcb64bb0 100644 --- a/tests/project/main/test_main_project.py +++ b/tests/project/main/test_main_project.py @@ -137,3 +137,17 @@ def test_from_ethpm_with_deployments(ipfs_mock, project, network): p = project.from_ethpm("ipfs://testipfs-math") assert len(p.Math) == 1 assert p.Math[0].address == "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2" + + +def test_compile_source_solc_without_pragma(project): + project.compile_source("""contract X {}""") + + +def test_compile_source_vyper_without_pragma(project): + project.compiler.vyper.set_vyper_version("0.2.4") + project.compile_source( + """ +@external +def foo(): + pass""" + ) diff --git a/tests/project/test_project_structure.py b/tests/project/test_project_structure.py index c49ad6328..b12675ed8 100644 --- a/tests/project/test_project_structure.py +++ b/tests/project/test_project_structure.py @@ -36,7 +36,7 @@ def test_compiles(project, tmp_path): tmp_path.joinpath("sources").mkdir() with tmp_path.joinpath("sources/Foo.vy").open("w") as fp: fp.write( - """ + """# @version 0.2.4 @external def foo() -> int128: return 2 diff --git a/tests/project/test_sources.py b/tests/project/test_sources.py index 2947cb3a4..2aca231fd 100644 --- a/tests/project/test_sources.py +++ b/tests/project/test_sources.py @@ -71,3 +71,17 @@ def test_load_messy_project(): def test_get_pragma_spec(): assert sources.get_pragma_spec(MESSY_SOURCE) == NpmSpec(">=0.4.22 <0.7.0") + + +@pytest.mark.parametrize( + "version, spec", + [ + ("0.1.0b16", NpmSpec("0.1.0-beta.16")), + ("0.1.0Beta17", NpmSpec("0.1.0-beta.17")), + ("^0.2.0", NpmSpec("^0.2.0")), + ("<=0.2.4", NpmSpec("<=0.2.4")), + ], +) +def test_get_vyper_pragma_spec(version, spec): + source = f"""# @version {version}""" + assert sources.get_vyper_pragma_spec(source) == spec