Source code for crytic_compile.platform.truffle

"""
Truffle platform
"""
import glob
import json
import logging
import os
import platform
import re
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Tuple, List, Dict

from crytic_compile.platform.types import Type
from crytic_compile.platform.exceptions import InvalidCompilation
from crytic_compile.utils.naming import convert_filename
from crytic_compile.compiler.compiler import CompilerVersion
from crytic_compile.platform import solc
from crytic_compile.utils.natspec import Natspec
from .abstract_platform import AbstractPlatform

# Handle cycle
if TYPE_CHECKING:
    from crytic_compile import CryticCompile

LOGGER = logging.getLogger("CryticCompile")


[docs]def export_to_truffle(crytic_compile: "CryticCompile", **kwargs: str) -> Optional[str]: """ Export to the truffle format :param crytic_compile: :param kwargs: :return: """ # Get our export directory, if it's set, we create the path. export_dir = kwargs.get("export_dir", "crytic-export") if export_dir and not os.path.exists(export_dir): os.makedirs(export_dir) # Loop for each contract filename. results: List[Dict] = [] for contract_name in crytic_compile.contracts_names: # Create the informational object to output for this contract filename = crytic_compile.contracts_filenames[contract_name] output = { "contractName": contract_name, "abi": crytic_compile.abi(contract_name), "bytecode": "0x" + crytic_compile.bytecode_init(contract_name), "deployedBytecode": "0x" + crytic_compile.bytecode_runtime(contract_name), "ast": crytic_compile.ast(filename.absolute), "userdoc": crytic_compile.natspec[contract_name].userdoc.export(), "devdoc": crytic_compile.natspec[contract_name].devdoc.export(), } results.append(output) # If we have an export directory, export it. path = os.path.join(export_dir, contract_name + ".json") with open(path, "w", encoding="utf8") as file_desc: json.dump(output, file_desc) return export_dir
[docs]class Truffle(AbstractPlatform): """ Truffle platform """ NAME = "Truffle" PROJECT_URL = "https://github.com/trufflesuite/truffle" TYPE = Type.TRUFFLE
[docs] def compile(self, crytic_compile: "CryticCompile", **kwargs: str): """ Compile the target :param kwargs: :return: """ build_directory = kwargs.get("truffle_build_directory", os.path.join("build", "contracts")) truffle_ignore_compile = kwargs.get("truffle_ignore_compile", False) or kwargs.get( "ignore_compile", False ) truffle_version = kwargs.get("truffle_version", None) # crytic_compile.type = Type.TRUFFLE # Truffle on windows has naming conflicts where it will invoke truffle.js directly instead # of truffle.cmd (unless in powershell or git bash). # The cleanest solution is to explicitly call # truffle.cmd. Reference: # https://truffleframework.com/docs/truffle/reference/configuration#resolving-naming-conflicts-on-windows if platform.system() == "Windows": base_cmd = ["truffle.cmd"] elif kwargs.get("npx_disable", False): base_cmd = ["truffle"] else: base_cmd = ["npx", "truffle"] if truffle_version: if truffle_version.startswith("truffle"): base_cmd = ["npx", truffle_version] else: base_cmd = ["npx", f"truffle@{truffle_version}"] elif os.path.isfile(os.path.join(self._target, "package.json")): with open(os.path.join(self._target, "package.json"), encoding="utf8") as file_desc: package = json.load(file_desc) if "devDependencies" in package: if "truffle" in package["devDependencies"]: version = package["devDependencies"]["truffle"] if version.startswith("^"): version = version[1:] truffle_version = "truffle@{}".format(version) base_cmd = ["npx", truffle_version] if "dependencies" in package: if "truffle" in package["dependencies"]: version = package["dependencies"]["truffle"] if version.startswith("^"): version = version[1:] truffle_version = "truffle@{}".format(version) base_cmd = ["npx", truffle_version] if not truffle_ignore_compile: cmd = base_cmd + ["compile", "--all"] LOGGER.info( "'%s' running (use --truffle-version truffle@x.x.x to use specific version)", " ".join(cmd), ) process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self._target ) stdout_bytes, stderr_bytes = process.communicate() stdout, stderr = ( stdout_bytes.decode(), stderr_bytes.decode(), ) # convert bytestrings to unicode strings LOGGER.info(stdout) if stderr: LOGGER.error(stderr) if not os.path.isdir(os.path.join(self._target, build_directory)): if os.path.isdir(os.path.join(self._target, "node_modules")): raise InvalidCompilation( f"External dependencies {self._target} not found, please install them. (npm install)" ) raise InvalidCompilation("`truffle compile` failed. Can you run it?") filenames = glob.glob(os.path.join(self._target, build_directory, "*.json")) optimized = None version = None compiler = None for filename_txt in filenames: with open(filename_txt, encoding="utf8") as file_desc: target_loaded = json.load(file_desc) if optimized is None: if "metadata" in target_loaded: metadata = target_loaded["metadata"] try: metadata = json.loads(metadata) if "settings" in metadata: if "optimizer" in metadata["settings"]: if "enabled" in metadata["settings"]["optimizer"]: optimized = metadata["settings"]["optimizer"]["enabled"] except json.decoder.JSONDecodeError: pass userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) if not "ast" in target_loaded: continue filename = target_loaded["ast"]["absolutePath"] filename = convert_filename( filename, _relative_to_short, crytic_compile, working_dir=self._target ) crytic_compile.asts[filename.absolute] = target_loaded["ast"] crytic_compile.filenames.add(filename) contract_name = target_loaded["contractName"] crytic_compile.natspec[contract_name] = natspec crytic_compile.contracts_filenames[contract_name] = filename crytic_compile.contracts_names.add(contract_name) crytic_compile.abis[contract_name] = target_loaded["abi"] crytic_compile.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( "0x", "" ) crytic_compile.bytecodes_runtime[contract_name] = target_loaded[ "deployedBytecode" ].replace("0x", "") crytic_compile.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") crytic_compile.srcmaps_runtime[contract_name] = target_loaded[ "deployedSourceMap" ].split(";") if compiler is None: compiler = target_loaded.get("compiler", {}).get("name", None) if version is None: version = target_loaded.get("compiler", {}).get("version", None) if "+" in version: version = version[0 : version.find("+")] if version is None or compiler is None: version_from_config = _get_version_from_config(self._target) if version_from_config: version, compiler = version_from_config else: version, compiler = _get_version(base_cmd, cwd=self._target) crytic_compile.compiler_version = CompilerVersion( compiler=compiler, version=version, optimized=optimized )
[docs] @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """ Check if the target is a truffle project :param target: :return: """ truffle_ignore = kwargs.get("truffle_ignore", False) if truffle_ignore: return False return os.path.isfile(os.path.join(target, "truffle.js")) or os.path.isfile( os.path.join(target, "truffle-config.js") )
[docs] def is_dependency(self, path: str) -> bool: """ Check if the target is a dependency :param path: :return: """ return "node_modules" in Path(path).parts
def _guessed_tests(self) -> List[str]: """ Guess the potential unit tests commands :return: """ return ["truffle test"]
def _get_version_from_config(target: str) -> Optional[Tuple[str, str]]: """ Naive check on the truffleconfig file to get the version :param target: :return: (version, compiler) | None """ config = Path(target, "truffle-config.js") if not config.exists(): config = Path(target, "truffle.js") if not config.exists(): return None with open(config) as config_f: config_buffer = config_f.read() # The config is a javascript file # Use a naive regex to match the solc version match = re.search(r'solc: {[ ]*\n[ ]*version: "([0-9\.]*)', config_buffer) if match: if match.groups(): version = match.groups()[0] return version, "solc-js" return None def _get_version(truffle_call, cwd): cmd = truffle_call + ["version"] try: process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) except OSError as error: raise InvalidCompilation(f"Truffle failed: {error}") stdout, _ = process.communicate() stdout = stdout.decode() # convert bytestrings to unicode strings if not stdout: raise InvalidCompilation(f"Truffle failed to run: 'truffle version'") stdout = stdout.split("\n") for line in stdout: if "Solidity" in line: if "native" in line: return solc.get_version("solc"), "solc-native" version = re.findall(r"\d+\.\d+\.\d+", line)[0] compiler = re.findall(r"(solc[a-z\-]*)", line) if len(compiler) > 0: return version, compiler raise InvalidCompilation(f"Solidity version not found {stdout}") def _relative_to_short(relative): short = relative try: short = short.relative_to(Path("contracts")) except ValueError: try: short = short.relative_to("node_modules") except ValueError: pass return short