"""
Waffle platform
"""
import json
import logging
import os
import re
import subprocess
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List
from crytic_compile.compiler.compiler import CompilerVersion
from crytic_compile.platform.abstract_platform import AbstractPlatform
from crytic_compile.platform.exceptions import InvalidCompilation
from crytic_compile.platform.types import Type
from crytic_compile.utils.naming import convert_filename
# Handle cycle
from crytic_compile.utils.natspec import Natspec
if TYPE_CHECKING:
from crytic_compile import CryticCompile
LOGGER = logging.getLogger("CryticCompile")
[docs]class Waffle(AbstractPlatform):
"""
Waffle platform
"""
NAME = "Waffle"
PROJECT_URL = "https://github.com/EthWorks/Waffle"
TYPE = Type.WAFFLE
[docs] def compile(self, crytic_compile: "CryticCompile", **kwargs: str):
"""
Compile the target
:param crytic_compile:
:param target:
:param kwargs:
:return:
"""
waffle_ignore_compile = kwargs.get("waffle_ignore_compile", False) or kwargs.get(
"ignore_compile", False
)
target = self._target
cmd = ["waffle"]
if not kwargs.get("npx_disable", False):
cmd = ["npx"] + cmd
# Default behaviour (without any config_file)
build_directory = os.path.join("build")
compiler = "native"
version = _get_version(compiler, target)
config: Dict = dict()
config_file = kwargs.get("waffle_config_file", None)
# Read config file
if config_file:
config = _load_config(config_file)
version = _get_version(compiler, target, config=config)
if "targetPath" in config:
build_directory = config["targetPath"]
if "compiler" in config:
compiler = config["compiler"]
if "outputType" not in config or config["outputType"] != "all":
config["outputType"] = "all"
needed_config = {
"compilerOptions": {
"outputSelection": {
"*": {
"*": [
"evm.bytecode.object",
"evm.deployedBytecode.object",
"abi",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.sourceMap",
],
"": ["ast"],
}
}
}
}
# Set the config as it should be
if "compilerOptions" in config:
curr_config: Dict = config["compilerOptions"]
curr_needed_config: Dict = needed_config["compilerOptions"]
if "outputSelection" in curr_config:
curr_config = curr_config["outputSelection"]
curr_needed_config = curr_needed_config["outputSelection"]
if "*" in curr_config:
curr_config = curr_config["*"]
curr_needed_config = curr_needed_config["*"]
if "*" in curr_config:
curr_config["*"] += curr_needed_config["*"]
else:
curr_config["*"] = curr_needed_config["*"]
if "" in curr_config:
curr_config[""] += curr_needed_config[""]
else:
curr_config[""] = curr_needed_config[""]
else:
curr_config["*"] = curr_needed_config["*"]
else:
curr_config["outputSelection"] = curr_needed_config["outputSelection"]
else:
config["compilerOptions"] = needed_config["compilerOptions"]
if not waffle_ignore_compile:
with tempfile.NamedTemporaryFile(mode="w", suffix=".json") as file_desc:
json.dump(config, file_desc)
file_desc.flush()
cmd += [os.path.relpath(file_desc.name)]
LOGGER.info("Temporary file created: %s", file_desc.name)
LOGGER.info("'%s running", " ".join(cmd))
try:
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=target
)
except OSError as error:
raise InvalidCompilation(error)
stdout, stderr = process.communicate()
if stdout:
LOGGER.info(stdout.decode())
if stderr:
LOGGER.error(stderr.decode())
if not os.path.isdir(os.path.join(target, build_directory)):
raise InvalidCompilation("`waffle` compilation failed: build directory not found")
combined_path = os.path.join(target, build_directory, "Combined-Json.json")
if not os.path.exists(combined_path):
raise InvalidCompilation("`Combined-Json.json` not found")
with open(combined_path, "r") as file_desc:
target_all = json.load(file_desc)
optimized = None
for contract in target_all["contracts"]:
target_loaded = target_all["contracts"][contract]
contract = contract.split(":")
filename_rel = os.path.join(target, contract[0])
filename = convert_filename(
filename_rel, _relative_to_short, crytic_compile, working_dir=target
)
contract_name = contract[1]
crytic_compile.asts[filename.absolute] = target_all["sources"][contract[0]]["AST"]
crytic_compile.filenames.add(filename)
crytic_compile.contracts_filenames[contract_name] = filename
crytic_compile.contracts_names.add(contract_name)
crytic_compile.abis[contract_name] = target_loaded["abi"]
userdoc = target_loaded.get("userdoc", {})
devdoc = target_loaded.get("devdoc", {})
natspec = Natspec(userdoc, devdoc)
crytic_compile.natspec[contract_name] = natspec
crytic_compile.bytecodes_init[contract_name] = target_loaded["evm"]["bytecode"][
"object"
]
crytic_compile.srcmaps_init[contract_name] = target_loaded["evm"]["bytecode"][
"sourceMap"
].split(";")
crytic_compile.bytecodes_runtime[contract_name] = target_loaded["evm"][
"deployedBytecode"
]["object"]
crytic_compile.srcmaps_runtime[contract_name] = target_loaded["evm"][
"deployedBytecode"
]["sourceMap"].split(";")
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 waffle project
:param target:
:return:
"""
waffle_ignore = kwargs.get("waffle_ignore", False)
if waffle_ignore:
return False
if os.path.isfile(os.path.join(target, "package.json")):
with open("package.json", encoding="utf8") as file_desc:
package = json.load(file_desc)
if "dependencies" in package:
return "ethereum-waffle" in package["dependencies"]
return False
[docs] def is_dependency(self, path: str) -> bool:
"""
Check if the path 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 ["npx mocha"]
def _load_config(config_file: str) -> Dict:
"""
Load the config file
:param config_file:
:return:
"""
with open(config_file, "r") as file_desc:
content = file_desc.read()
if "module.exports" in content:
raise InvalidCompilation("module.export to supported for waffle")
return json.loads(content)
def _get_version(compiler: str, cwd: str, config=None) -> str:
version = ""
if config is not None and "solcVersion" in config:
version = re.findall(r"\d+\.\d+\.\d+", config["solcVersion"])[0]
elif compiler == "dockerized-solc":
version = config["docker-tag"]
elif compiler == "native":
cmd = ["solc", "--version"]
try:
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
except OSError as error:
raise InvalidCompilation(error)
stdout_bytes, _ = process.communicate()
stdout_txt = stdout_bytes.decode() # convert bytestrings to unicode strings
stdout = stdout_txt.split("\n")
for line in stdout:
if "Version" in line:
version = re.findall(r"\d+\.\d+\.\d+", line)[0]
elif compiler == "solc-js":
cmd = ["solcjs", "--version"]
try:
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
except OSError as error:
raise InvalidCompilation(error)
stdout_bytes, _ = process.communicate()
stdout_txt = stdout_bytes.decode() # convert bytestrings to unicode strings
version = re.findall(r"\d+\.\d+\.\d+", stdout_txt)[0]
else:
raise InvalidCompilation(f"Solidity version not found {compiler}")
return version
def _relative_to_short(relative: Path) -> Path:
short = relative
try:
short = short.relative_to(Path("contracts"))
except ValueError:
try:
short = short.relative_to("node_modules")
except ValueError:
pass
return short