From 4b3e0383235ca4ca0127f24c2e05543bb45a56bb Mon Sep 17 00:00:00 2001 From: Amir Bandeali Date: Wed, 29 Nov 2017 22:02:43 -0800 Subject: Add contracts to packages, fix most linting errors --- packages/contracts/deploy/src/compiler.ts | 248 ++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 packages/contracts/deploy/src/compiler.ts (limited to 'packages/contracts/deploy/src/compiler.ts') diff --git a/packages/contracts/deploy/src/compiler.ts b/packages/contracts/deploy/src/compiler.ts new file mode 100644 index 000000000..5909bdda1 --- /dev/null +++ b/packages/contracts/deploy/src/compiler.ts @@ -0,0 +1,248 @@ +import promisify = require('es6-promisify'); +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; +import * as path from 'path'; +import solc = require('solc'); +import * as Web3 from 'web3'; + +import {binPaths} from './../solc/bin_paths'; +import {fsWrapper} from './utils/fs_wrapper'; +import { + CompilerOptions, + ContractArtifact, + ContractData, + ContractNetworks, + ContractSources, + ImportContents, + SolcErrors, +} from './utils/types'; +import {utils} from './utils/utils'; + +const SOLIDITY_FILE_EXTENSION = '.sol'; + +/** + * Recursively retrieves Solidity source code from directory. + * @param dirPath Directory to search. + * @return Mapping of contract name to contract source. + */ +async function getContractSourcesAsync(dirPath: string): Promise { + let dirContents: string[] = []; + try { + dirContents = await fsWrapper.readdirAsync(dirPath); + } catch (err) { + throw new Error(`No directory found at ${dirPath}`); + } + let sources: ContractSources = {}; + for (const name of dirContents) { + const contentPath = `${dirPath}/${name}`; + if (path.extname(name) === SOLIDITY_FILE_EXTENSION) { + try { + const opts = { + encoding: 'utf8', + }; + sources[name] = await fsWrapper.readFileAsync(contentPath, opts); + utils.consoleLog(`Reading ${name} source...`); + } catch (err) { + utils.consoleLog(`Could not find file at ${contentPath}`); + } + } else { + try { + const nestedSources = await getContractSourcesAsync(contentPath); + sources = { + ...sources, + ...nestedSources, + }; + } catch (err) { + utils.consoleLog(`${contentPath} is not a directory or ${SOLIDITY_FILE_EXTENSION} file`); + } + } + } + return sources; +} +/** + * Searches Solidity source code for compiler version. + * @param source Source code of contract. + * @return Solc compiler version. + */ +function parseSolidityVersion(source: string): string { + const solcVersionMatch = source.match(/(?:solidity\s\^?)([0-9]{1,2}[.][0-9]{1,2}[.][0-9]{1,2})/); + if (_.isNull(solcVersionMatch)) { + throw new Error('Could not find Solidity version in source'); + } + const solcVersion = solcVersionMatch[1]; + return solcVersion; +} +/** + * Normalizes the path found in the error message. + * 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: string): string { + const errPathMatch = errMsg.match(/(.*\.sol)/); + if (_.isNull(errPathMatch)) { + throw new Error('Could not find a path in error message'); + } + const errPath = errPathMatch[0]; + const baseContract = path.basename(errPath); + const normalizedErrMsg = errMsg.replace(errPath, baseContract); + return normalizedErrMsg; +} + +export class Compiler { + private contractsDir: string; + private networkId: number; + private optimizerEnabled: number; + private artifactsDir: string; + private contractSourcesIfExists?: ContractSources; + private solcErrors: Set; + + constructor(opts: CompilerOptions) { + this.contractsDir = opts.contractsDir; + this.networkId = opts.networkId; + this.optimizerEnabled = opts.optimizerEnabled; + this.artifactsDir = opts.artifactsDir; + this.solcErrors = new Set(); + } + /** + * Compiles all Solidity files found in contractsDir and writes JSON artifacts to artifactsDir. + */ + public async compileAllAsync(): Promise { + await this.createArtifactsDirIfDoesNotExistAsync(); + this.contractSourcesIfExists = await getContractSourcesAsync(this.contractsDir); + + const contractBaseNames = _.keys(this.contractSourcesIfExists); + const compiledContractPromises = _.map(contractBaseNames, async (contractBaseName: string): Promise => { + return this.compileContractAsync(contractBaseName); + }); + await Promise.all(compiledContractPromises); + + this.solcErrors.forEach(errMsg => { + utils.consoleLog(errMsg); + }); + } + /** + * Compiles contract and saves artifact to artifactsDir. + * @param contractBaseName Name of contract with '.sol' extension. + */ + private async compileContractAsync(contractBaseName: string): Promise { + if (_.isUndefined(this.contractSourcesIfExists)) { + throw new Error('Contract sources not yet initialized'); + } + + const source = this.contractSourcesIfExists[contractBaseName]; + const contractName = path.basename(contractBaseName, SOLIDITY_FILE_EXTENSION); + const currentArtifactPath = `${this.artifactsDir}/${contractName}.json`; + const sourceHash = `0x${ethUtil.sha3(source).toString('hex')}`; + + let currentArtifactString: string; + let currentArtifact: ContractArtifact; + let oldNetworks: ContractNetworks; + let shouldCompile: boolean; + try { + const opts = { + encoding: 'utf8', + }; + currentArtifactString = await fsWrapper.readFileAsync(currentArtifactPath, opts); + currentArtifact = JSON.parse(currentArtifactString); + oldNetworks = currentArtifact.networks; + const oldNetwork: ContractData = oldNetworks[this.networkId]; + shouldCompile = _.isUndefined(oldNetwork) || + oldNetwork.keccak256 !== sourceHash || + oldNetwork.optimizer_enabled !== this.optimizerEnabled; + } catch (err) { + shouldCompile = true; + } + + if (!shouldCompile) { + return; + } + + const input = { + [contractBaseName]: source, + }; + const solcVersion = parseSolidityVersion(source); + const fullSolcVersion = binPaths[solcVersion]; + const solcBinPath = `./../solc/solc_bin/${fullSolcVersion}`; + const solcBin = require(solcBinPath); + const solcInstance = solc.setupMethods(solcBin); + + utils.consoleLog(`Compiling ${contractBaseName}...`); + const sourcesToCompile = { + sources: input, + }; + const compiled = solcInstance.compile(sourcesToCompile, + this.optimizerEnabled, + this.findImportsIfSourcesExist.bind(this)); + + if (!_.isUndefined(compiled.errors)) { + _.each(compiled.errors, errMsg => { + const normalizedErrMsg = getNormalizedErrMsg(errMsg); + this.solcErrors.add(normalizedErrMsg); + }); + } + + const contractIdentifier = `${contractBaseName}:${contractName}`; + const abi: Web3.ContractAbi = JSON.parse(compiled.contracts[contractIdentifier].interface); + const unlinked_binary = `0x${compiled.contracts[contractIdentifier].bytecode}`; + const updated_at = Date.now(); + const contractData: ContractData = { + solc_version: solcVersion, + keccak256: sourceHash, + optimizer_enabled: this.optimizerEnabled, + abi, + unlinked_binary, + updated_at, + }; + + let newArtifact: ContractArtifact; + if (!_.isUndefined(currentArtifactString)) { + newArtifact = { + ...currentArtifact, + networks: { + ...oldNetworks, + [this.networkId]: contractData, + }, + }; + } else { + newArtifact = { + contract_name: contractName, + networks: { + [this.networkId]: contractData, + }, + }; + } + + const artifactString = utils.stringifyWithFormatting(newArtifact); + await fsWrapper.writeFileAsync(currentArtifactPath, artifactString); + utils.consoleLog(`${contractBaseName} artifact saved!`); + } + /** + * Callback to resolve dependencies with `solc.compile`. + * Throws error if contractSources not yet initialized. + * @param importPath Path to an imported dependency. + * @return Import contents object containing source code of dependency. + */ + private findImportsIfSourcesExist(importPath: string): ImportContents { + if (_.isUndefined(this.contractSourcesIfExists)) { + throw new Error('Contract sources not yet initialized'); + } + const contractBaseName = path.basename(importPath); + const source = this.contractSourcesIfExists[contractBaseName]; + const importContents: ImportContents = { + contents: source, + }; + return importContents; + } + /** + * Creates the artifacts directory if it does not already exist. + */ + private async createArtifactsDirIfDoesNotExistAsync(): Promise { + if (!fsWrapper.doesPathExistSync(this.artifactsDir)) { + utils.consoleLog('Creating artifacts directory...'); + await fsWrapper.mkdirAsync(this.artifactsDir); + } + } +} -- cgit v1.2.3