@0x/sol-compiler
Version:
Solidity compiler wrapper and artifactor
520 lines • 24.7 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getJSFullSolcVersionAsync = exports.getDockerFullSolcVersionAsync = exports.normalizeSolcVersion = exports.getSolidityVersionFromSolcVersion = exports.getDependencyNameToPackagePath = exports.addHexPrefixToContractBytecode = exports.getSolcJSVersionFromPath = exports.getSolcJSFromPath = exports.getSolcJSAsync = exports.preFetchCSolcJSBinariesAsync = exports.getSourcesWithDependencies = exports.getSourceTreeHash = exports.printCompilationErrorsAndWarnings = exports.makeContractPathsRelative = exports.compileDockerAsync = exports.compileSolcJSAsync = exports.getSolcJSReleasesAsync = exports.parseDependencies = exports.getNormalizedErrMsg = exports.parseSolidityVersionRange = exports.createDirIfDoesNotExistAsync = exports.getContractArtifactIfExistsAsync = void 0;
const utils_1 = require("@0x/utils");
const chalk_1 = require("chalk");
const child_process_1 = require("child_process");
const ethUtil = require("ethereumjs-util");
const _ = require("lodash");
const path = require("path");
const requireFromString = require("require-from-string");
const solc = require("solc");
const stripComments = require("strip-comments");
const util_1 = require("util");
const constants_1 = require("./constants");
const fs_wrapper_1 = require("./fs_wrapper");
const types_1 = require("./types");
/**
* Gets contract data or returns if an artifact does not exist.
* @param artifactsDir Path to the artifacts directory.
* @param contractName Name of contract.
* @return Contract data or undefined.
*/
function getContractArtifactIfExistsAsync(artifactsDir, contractName) {
return __awaiter(this, void 0, void 0, function* () {
let contractArtifact;
const currentArtifactPath = `${artifactsDir}/${path.basename(contractName, constants_1.constants.SOLIDITY_FILE_EXTENSION)}.json`;
try {
const opts = {
encoding: 'utf8',
};
const contractArtifactString = yield fs_wrapper_1.fsWrapper.readFileAsync(currentArtifactPath, opts);
contractArtifact = JSON.parse(contractArtifactString);
return contractArtifact;
}
catch (err) {
return undefined;
}
});
}
exports.getContractArtifactIfExistsAsync = getContractArtifactIfExistsAsync;
/**
* Creates a directory if it does not already exist.
* @param artifactsDir Path to the directory.
*/
function createDirIfDoesNotExistAsync(dirPath) {
return __awaiter(this, void 0, void 0, function* () {
if (!fs_wrapper_1.fsWrapper.doesPathExistSync(dirPath)) {
utils_1.logUtils.warn(`Creating directory at ${dirPath}...`);
yield fs_wrapper_1.fsWrapper.mkdirpAsync(dirPath);
}
});
}
exports.createDirIfDoesNotExistAsync = createDirIfDoesNotExistAsync;
/**
* Searches Solidity source code for compiler version range.
* @param source Source code of contract.
* @return Solc compiler version range.
*/
function parseSolidityVersionRange(source) {
const SOLIDITY_VERSION_RANGE_REGEX = /pragma\s+solidity\s+(.*);/;
const solcVersionRangeMatch = source.match(SOLIDITY_VERSION_RANGE_REGEX);
if (solcVersionRangeMatch === null) {
throw new Error('Could not find Solidity version range in source');
}
const solcVersionRange = solcVersionRangeMatch[1];
return solcVersionRange;
}
exports.parseSolidityVersionRange = parseSolidityVersionRange;
/**
* Normalizes the path found in the error message. If it cannot be normalized
* the original error message is returned.
* Example: converts 'base/Token.sol:6:46: Warning: Unused local variable'
* to 'Token.sol:6:46: Warning: Unused local variable'
* This is used to prevent logging the same error multiple times.
* @param errMsg An error message from the compiled output.
* @return The error message with directories truncated from the contract path.
*/
function getNormalizedErrMsg(errMsg) {
const SOLIDITY_FILE_EXTENSION_REGEX = /(.*\.sol):/;
const errPathMatch = errMsg.match(SOLIDITY_FILE_EXTENSION_REGEX);
if (errPathMatch === null) {
// This can occur if solidity outputs a general warning, e.g
// Warning: This is a pre-release compiler version, please do not use it in production.
return errMsg;
}
const errPath = errPathMatch[0];
const baseContract = path.basename(errPath);
const normalizedErrMsg = errMsg.replace(errPath, baseContract);
return normalizedErrMsg;
}
exports.getNormalizedErrMsg = getNormalizedErrMsg;
/**
* Parses the contract source code and extracts the dependencies
* @param source Contract source code
* @return List of dependencies
*/
function parseDependencies(contractSource) {
// TODO: Use a proper parser
const source = contractSource.source;
const sourceWithoutComments = stripComments(source);
const IMPORT_REGEX = /(import\s)/;
const dependencies = [];
const lines = sourceWithoutComments.split('\n');
_.forEach(lines, line => {
var _a;
if (line.match(IMPORT_REGEX) !== null) {
const dependencyMatch = (_a = line.match(constants_1.constants.DEPENDENCY_PATH_REGEX_DOUBLE_QUOTES)) !== null && _a !== void 0 ? _a : line.match(constants_1.constants.DEPENDENCY_PATH_REGEX_SINGLE_QUOTES);
if (dependencyMatch !== null) {
let dependencyPath = dependencyMatch[1];
if (dependencyPath.startsWith('.')) {
dependencyPath = path.join(path.dirname(contractSource.path), dependencyPath);
}
dependencies.push(dependencyPath);
}
}
});
return dependencies;
}
exports.parseDependencies = parseDependencies;
let solcJSReleasesCache;
/**
* Fetches the list of available solidity compilers
* @param isOfflineMode Offline mode flag
*/
function getSolcJSReleasesAsync(isOfflineMode) {
return __awaiter(this, void 0, void 0, function* () {
if (isOfflineMode) {
return constants_1.constants.SOLC_BIN_PATHS;
}
if (solcJSReleasesCache === undefined) {
// See if we cached it on-disk first.
try {
const st = yield fs_wrapper_1.fsWrapper.statAsync(constants_1.constants.SOLCJS_RELEASES_PATH);
if (Date.now() - st.ctime.getTime() >= constants_1.constants.SOLCJS_RELEASES_CACHE_EXPIRY) {
// Remove the cached file and ignore it if it's too old.
yield fs_wrapper_1.fsWrapper.removeFileAsync(constants_1.constants.SOLCJS_RELEASES_PATH);
}
else {
// Use the cached file otherwise.
return (solcJSReleasesCache = JSON.parse((yield fs_wrapper_1.fsWrapper.readFileAsync(constants_1.constants.SOLCJS_RELEASES_PATH))));
}
}
catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
// Fetch from the WWW.
const versionList = yield fetch('https://binaries.soliditylang.org/bin/list.json');
const versionListJSON = yield versionList.json();
solcJSReleasesCache = versionListJSON.releases;
// Cache the result on disk.
yield fs_wrapper_1.fsWrapper.writeFileAsync(constants_1.constants.SOLCJS_RELEASES_PATH, JSON.stringify(solcJSReleasesCache, null, '\t'));
}
return solcJSReleasesCache;
});
}
exports.getSolcJSReleasesAsync = getSolcJSReleasesAsync;
/**
* Compiles the contracts and prints errors/warnings
* @param solcInstance Instance of a solc compiler
* @param standardInput Solidity standard JSON input
* @param isOfflineMode Offline mode flag
*/
function compileSolcJSAsync(solcInstance, standardInput) {
return __awaiter(this, void 0, void 0, function* () {
const standardInputStr = JSON.stringify(standardInput);
const standardOutputStr = solcInstance.compile(standardInputStr);
const compiled = JSON.parse(standardOutputStr);
return compiled;
});
}
exports.compileSolcJSAsync = compileSolcJSAsync;
/**
* Compiles the contracts and prints errors/warnings
* @param solidityVersion Solidity version
* @param standardInput Solidity standard JSON input
*/
function compileDockerAsync(solidityVersion, standardInput) {
return __awaiter(this, void 0, void 0, function* () {
const standardInputStr = JSON.stringify(standardInput, null, 2);
// prettier-ignore
const dockerArgs = [
'run',
'-i',
'-a', 'stdin',
'-a', 'stdout',
'-a', 'stderr',
`ethereum/solc:${solidityVersion}`,
'solc', '--standard-json',
];
return new Promise((accept, reject) => {
const p = child_process_1.spawn('docker', dockerArgs, { shell: true, stdio: ['pipe', 'pipe', 'inherit'] });
p.stdin.write(standardInputStr);
p.stdin.end();
let fullOutput = '';
p.stdout.on('data', (chunk) => {
fullOutput += chunk;
});
p.on('close', code => {
if (code !== 0) {
reject('Compilation failed');
}
accept(JSON.parse(fullOutput));
});
});
});
}
exports.compileDockerAsync = compileDockerAsync;
/**
* Example "relative" paths:
* /user/leo/0x-monorepo/contracts/extensions/contracts/extension.sol -> extension.sol
* /user/leo/0x-monorepo/node_modules/@0x/contracts-protocol/contracts/exchange.sol -> @0x/contracts-protocol/contracts/exchange.sol
*/
function makeContractPathRelative(absolutePath, contractsDir, dependencyNameToPath) {
let contractPath = absolutePath.replace(`${contractsDir}/`, '');
_.map(dependencyNameToPath, (packagePath, dependencyName) => {
contractPath = contractPath.replace(packagePath, dependencyName);
});
return contractPath;
}
/**
* Makes the path relative removing all system-dependent data. Converts absolute paths to a format suitable for artifacts.
* @param absolutePathToSmth Absolute path to contract or source
* @param contractsDir Current package contracts directory location
* @param dependencyNameToPath Mapping of dependency name to package path
*/
function makeContractPathsRelative(absolutePathToSmth, contractsDir, dependencyNameToPath) {
return _.mapKeys(absolutePathToSmth, (_val, absoluteContractPath) => makeContractPathRelative(absoluteContractPath, contractsDir, dependencyNameToPath));
}
exports.makeContractPathsRelative = makeContractPathsRelative;
/**
* Separates errors from warnings, formats the messages and prints them. Throws if there is any compilation error (not warning).
* @param solcErrors The errors field of standard JSON output that contains errors and warnings.
*/
function printCompilationErrorsAndWarnings(solcErrors) {
const SOLIDITY_WARNING = 'warning';
const errors = _.filter(solcErrors, entry => entry.severity !== SOLIDITY_WARNING);
const warnings = _.filter(solcErrors, entry => entry.severity === SOLIDITY_WARNING);
if (!_.isEmpty(errors)) {
errors.forEach(error => {
const normalizedErrMsg = getNormalizedErrMsg(error.formattedMessage || error.message);
utils_1.logUtils.log(chalk_1.default.red('error'), normalizedErrMsg);
});
throw new types_1.CompilationError(errors.length);
}
else {
warnings.forEach(warning => {
const normalizedWarningMsg = getNormalizedErrMsg(warning.formattedMessage || warning.message);
utils_1.logUtils.log(chalk_1.default.yellow('warning'), normalizedWarningMsg);
});
}
}
exports.printCompilationErrorsAndWarnings = printCompilationErrorsAndWarnings;
/**
* Gets the source tree hash for a file and its dependencies.
* @param fileName Name of contract file.
*/
function getSourceTreeHash(resolver, importPath) {
const contractSource = resolver.resolve(importPath);
const dependencies = parseDependencies(contractSource);
const sourceHash = ethUtil.keccak256(Buffer.from(contractSource.source));
if (dependencies.length === 0) {
return sourceHash;
}
else {
const dependencySourceTreeHashes = _.map(dependencies, (dependency) => {
try {
return getSourceTreeHash(resolver, dependency);
}
catch (e) {
if (/Error when trying to resolve dependencies for/.test(e.message)) {
throw e;
}
else {
throw Error(`Error when trying to resolve dependencies for ${importPath}: ${e.message}`);
}
}
});
const sourceTreeHashesBuffer = Buffer.concat([sourceHash, ...dependencySourceTreeHashes]);
const sourceTreeHash = ethUtil.keccak256(sourceTreeHashesBuffer);
return sourceTreeHash;
}
}
exports.getSourceTreeHash = getSourceTreeHash;
/**
* Recursively parses imports from sources starting from `contractPath`.
* @return Sources required by imports.
*/
function getSourcesWithDependencies(contractPath, sourcesByAbsolutePath, importRemappings) {
const compiledImports = { [`./${path.basename(contractPath)}`]: sourcesByAbsolutePath[contractPath] };
recursivelyGatherDependencySources(contractPath, path.dirname(contractPath), sourcesByAbsolutePath, importRemappings, compiledImports);
return compiledImports;
}
exports.getSourcesWithDependencies = getSourcesWithDependencies;
function recursivelyGatherDependencySources(contractPath, rootDir, sourcesByAbsolutePath, importRemappings, compiledImports, visitedAbsolutePaths = {}, importRootDir) {
var _a;
if (visitedAbsolutePaths[contractPath]) {
return;
}
else {
visitedAbsolutePaths[contractPath] = true;
}
const contractSource = sourcesByAbsolutePath[contractPath].content;
const contractSourceWithoutComments = stripComments(contractSource);
const importStatementMatches = contractSourceWithoutComments.match(/\nimport[^;]*;/g);
if (importStatementMatches === null) {
return;
}
const lastPathSeparatorPos = contractPath.lastIndexOf('/');
const contractFolder = lastPathSeparatorPos === -1 ? '' : contractPath.slice(0, lastPathSeparatorPos + 1);
for (const importStatementMatch of importStatementMatches) {
const importPathMatches = (_a = importStatementMatch.match(constants_1.constants.DEPENDENCY_PATH_REGEX_DOUBLE_QUOTES)) !== null && _a !== void 0 ? _a : importStatementMatch.match(constants_1.constants.DEPENDENCY_PATH_REGEX_SINGLE_QUOTES);
if (importPathMatches === null || importPathMatches.length === 0) {
continue;
}
let importPath = importPathMatches[1];
let absPath = importPath;
let _importRootDir = importRootDir;
if (importPath.startsWith('.')) {
absPath = path.join(contractFolder, importPath);
if (_importRootDir) {
// If there's an `_importRootDir`, we're in a package, so express
// the import path as within the package.
importPath = path.join(_importRootDir, importPath);
}
else {
// Express relative imports paths as paths from the root directory.
importPath = path.relative(rootDir, absPath);
if (!importPath.startsWith('.')) {
importPath = `./${importPath}`;
}
}
}
else {
for (const [prefix, replacement] of Object.entries(importRemappings)) {
if (importPath.startsWith(prefix)) {
absPath = `${replacement}${importPath.substr(prefix.length)}`;
_importRootDir = path.dirname(importPath);
break;
}
}
}
compiledImports[importPath] = sourcesByAbsolutePath[absPath];
recursivelyGatherDependencySources(absPath, rootDir, sourcesByAbsolutePath, importRemappings, compiledImports, visitedAbsolutePaths, _importRootDir);
}
}
const solcJSCache = {};
let solcJSReleases;
/**
* Calls `getSolcJSAsync()` for every solc version passed in.
* @param versions Arrays of solc versions.
*/
function preFetchCSolcJSBinariesAsync(solcVersions) {
return __awaiter(this, void 0, void 0, function* () {
const compilerVersions = solcVersions.map(solcVersion => getSolidityVersionFromSolcVersion(solcVersion));
utils_1.logUtils.log(`Pre-fetching solidity versions: ${compilerVersions.join(', ')}...`);
yield Promise.all(compilerVersions.map((v) => __awaiter(this, void 0, void 0, function* () { return getSolcJSAsync(v, false); })));
});
}
exports.preFetchCSolcJSBinariesAsync = preFetchCSolcJSBinariesAsync;
/**
* Gets the solidity compiler instance. If the compiler is already cached - gets it from FS,
* otherwise - fetches it and caches it.
* @param solidityVersion The solidity version. e.g. 0.5.0
* @param isOfflineMode Offline mode flag
*/
function getSolcJSAsync(solidityVersion, isOfflineMode) {
return __awaiter(this, void 0, void 0, function* () {
if (!solcJSReleases) {
solcJSReleases = yield getSolcJSReleasesAsync(isOfflineMode);
}
const fullSolcVersion = solcJSReleases[solidityVersion];
if (fullSolcVersion === undefined) {
throw new Error(`${solidityVersion} is not a known compiler version`);
}
if (solcJSCache[fullSolcVersion]) {
return solcJSCache[fullSolcVersion];
}
const compilerBinFilename = path.join(constants_1.constants.SOLC_BIN_DIR, fullSolcVersion);
let solcjs;
if (yield fs_wrapper_1.fsWrapper.doesFileExistAsync(compilerBinFilename)) {
solcjs = (yield fs_wrapper_1.fsWrapper.readFileAsync(compilerBinFilename)).toString();
}
else {
utils_1.logUtils.warn(`Downloading ${fullSolcVersion}...`);
const url = `${constants_1.constants.BASE_COMPILER_URL}${fullSolcVersion}`;
const response = yield utils_1.fetchAsync(url);
const SUCCESS_STATUS = 200;
if (response.status !== SUCCESS_STATUS) {
throw new Error(`Failed to load ${fullSolcVersion}`);
}
solcjs = yield response.text();
yield fs_wrapper_1.fsWrapper.writeFileAsync(compilerBinFilename, solcjs);
}
if (solcjs.length === 0) {
throw new Error('No compiler available');
}
const solcInstance = solc.setupMethods(requireFromString(solcjs, compilerBinFilename));
return (solcJSCache[fullSolcVersion] = solcInstance);
});
}
exports.getSolcJSAsync = getSolcJSAsync;
/**
* Gets the solidity compiler instance from a module path.
* @param path The path to the solc module.
*/
function getSolcJSFromPath(modulePath) {
return require(modulePath);
}
exports.getSolcJSFromPath = getSolcJSFromPath;
/**
* Gets the solidity compiler version from a module path.
* @param path The path to the solc module.
*/
function getSolcJSVersionFromPath(modulePath) {
return normalizeSolcVersion(require(modulePath).version());
}
exports.getSolcJSVersionFromPath = getSolcJSVersionFromPath;
/**
* Solidity compiler emits the bytecode without a 0x prefix for a hex. This function fixes it if bytecode is present.
* @param compiledContract The standard JSON output section for a contract. Geth modified in place.
*/
function addHexPrefixToContractBytecode(compiledContract) {
if (compiledContract.evm !== undefined) {
if (compiledContract.evm.bytecode !== undefined && compiledContract.evm.bytecode.object !== undefined) {
compiledContract.evm.bytecode.object = ethUtil.addHexPrefix(compiledContract.evm.bytecode.object);
}
if (compiledContract.evm.deployedBytecode !== undefined &&
compiledContract.evm.deployedBytecode.object !== undefined) {
compiledContract.evm.deployedBytecode.object = ethUtil.addHexPrefix(compiledContract.evm.deployedBytecode.object);
}
}
}
exports.addHexPrefixToContractBytecode = addHexPrefixToContractBytecode;
/**
* Takes the list of resolved contract sources from `SpyResolver` and produces a mapping from dependency name
* to package path used in `remappings` later, as well as in generating the "relative" source paths saved to the artifact files.
* @param contractSources The list of resolved contract sources
*/
function getDependencyNameToPackagePath(contractSources) {
const allTouchedFiles = contractSources.map(contractSource => `${contractSource.absolutePath}`);
const NODE_MODULES = 'node_modules';
const allTouchedDependencies = _.filter(allTouchedFiles, filePath => filePath.includes(NODE_MODULES));
const dependencyNameToPath = {};
_.map(allTouchedDependencies, dependencyFilePath => {
const lastNodeModulesStart = dependencyFilePath.lastIndexOf(NODE_MODULES);
const lastNodeModulesEnd = lastNodeModulesStart + NODE_MODULES.length;
const importPath = dependencyFilePath.substr(lastNodeModulesEnd + 1);
let packageName;
let packageScopeIfExists;
let dependencyName;
if (_.startsWith(importPath, '@')) {
[packageScopeIfExists, packageName] = importPath.split('/');
dependencyName = `${packageScopeIfExists}/${packageName}`;
}
else {
[packageName] = importPath.split('/');
dependencyName = `${packageName}`;
}
const dependencyPackagePath = path.join(dependencyFilePath.substr(0, lastNodeModulesEnd), dependencyName);
dependencyNameToPath[dependencyName] = dependencyPackagePath;
});
return dependencyNameToPath;
}
exports.getDependencyNameToPackagePath = getDependencyNameToPackagePath;
/**
* Extract the solidity version (e.g., '0.5.9') from a solc version (e.g., `0.5.9+commit.34d3134f`).
*/
function getSolidityVersionFromSolcVersion(solcVersion) {
const m = /(\d+\.\d+\.\d+)\+commit\.[a-fA-F0-9]{8}/.exec(solcVersion);
if (!m) {
throw new Error(`Unable to parse solc version string "${solcVersion}"`);
}
return m[1];
}
exports.getSolidityVersionFromSolcVersion = getSolidityVersionFromSolcVersion;
/**
* Strips any extra characters before and after the version + commit hash of a solc version string.
*/
function normalizeSolcVersion(fullSolcVersion) {
const m = /\d+\.\d+\.\d+\+commit\.[a-fA-F0-9]{8}/.exec(fullSolcVersion);
if (!m) {
throw new Error(`Unable to parse solc version string "${fullSolcVersion}"`);
}
return m[0];
}
exports.normalizeSolcVersion = normalizeSolcVersion;
/**
* Gets the full version string of a dockerized solc.
*/
function getDockerFullSolcVersionAsync(solidityVersion) {
return __awaiter(this, void 0, void 0, function* () {
const dockerCommand = `docker run ethereum/solc:${solidityVersion} --version`;
const versionCommandOutput = (yield util_1.promisify(child_process_1.exec)(dockerCommand)).stdout.toString();
const versionCommandOutputParts = versionCommandOutput.split(' ');
return normalizeSolcVersion(versionCommandOutputParts[versionCommandOutputParts.length - 1].trim());
});
}
exports.getDockerFullSolcVersionAsync = getDockerFullSolcVersionAsync;
/**
* Gets the full version string of a JS module solc.
*/
function getJSFullSolcVersionAsync(solidityVersion, isOfflineMode = false) {
return __awaiter(this, void 0, void 0, function* () {
return normalizeSolcVersion((yield getSolcJSAsync(solidityVersion, isOfflineMode)).version());
});
}
exports.getJSFullSolcVersionAsync = getJSFullSolcVersionAsync;
// tslint:disable-next-line: max-file-line-count
//# sourceMappingURL=compiler.js.map