mirror of
https://github.com/embarklabs/embark.git
synced 2025-01-27 05:56:21 +00:00
59d3a3be83
Fiddle is properly deploying now, except the source code needs to be saved to the filesystem in order to be recalled later. Fixes for handling errors on deploy and compilation. Update contract state UI for determining state / interface / deployed.
560 lines
20 KiB
JavaScript
560 lines
20 KiB
JavaScript
let toposort = require('toposort');
|
|
let async = require('async');
|
|
const cloneDeep = require('clone-deep');
|
|
const _ = require('lodash');
|
|
|
|
let utils = require('../../utils/utils.js');
|
|
|
|
// 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: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:all", (contractName, cb) => {
|
|
let contracts = self.listContracts();
|
|
let results = [];
|
|
let i = 0;
|
|
for (let className in contracts) {
|
|
let contract = contracts[className];
|
|
|
|
results.push({
|
|
index: i,
|
|
className: contract.className,
|
|
deploy: contract.deploy,
|
|
error: contract.error,
|
|
address: contract.deployedAddress
|
|
});
|
|
i += 1;
|
|
}
|
|
cb(results);
|
|
});
|
|
|
|
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}, (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});
|
|
res.send({result: newContract._address});
|
|
} catch (e) {
|
|
res.send({result: e.message});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
);
|
|
|
|
embark.registerAPICall(
|
|
'get',
|
|
'/embark-api/contracts',
|
|
(req, res) => {
|
|
self.events.request('contracts:all', null, res.send.bind(res));
|
|
}
|
|
);
|
|
|
|
plugin.registerAPICall(
|
|
'post',
|
|
'/embark-api/contract/deploy',
|
|
(req, res) => {
|
|
this.logger.trace(`POST request /embark-api/contract/deploy:\n ${JSON.stringify(req.body)}`);
|
|
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});
|
|
}
|
|
// pick the compiled contracts that have been built
|
|
const builtContracts = _.pick(self.contracts, contractNames);
|
|
|
|
// for each contract, deploy (in parallel)
|
|
async.eachOf(builtContracts, (contract, contractName, callback) => {
|
|
contract.args = []; /* TODO: override contract.args */
|
|
contract.className = contractName;
|
|
self.events.request("deploy:contract", contract, (err) => {
|
|
callback(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.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];
|
|
}
|
|
|
|
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;
|