embark-area-51/lib/modules/contracts_manager/index.js

588 lines
21 KiB
JavaScript

let toposort = require('toposort');
let async = require('async');
const cloneDeep = require('clone-deep');
const utils = require('../../utils/utils.js');
const fs = require('../../core/fs');
// TODO: create a contract object
class ContractsManager {
constructor(embark, options) {
const self = this;
this.logger = embark.logger;
this.events = embark.events;
this.contracts = {};
this.contractDependencies = {};
this.deployOnlyOnConfig = false;
this.compileError = false;
this.compileOnceOnly = options.compileOnceOnly;
this.disableOptimizations = options.disableOptimizations;
self.events.setCommandHandler('contracts:list', (cb) => {
cb(self.compileError, self.listContracts());
});
self.events.setCommandHandler('contracts:all', (cb) => {
cb(self.compileError, self.contracts);
});
self.events.setCommandHandler('contracts:dependencies', (cb) => {
cb(self.compileError, self.contractDependencies);
});
self.events.setCommandHandler("contracts:contract", (contractName, cb) => {
cb(self.getContract(contractName));
});
self.events.setCommandHandler("contracts:contract:byTxHash", (txHash, cb) => {
self.getContractByTxHash(txHash, cb)
});
self.events.setCommandHandler("contracts:build", (configOnly, cb) => {
self.deployOnlyOnConfig = configOnly; // temporary, should refactor
self.build((err) => {
cb(err);
});
});
self.events.setCommandHandler("contracts:reset:dependencies", (cb) => {
self.contractDependencies = {};
cb();
});
self.events.on("deploy:contract:error", (_contract) => {
self.events.emit('contractsState', self.contractsState());
});
self.events.on("deploy:contract:deployed", (_contract) => {
self.events.emit('contractsState', self.contractsState());
});
self.events.on("deploy:contract:undeployed", (_contract) => {
self.events.emit('contractsState', self.contractsState());
});
this.events.setCommandHandler('setDashboardState', () => {
self.events.emit('contractsState', self.contractsState());
});
self.events.setCommandHandler("contracts:formatted:all", (cb) => {
const contracts = self.listContracts().map((contract, index) => (
{
className: contract.className,
deploy: contract.deploy,
error: contract.error,
address: contract.deployedAddress,
isFiddle: Boolean(contract.isFiddle),
args: contract.args,
transactionHash: contract.transactionHash,
gas: contract.gas,
gasPrice: contract.gasPrice,
index
}
));
cb(contracts);
});
embark.registerAPICall(
'get',
'/embark-api/contract/:contractName',
(req, res) => {
self.events.request('contracts:contract', req.params.contractName, res.send.bind(res));
}
);
embark.registerAPICall(
'post',
'/embark-api/contract/:contractName/function',
(req, res) => {
async.parallel({
contract: (callback) => {
self.events.request('contracts:contract', req.body.contractName, (contract) => callback(null, contract));
},
account: (callback) => {
self.events.request("blockchain:defaultAccount:get", (account) => callback(null, account));
}
}, (error, result) => {
if (error) {
return res.send({error: error.message});
}
const {account, contract} = result;
const abi = contract.abiDefinition.find(definition => definition.name === req.body.method);
const funcCall = (abi.constant === true || abi.stateMutability === 'view' || abi.stateMutability === 'pure') ? 'call' : 'send';
self.events.request("blockchain:contract:create", {abi: contract.abiDefinition, address: contract.deployedAddress}, (contractObj) => {
try {
contractObj.methods[req.body.method].apply(this, req.body.inputs)[funcCall]({from: account, gasPrice: req.body.gasPrice}, (error, result) => {
if (error) {
return res.send({result: error.message});
}
res.send({result});
});
} catch (e) {
res.send({result: e.message});
}
});
});
}
);
embark.registerAPICall(
'post',
'/embark-api/contract/:contractName/deploy',
(req, res) => {
async.parallel({
contract: (callback) => {
self.events.request('contracts:contract', req.body.contractName, (contract) => callback(null, contract));
},
account: (callback) => {
self.events.request("blockchain:defaultAccount:get", (account) => callback(null, account));
}
}, (error, result) => {
if (error) {
return res.send({error: error.message});
}
const {account, contract} = result;
self.events.request("blockchain:contract:create", {abi: contract.abiDefinition}, async (contractObj) => {
try {
const params = {data: `0x${contract.code}`, arguments: req.body.inputs};
let gas = await contractObj.deploy(params).estimateGas();
let newContract = await contractObj.deploy(params).send({from: account, gas, gasPrice: req.body.gasPrice});
res.send({result: newContract._address});
} catch (e) {
res.send({result: e.message});
}
});
});
}
);
embark.registerAPICall(
'get',
'/embark-api/contracts',
(req, res) => {
const result = [];
self.events.request('contracts:formatted:all', (contracts) => {
contracts.forEach((contract) => {
self.events.request('contracts:contract', contract.className, (c) => (
result.push(Object.assign(contract, c))
));
});
});
res.send(result);
}
);
embark.registerAPICall(
'post',
'/embark-api/contract/deploy',
(req, res) => {
this.logger.trace(`POST request /embark-api/contract/deploy:\n ${JSON.stringify(req.body)}`);
if(typeof req.body.compiledContract !== 'object'){
return res.send({error: 'Body parameter \'compiledContract\' must be an object'});
}
self.compiledContracts = Object.assign(self.compiledContracts, req.body.compiledContract);
const contractNames = Object.keys(req.body.compiledContract);
self.build((err, _mgr) => {
if(err){
return res.send({error: err.message});
}
// for each compiled contract, deploy (in parallel)
async.each(contractNames, (contractName, next) => {
const contract = self.contracts[contractName];
contract.args = []; /* TODO: override contract.args */
contract.className = contractName;
contract.isFiddle = true;
self.events.request("deploy:contract", contract, (err) => {
next(err);
});
}, (err) => {
let responseData = {};
if(err){
responseData.error = err.message;
}
else responseData.result = contractNames;
this.logger.trace(`POST response /embark-api/contract/deploy:\n ${JSON.stringify(responseData)}`);
res.send(responseData);
});
}, false, false);
}
);
}
build(done, useContractFiles = true, resetContracts = true) {
let self = this;
self.contracts = {};
let compilerOptions = {disableOptimizations: this.disableOptimizations};
if(resetContracts) self.contracts = {};
async.waterfall([
function loadContractFiles(callback) {
self.events.request("config:contractsFiles", (contractsFiles) => {
self.contractsFiles = contractsFiles;
callback();
});
},
function loadContractConfigs(callback) {
self.events.request("config:contractsConfig", (contractsConfig) => {
self.contractsConfig = cloneDeep(contractsConfig);
callback();
});
},
function compileContracts(callback) {
self.events.emit("status", __("Compiling..."));
if (self.compileOnceOnly && self.compiledContracts && Object.keys(self.compiledContracts).length) {
return callback();
}
self.events.request("compiler:contracts", self.contractsFiles, compilerOptions, function (err, compiledObject) {
self.compiledContracts = compiledObject;
callback(err);
});
},
function prepareContractsFromConfig(callback) {
self.events.emit("status", __("Building..."));
// if we are appending contracts (ie fiddle), we
// don't need to build a contract from config, so
// we can skip this entirely
if(!resetContracts) return callback();
let className, contract;
for (className in self.contractsConfig.contracts) {
contract = self.contractsConfig.contracts[className];
contract.className = className;
contract.args = contract.args || [];
self.contracts[className] = contract;
}
callback();
},
function getGasPriceForNetwork(callback) {
return callback(null, self.contractsConfig.gasPrice);
},
function prepareContractsFromCompilation(gasPrice, callback) {
let className, compiledContract, contractConfig, contract;
for (className in self.compiledContracts) {
compiledContract = self.compiledContracts[className];
contractConfig = self.contractsConfig.contracts[className];
contract = self.contracts[className] || {className: className, args: []};
contract.code = compiledContract.code;
contract.runtimeBytecode = compiledContract.runtimeBytecode;
contract.realRuntimeBytecode = (compiledContract.realRuntimeBytecode || compiledContract.runtimeBytecode);
contract.swarmHash = compiledContract.swarmHash;
contract.gasEstimates = compiledContract.gasEstimates;
contract.functionHashes = compiledContract.functionHashes;
contract.abiDefinition = compiledContract.abiDefinition;
contract.filename = compiledContract.filename;
contract.originalFilename = compiledContract.originalFilename || ("contracts/" + contract.filename);
contract.path = fs.dappPath(contract.originalFilename);
contract.gas = (contractConfig && contractConfig.gas) || self.contractsConfig.gas || 'auto';
contract.gasPrice = contract.gasPrice || gasPrice;
contract.type = 'file';
contract.className = className;
self.contracts[className] = contract;
}
callback();
},
function setDeployIntention(callback) {
let className, contract;
for (className in self.contracts) {
contract = self.contracts[className];
contract.deploy = (contract.deploy === undefined) || contract.deploy;
if (self.deployOnlyOnConfig && !self.contractsConfig.contracts[className]) {
contract.deploy = false;
}
if (contract.code === "") {
const message = __("assuming %s to be an interface", className);
if (contract.silent) {
self.logger.trace(message);
} else {
self.logger.info(message);
}
contract.deploy = false;
}
}
callback();
},
/*eslint complexity: ["error", 11]*/
function dealWithSpecialConfigs(callback) {
let className, contract, parentContractName, parentContract;
let dictionary = Object.keys(self.contracts);
for (className in self.contracts) {
contract = self.contracts[className];
if (contract.instanceOf === undefined) {
continue;
}
parentContractName = contract.instanceOf;
parentContract = self.contracts[parentContractName];
if (parentContract === className) {
self.logger.error(__("%s : instanceOf is set to itself", className));
continue;
}
if (parentContract === undefined) {
self.logger.error(__("{{className}}: couldn't find instanceOf contract {{parentContractName}}", {
className: className,
parentContractName: parentContractName
}));
let suggestion = utils.proposeAlternative(parentContractName, dictionary, [className, parentContractName]);
if (suggestion) {
self.logger.warn(__('did you mean "%s"?', suggestion));
}
continue;
}
if (parentContract.args && parentContract.args.length > 0 && ((contract.args && contract.args.length === 0) || contract.args === undefined)) {
contract.args = parentContract.args;
}
if (contract.code !== undefined) {
self.logger.error(__("{{className}} has code associated to it but it's configured as an instanceOf {{parentContractName}}", {
className: className,
parentContractName: parentContractName
}));
}
contract.code = parentContract.code;
contract.runtimeBytecode = parentContract.runtimeBytecode;
contract.realRuntimeBytecode = (parentContract.realRuntimeBytecode || parentContract.runtimeBytecode);
contract.gasEstimates = parentContract.gasEstimates;
contract.functionHashes = parentContract.functionHashes;
contract.abiDefinition = parentContract.abiDefinition;
contract.gas = contract.gas || parentContract.gas;
contract.gasPrice = contract.gasPrice || parentContract.gasPrice;
contract.type = 'instance';
}
callback();
},
function removeContractsWithNoCode(callback) {
let className, contract;
let dictionary = Object.keys(self.contracts);
for (className in self.contracts) {
contract = self.contracts[className];
if (contract.code === undefined) {
self.logger.error(__("%s has no code associated", className));
let suggestion = utils.proposeAlternative(className, dictionary, [className]);
if (suggestion) {
self.logger.warn(__('did you mean "%s"?', suggestion));
}
delete self.contracts[className];
}
}
self.logger.trace(self.contracts);
callback();
},
// TODO: needs refactoring, has gotten too complex
/*eslint complexity: ["error", 16]*/
/*eslint max-depth: ["error", 16]*/
function determineDependencies(callback) {
let className, contract;
for (className in self.contracts) {
contract = self.contracts[className];
// look in code for dependencies
let libMatches = (contract.code.match(/:(.*?)(?=_)/g) || []);
for (let match of libMatches) {
self.contractDependencies[className] = self.contractDependencies[className] || [];
self.contractDependencies[className].push(match.substr(1));
}
// look in arguments for dependencies
if (contract.args === []) continue;
let ref;
if (Array.isArray(contract.args)) {
ref = contract.args;
} else {
let keys = Object.keys(contract.args);
ref = keys.map((k) => contract.args[k]).filter((x) => !x);
}
for (let j = 0; j < ref.length; j++) {
let arg = ref[j];
if (arg[0] === "$" && !arg.startsWith('$accounts')) {
self.contractDependencies[className] = self.contractDependencies[className] || [];
self.contractDependencies[className].push(arg.substr(1));
self.checkDependency(className, arg.substr(1));
}
if (Array.isArray(arg)) {
for (let sub_arg of arg) {
if (sub_arg[0] === "$" && !sub_arg.startsWith('$accounts')) {
self.contractDependencies[className] = self.contractDependencies[className] || [];
self.contractDependencies[className].push(sub_arg.substr(1));
self.checkDependency(className, sub_arg.substr(1));
}
}
}
}
// look in onDeploy for dependencies
if (contract.onDeploy !== [] && contract.onDeploy !== undefined) {
let regex = /\$\w+/g;
contract.onDeploy.map((cmd) => {
if (cmd.indexOf('$accounts') > -1) {
return;
}
cmd.replace(regex, (match) => {
self.contractDependencies[className] = self.contractDependencies[className] || [];
self.contractDependencies[className].push(match.substr(1));
});
});
}
// Remove duplicates
if (self.contractDependencies[className]) {
const o = {};
self.contractDependencies[className].forEach(function (e) {
o[e] = true;
});
self.contractDependencies[className] = Object.keys(o);
}
}
callback();
}
], function (err) {
if (err) {
self.compileError = true;
self.events.emit("status", __("Compile/Build error"));
self.events.emit("outputError", __("Error building Dapp, please check console"));
self.logger.error(__("Error Compiling/Building contracts: ") + err);
} else {
self.compileError = false;
}
self.logger.trace("finished".underline);
done(err, self);
});
}
checkDependency(className, dependencyName) {
if (!this.contractDependencies[className]) {
return;
}
if (!this.contracts[dependencyName]) {
this.logger.warn(__('{{className}} has a dependency on {{dependencyName}}', {className, dependencyName}) +
__(', but it is not present in the contracts'));
return;
}
if (!this.contracts[dependencyName].deploy) {
this.logger.warn(__('{{className}} has a dependency on {{dependencyName}}', {className, dependencyName}) +
__(', but it is not set to deploy. It could be an interface.'));
}
}
getContract(className) {
return this.contracts[className];
}
getContractByTxHash(txHash, cb) {
this.events.request("blockchain:getTransaction", txHash, (err, tx) => {
if (err) return cb(err);
for (let contractName in this.contracts) {
let contract = this.contracts[contractName];
if (tx.to === contract.deployedAddress) {
return cb(null, contract);
}
}
cb("no known contract found for txHash: " + txHash);
});
}
sortContracts(contractList) {
let converted_dependencies = [], i;
for (let contract in this.contractDependencies) {
let dependencies = this.contractDependencies[contract];
for (i = 0; i < dependencies.length; i++) {
converted_dependencies.push([contract, dependencies[i]]);
}
}
let orderedDependencies;
try {
orderedDependencies = toposort(converted_dependencies.filter((x) => x[0] !== x[1])).reverse();
} catch (e) {
this.logger.error((__("Error: ") + e.message).red);
this.logger.error(__("there are two or more contracts that depend on each other in a cyclic manner").bold.red);
this.logger.error(__("Embark couldn't determine which one to deploy first").red);
throw new Error("CyclicDependencyError");
//process.exit(0);
}
return contractList.sort(function (a, b) {
let order_a = orderedDependencies.indexOf(a.className);
let order_b = orderedDependencies.indexOf(b.className);
return order_a - order_b;
});
}
// TODO: should be built contracts
listContracts() {
let contracts = [];
for (let className in this.contracts) {
let contract = this.contracts[className];
contracts.push(contract);
}
return this.sortContracts(contracts);
}
contractsState() {
let data = [];
for (let className in this.contracts) {
let contract = this.contracts[className];
if (contract.silent) {
continue;
}
let contractData;
if (contract.deploy === false) {
contractData = [
className.green,
__('Interface or set to not deploy').green,
"\t\tn/a".green
];
} else if (contract.error) {
contractData = [
className.green,
(contract.error).split("\n")[0].replace(/Error: /g, '').substring(0, 32).red,
'\t\tError'.red
];
} else {
contractData = [
className.green,
(contract.deployedAddress || '...').green,
((contract.deployedAddress !== undefined) ? ("\t\t" + __("Deployed")).green : ("\t\t" + __("Pending")).magenta)
];
}
data.push(contractData);
}
return data;
}
}
module.exports = ContractsManager;