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 Artifactor = require('truffle-artifactor');
const OS = require('os');
const SUPPORTED_COMPILERS = {
'solc': solcCompile,
'vyper': vyperCompile,
'external': externalCompile,
};
/* A replacement for truffe-artifacts.save, that
puts in only MythX-needed fields.
*/
const mythXsave = function(object) {
var self = this;
return new Promise(function(accept, reject) {
if (object.contractName == null) {
return reject(new Error('You must specify a contract name.'));
}
delete object.contract_name;
var outputPath = object.contractName;
// Create new path off of destination.
outputPath = path.join(self.destination, outputPath);
outputPath = path.resolve(outputPath);
// Add json extension.
outputPath = outputPath + '.json';
fs.readFile(outputPath, {encoding: 'utf8'}, function(err, json) {
// No need to handle the error. If the file doesn't exist then we'll start afresh
// with a new object.
const finalObject = object;
if (!err) {
try {
JSON.parse(json);
} catch (e) {
reject(e);
}
/*
// normalize existing and merge into final
finalObject = Schema.normalize(existingObjDirty);
// merge networks
var finalNetworks = {};
_.merge(finalNetworks, finalObject.networks, object.networks);
// update existing with new
_.assign(finalObject, object);
finalObject.networks = finalNetworks;
*/
}
// update timestamp
finalObject.updatedAt = new Date().toISOString();
// output object
fs.outputFile(outputPath, JSON.stringify(finalObject, null, 2), 'utf8', function(err) {
if (err) return reject(err);
accept();
});
});
});
};
/* 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);
if (!config.resolver) {
config.resolver = new Resolver(config);
}
if (!config.artifactor) {
config.artifactor = new Artifactor(config.build_mythx_contracts);
config.artifactor.save = mythXsave;
}
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);
accept(resultList);
};
func(...args, callback);
});
}
const Contracts = {
// 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);
const collect = async (compilations) => {
let result = {
outputs: {},
basenames: {}
};
for (let compilation of 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;
};
return await collect(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 = await multiPromisify(compileFunc)(config);
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;