Home Reference Source

lib/wfc.js

/*
   This is largely a cut and paste of truffle-workflow-compile

   We have modified it though to save additional information per contract
   to assist MythX analysis.

   In particular we add:
     sourceList[]  - a list of the sources that can be used in a sourceMap.
     sources       - a dict whose key is an entry of sourceList and whose value contains
         source: string
         ast: ast
         legacyAst: ast
*/
const mkdirp = require('mkdirp');
const path = require('path');
const fs = require('fs-extra');
const { callbackify, promisify } = require('util');
const Config = require('truffle-config');
const solcCompile = require('../compat/truffle-compile');
const vyperCompile = require('truffle-compile-vyper');
const externalCompile = require('truffle-external-compile');
const expect = require('truffle-expect');
const Resolver = require('truffle-resolver');
const OS = require('os');

const SUPPORTED_COMPILERS = {
    'solc': solcCompile,
    'vyper': vyperCompile,
    'external': externalCompile,
};

/* FIXME: if truffle-worflow-compile added a parameter, a directory name
   under "build", we wouldn't have to change this.
*/
function prepareConfig(options) {
    expect.options(options, [
        'build_mythx_contracts'
    ]);

    // Use a config object to ensure we get the default sources.
    const config = Config.default().merge(options);

    config.compilersInfo = {};

    if (!config.resolver) {
        config.resolver = new Resolver(config);
    }

    return config;
}

/*
  This function is not modified from truffle-workflow-compile.
*/
function multiPromisify (func) {
    // FIXME: accumulating this to a list is weird.
    const resultList = [];
    return (...args) => new Promise( (accept, reject) => {
        const callback = (err, ...results) => {
            if (err) reject(err);
            resultList.push(results);
            //FROM truffle-workflow-compile
            accept(resultList);
        };

        func(...args, callback);
    });
}

const Contracts = {
    collectCompilations: async compilations => {
        let result = { outputs: {}, basenames: {} };

        for (let compilation of await Promise.all(compilations)) {
            let { compiler, artifacts } = compilation;

            if (artifacts) {
                result.outputs[compiler] = artifacts;

                for (const artifact of artifacts) {
                    for (let [ basename, abstraction ] of Object.entries(artifact)) {
                        result.basenames[basename] = abstraction;
                    }

                }
            }
        }

        return result;
    },

    // contracts_directory: String. Directory where .sol files can be found.
    // contracts_build_mythx_contracts: String. Directory where .sol.js files can be found and written to.
    // all: Boolean. Compile all sources found. Defaults to true. If false, will compare sources against built files
    //      in the build directory to see what needs to be compiled.
    // quiet: Boolean. Suppress output. Defaults to false.
    // strict: Boolean. Return compiler warnings as errors. Defaults to false.
    compile: callbackify(async function(options) {
        const config = prepareConfig(options);

        // FIXME: Simplify by removing vyper right now.
        delete config.compilers.vyper;

        const compilers = (config.compiler)
            ? [config.compiler]
            : Object.keys(config.compilers);


        // convert to promise to compile+write
        const compilations = await this.compileSources(config, compilers);
        return await this.collectCompilations(compilations);
    }),

    compileSources: async function(config, compilers) {
        return Promise.all(
            compilers.map(async (compiler) => {
                const compile = SUPPORTED_COMPILERS[compiler];
                if (!compile) throw new Error('Unsupported compiler: ' + compiler);

                const compileFunc = (config.all === true || config.compileAll === true)
                    ? compile.all
                    : compile.necessary;

                let results;
                try {
                    results = await multiPromisify(compileFunc)(config);
                } catch (e) {
                    if(config.debug) {
                        console.error(e);
                    } else {
                        console.error(e.message);
                    }
                    process.exit(1);
                }

                if (results && results.length > 0) {
                    let seenStale = false;
                    for (const result of results) {
                        const [artifact, stale] = result;
                        if (stale) {
                            if (config.quiet != true && config.quietWrite != true && !seenStale) {
                                const relPath = path.relative(config.working_directory, config.build_mythx_contracts);
                                config.logger.log(`Writing artifacts to .${path.sep}${relPath}${OS.EOL}`);
                                seenStale = true;
                            }
                            await this.writeContracts(artifact, config);
                        }
                    }
                }
                return { compiler, results };
            })
        );
    },

    writeContracts: async function(artifact, options) {
        await promisify(mkdirp)(options.build_mythx_contracts);

        const shortNames = Object.keys(artifact);
        await Promise.all(shortNames.map(async (shortName) => {
            const jsonData = JSON.stringify(artifact[shortName], null, 4);
            const jsonPath = path.join(options.build_mythx_contracts, shortName + '.json');
            return await promisify(fs.writeFile)(jsonPath, jsonData);
        }));
    }
};

module.exports = Contracts;