refactor(@embark/embark-deploy-tracker): Add back contract tracking

Add back contract tracking to the refactored code. Deployment checks are added as plugins to the `embark-deployment` module.

Adds ability to track if a contract has already been deployed, and skips deployment if so.

Updates error handling flow for deployment process.

Adds a contract class to the `embark-contracts-manager`, to add a `log` function for the contract. This `log` function can be called from any module that has the contract instance.

Adds TS interfaces for contracts configuration.

Handles the following cases:
1. Contract already deployed
2. Contract not deployed
3. Contract is configured with `{track: false}` (deploy if not deployed, and don't track)
5. Contract is configured with an `address` in the config
6. `trackContracts` set to `false` from `engine` (always deploy but don't track contracts). Currently used for the tests.
7. Contract deployment produces an error
8. Interface deployment shows warning.

PR with unit tests and documenation to follow.
This commit is contained in:
emizzle 2019-08-02 15:00:15 +10:00 committed by Iuri Matias
parent 57b3b8d131
commit 59a0eea295
13 changed files with 373 additions and 197 deletions

View File

@ -26,21 +26,20 @@
},
"main": "./dist/index.js",
"scripts": {
"build": "cross-env BABEL_ENV=node babel src --extensions \".js\" --out-dir dist --root-mode upward --source-maps",
"build": "cross-env BABEL_ENV=node babel src --extensions \".js,.ts\" --out-dir dist --root-mode upward --source-maps",
"ci": "npm run qa",
"clean": "npm run reset",
"lint": "npm-run-all lint:*",
"lint:js": "eslint src/",
"// lint:ts": "tslint -c tslint.json \"src/**/*.ts\"",
"lint:ts": "tslint -c tslint.json \"src/**/*.ts\"",
"package": "npm pack",
"// qa": "npm-run-all lint typecheck build package",
"qa": "npm-run-all lint build package",
"qa": "npm-run-all lint typecheck build package",
"reset": "npx rimraf dist embark-*.tgz package",
"start": "npm run watch",
"// typecheck": "tsc",
"typecheck": "tsc",
"watch": "run-p watch:*",
"watch:build": "npm run build -- --verbose --watch",
"// watch:typecheck": "npm run typecheck -- --preserveWatchOutput --watch"
"watch:typecheck": "npm run typecheck -- --preserveWatchOutput --watch"
},
"eslintConfig": {
"extends": "../../.eslintrc.json"

View File

@ -0,0 +1,49 @@
import { ContractConfig, Logger } from "embark";
const { sha3 } = require("embark-utils");
import { ABIDefinition } from "web3/eth/abi";
export default class Contract {
private logger: Logger;
public abiDefinition?: ABIDefinition[];
public deployedAddress?: string;
public className: string = "";
public address?: string;
public args?: any[] = [];
public instanceOf?: string;
public gas?: number;
public gasPrice?: number;
public silent?: boolean = false;
public track?: boolean = true;
public deploy?: boolean = true;
public realRuntimeBytecode: string = "";
public realArgs: any[] = [];
constructor(logger: Logger, contractConfig: ContractConfig) {
this.logger = logger;
this.address = contractConfig.address;
this.args = contractConfig.args;
this.instanceOf = contractConfig.instanceOf;
this.gas = contractConfig.gas;
this.gasPrice = contractConfig.gasPrice;
this.silent = contractConfig.silent;
this.track = contractConfig.track;
this.deploy = contractConfig.deploy;
}
/**
* Calculates a hash from runtime bytecode, classname, and deploy arguments.
* Used for uniquely identifying a contract, ie in chains.json.
*/
get hash() {
return sha3(this.realRuntimeBytecode + this.className + (this.realArgs || this.args).join(","));
}
/**
* Logs a message to the console. Logs with loglevel trace if contract has it's silent
* property set (in the config or internally, ie ENS contracts). Otherwise, logs with
* info log level.
* @param {string} message message to log to the console
*/
public log(message: string) {
this.silent ? this.logger.trace(message) : this.logger.info(message);
}
}

View File

@ -1,4 +1,5 @@
import {__} from 'embark-i18n';
import Contract from './contract';
const async = require('async');
const constants = require('embark-core/constants');
const {dappPath, proposeAlternative, toposort} = require('embark-utils');
@ -263,6 +264,7 @@ class ContractsManager {
self.events.emit("status", __("Building..."));
async.eachOf(contractsConfig.contracts, (contract, className, eachCb) => {
contract = new Contract(self.logger, contract);
if (!contract.artifact) {
contract.className = className;
contract.args = contract.args || [];

View File

@ -0,0 +1,84 @@
import {__} from 'embark-i18n';
import {toChecksumAddress} from 'embark-utils';
import Web3 from "web3";
export default class DeploymentChecks {
constructor({trackingFunctions, logger, events, plugins}) {
this.trackingFunctions = trackingFunctions;
this.logger = logger;
this.events = events;
this.plugins = plugins;
this._web3 = null;
}
get web3() {
return (async () => {
if (!this._web3) {
const provider = await this.events.request2("blockchain:client:provider", "ethereum");
this._web3 = new Web3(provider);
}
return this._web3;
})();
}
async checkContractConfig(params, cb) {
const {contract} = params;
// previous event action check
if (!params.shouldDeploy) {
return cb(null, params);
}
// contract config address field set - do not deploy
if (contract.address !== undefined) {
try {
toChecksumAddress(contract.address);
} catch (e) {
return cb(e);
}
contract.deployedAddress = contract.address;
contract.log(contract.className.bold.cyan + __(" already deployed at ").green + contract.deployedAddress.bold.cyan);
params.shouldDeploy = false;
return cb(null, params);
}
cb(null, params);
}
async checkIfAlreadyDeployed(params, cb) {
const {contract} = params;
const trackedContract = this.trackingFunctions.getContract(contract);
// previous event action check
if (!params.shouldDeploy) {
return cb(null, params);
}
// contract is not already tracked - deploy
if (!trackedContract || !trackedContract.address) {
return cb(null, params);
}
// tracked contract has track field set - deploy anyway, but tell user
if (trackedContract.track === false || this.trackingFunctions.trackContracts === false) {
contract.log(contract.className.bold.cyan + __(" will be redeployed").green);
return cb(null, params);
}
// if bytecode for the contract in chains.json exists on chain - don't deploy
const web3 = await this.web3;
let codeInChain = "";
try {
codeInChain = await web3.eth.getCode(trackedContract.address);
}
catch (err) {
return cb(err);
}
if (codeInChain.length > 3) { // it is "0x" or "0x0" for empty code, depending on web3 version
contract.deployedAddress = trackedContract.address;
contract.log(contract.className.bold.cyan + __(" already deployed at ").green + contract.deployedAddress.bold.cyan);
params.shouldDeploy = false;
}
cb(null, params);
}
}

View File

@ -1,130 +1,24 @@
import { __ } from 'embark-i18n';
import { dappPath, sha3 } from 'embark-utils';
const Web3 = require('web3');
import DeploymentChecks from "./deploymentChecks";
import TrackingFunctions from "./trackingFunctions";
class DeployTracker {
constructor(embark, options) {
this.logger = embark.logger;
this.events = embark.events;
this.plugins = options.plugins;
this.fs = embark.fs;
constructor(embark, {trackContracts, env, plugins}) {
const {logger, events, fs, config} = embark;
this.embark = embark;
this.trackContracts = (options.trackContracts !== false);
// TODO: unclear where env comes from
// TODO: we should be getting the env from a request to the config
this.env = options.env;
this.chainConfig = {};
this.chainFile = embark.config.contractsConfig.tracking;
this.events.on("blockchain:started", this.loadChainTrackerFile.bind(this));
this.embark.registerActionForEvent('deployment:deployContracts:beforeAll', this.setCurrentChain.bind(this));
this.embark.registerActionForEvent("deployment:contract:deployed", this.trackAndSaveContract.bind(this));
this.embark.registerActionForEvent("deploy:contract:shouldDeploy", this.checkIfDeploymentIsNeeded.bind(this));
}
const trackingFunctions = new TrackingFunctions({config, fs, logger, events, env, trackContracts});
const deploymentChecks = new DeploymentChecks({trackingFunctions, logger, events, plugins});
trackAndSaveContract(params, cb) {
if (!this.embark.config.contractsConfig.tracking) return cb();
let contract = params.contract;
this.trackContract(contract.className, contract.realRuntimeBytecode, contract.realArgs, contract.deployedAddress);
this.save();
cb();
this.embark.events.on("blockchain:started", trackingFunctions.loadChainTrackerFile.bind(trackingFunctions));
this.embark.registerActionForEvent('deployment:deployContracts:beforeAll', trackingFunctions.setCurrentChain.bind(trackingFunctions));
this.embark.registerActionForEvent("deployment:contract:deployed", trackingFunctions.trackAndSaveContract.bind(trackingFunctions));
this.embark.registerActionForEvent("deployment:contract:shouldDeploy", deploymentChecks.checkContractConfig.bind(deploymentChecks));
this.embark.registerActionForEvent("deployment:contract:shouldDeploy", deploymentChecks.checkIfAlreadyDeployed.bind(deploymentChecks));
}
checkIfDeploymentIsNeeded(params, cb) {
if (!this.embark.config.contractsConfig.tracking) return;
if (!this.trackContracts) {
return cb(null, params);
}
let contract = params.contract;
let trackedContract = this.getContract(contract.className, contract.realRuntimeBytecode, contract.realArgs);
if (trackedContract) {
params.contract.address = trackedContract.address;
}
if (params.shouldDeploy && trackedContract) {
params.shouldDeploy = true;
}
cb(null, params);
}
loadChainTrackerFile() {
if (this.chainFile === false) return;
if (this.chainFile === undefined) this.chainFile = ".embark/chains.json";
this.chainFile = dappPath(this.chainFile);
if (!this.fs.existsSync(this.chainFile)) {
this.logger.info(this.chainFile + ' ' + __('file not found, creating it...'));
this.fs.outputJSONSync(this.chainFile, {});
}
this.chainConfig = this.fs.readJSONSync(this.chainFile);
}
setCurrentChain(_params, callback) {
if (!this.embark.config.contractsConfig.tracking) return callback();
if (this.chainFile === false) return callback();
if (this.chainConfig === false) {
this.currentChain = {contracts: []};
return callback();
}
this.getBlock(0, (err) => {
if (err) {
// Retry with block 1 (Block 0 fails with Ganache-cli using the --fork option)
return this.getBlock(1, callback);
}
callback();
});
}
async getBlock(blockNum, cb) {
let provider = await this.events.request2("blockchain:client:provider", "ethereum");
var web3 = new Web3(provider);
try {
let block = await web3.eth.getBlock(blockNum, true);
let chainId = block.hash;
if (self.chainConfig[chainId] === undefined) {
self.chainConfig[chainId] = { contracts: {} };
}
self.currentChain = self.chainConfig[chainId];
self.currentChain.name = self.env;
cb();
} catch (err) {
return cb(err);
}
}
loadConfig(config) {
this.chainConfig = config;
return this;
}
trackContract(name, code, args, address) {
if (!this.currentChain) return false;
this.currentChain.contracts[sha3(code + name + args.join(','))] = { name, address };
}
getContract(name, code, args) {
if (!this.currentChain) return false;
let contract = this.currentChain.contracts[sha3(code + name + args.join(','))];
if (contract && contract.address === undefined) {
return false;
}
return contract;
}
save() {
if (this.chainConfig === false) {
return;
}
this.fs.writeJSONSync(this.chainFile, this.chainConfig, {spaces: 2});
}
}
module.exports = DeployTracker;

View File

@ -0,0 +1,111 @@
import {__} from 'embark-i18n';
import {dappPath} from 'embark-utils';
import Web3 from 'web3';
export default class TrackingFunctions {
constructor({config, env, fs, events, logger, trackContracts}) {
this.config = config;
this.chainConfig = {};
this.chainFile = config.contractsConfig.tracking;
this.currentChain = null;
this.env = env;
this.fs = fs;
this.events = events;
this.logger = logger;
this._web3 = null;
this.trackContracts = (trackContracts !== false);
}
get web3() {
return (async () => {
if (!this._web3) {
const provider = await this.events.request2("blockchain:client:provider", "ethereum");
this._web3 = new Web3(provider);
}
return this._web3;
})();
}
getContract(contract) {
if (!this.currentChain) return false;
let contractInFile = this.currentChain.contracts[contract.hash];
if (contractInFile && contractInFile.address === undefined) {
return false;
}
return contractInFile;
}
trackAndSaveContract(params, cb) {
const {contract} = params;
if (!this.chainFile || !this.trackContracts || contract.track === false) return cb();
this.trackContract(contract);
this.save();
cb();
}
loadChainTrackerFile() {
if (this.chainFile === false) return;
if (this.chainFile === undefined) this.chainFile = ".embark/chains.json";
this.chainFile = dappPath(this.chainFile);
if (!this.fs.existsSync(this.chainFile)) {
this.logger.info(this.chainFile + ' ' + __('file not found, creating it...'));
this.fs.outputJSONSync(this.chainFile, {});
this.chainConfig = {};
return;
}
this.chainConfig = this.fs.readJSONSync(this.chainFile);
}
setCurrentChain(_params, callback) {
if (this.chainFile === false) return callback();
if (this.chainConfig === false) {
this.currentChain = {contracts: []};
return callback();
}
this.getBlock(0, (err) => {
if (err) {
// Retry with block 1 (Block 0 fails with Ganache-cli using the --fork option)
return this.getBlock(1, callback);
}
callback();
});
}
async getBlock(blockNum, cb) {
try {
const web3 = await this.web3;
let block = await web3.eth.getBlock(blockNum, true);
let chainId = block.hash;
if (this.chainConfig[chainId] === undefined) {
this.chainConfig[chainId] = {contracts: {}};
}
this.currentChain = this.chainConfig[chainId];
this.currentChain.name = this.env;
cb();
} catch (err) {
return cb(err);
}
}
loadConfig(config) {
this.chainConfig = config;
return this;
}
trackContract(contract) {
if (!this.currentChain) return false;
this.currentChain.contracts[contract.hash] = {name: contract.className, address: contract.deployedAddress};
}
save() {
if (this.chainConfig === false) {
return;
}
this.fs.writeJSONSync(this.chainFile, this.chainConfig, {spaces: 2});
}
}

View File

@ -8,7 +8,7 @@ class ContractDeployer {
this.plugins = options.plugins;
this.deployer = {};
this.events.setCommandHandler("deployment:deployer:register", (blockchainType, deployerCb) => {
this.deployer[blockchainType] = deployerCb
this.deployer[blockchainType] = deployerCb;
});
this.events.setCommandHandler('deployment:contract:deploy', this.deployContract.bind(this));
@ -28,33 +28,33 @@ class ContractDeployer {
},
(next) => {
// self.plugins.emitAndRunActionsForEvent('deployment:contract:arguments', {contract: contract}, (_params) => {
this.plugins.emitAndRunActionsForEvent('deployment:contract:shouldDeploy', {contract: contract, shouldDeploy: true}, (_params) => {
next();
this.plugins.emitAndRunActionsForEvent('deployment:contract:shouldDeploy', {contract: contract, shouldDeploy: true}, (err, params) => {
next(err, params);
});
},
(next) => {
if (contract.deploy === false) {
(params, next) => {
if (!params.shouldDeploy) {
this.events.emit("deployment:contract:undeployed", contract);
return next();
return next(null, null);
}
console.dir("deploying contract");
console.dir(contract.className);
// this.deployer[contract.blockchainType].apply(this.deployer, [contract, next])
this.deployer["ethereum"].apply(this.deployer, [contract, next])
// next();
// TODO: implement `blockchainType` a la `this.deployer[contract.blockchainType].apply(this.deployer, [contract, next])`
this.deployer["ethereum"].apply(this.deployer, [contract, next]);
},
(next) => {
console.dir("-------> contract deployed")
if (contract.deploy === false) return next();
console.dir("-------> contract deployed 2")
this.plugins.emitAndRunActionsForEvent('deployment:contract:deployed', {contract: contract}, (_params) => {
next();
(receipt, next) => {
if (!receipt) return next();
this.plugins.emitAndRunActionsForEvent('deployment:contract:deployed', {contract, receipt}, (err, _params) => {
next(err);
});
}
], callback);
], (err) => {
if (err) {
this.events.emit("deploy:contract:error", contract);
}
callback(err);
});
}
}
module.exports = ContractDeployer;

View File

@ -15,7 +15,8 @@ class Deployment {
this.contractDeployer = new ContractDeployer({
events: this.events,
plugins: this.plugins
plugins: this.plugins,
logger: this.logger
});
this.events.setCommandHandler('deployment:contracts:deploy', (contractsList, contractDependencies, cb) => {
@ -28,7 +29,7 @@ class Deployment {
async.waterfall([
// TODO used to be called this.plugins.emitAndRunActionsForEvent("deploy:beforeAll", (err) => {
(next) => {this.plugins.emitAndRunActionsForEvent('deployment:deployContracts:beforeAll', {}, () => {next()});},
(next) => { this.deployAll(contracts, contractDependencies, () => { next() }); },
(next) => {this.deployAll(contracts, contractDependencies, next);},
(next) => {
this.events.emit('contractsDeployed');
this.plugins.emitAndRunActionsForEvent('deployment:deployContracts:afterAll', {}, () => {next()});
@ -37,7 +38,7 @@ class Deployment {
], done);
}
deployContract(contract, callback) {
deployContract(contract, errors, callback) {
console.dir("requesting to deploy contract")
this.events.request('deployment:contract:deploy', contract, (err) => {
if (err) {
@ -63,7 +64,7 @@ class Deployment {
function deploy(result, callback) {
console.dir("== deploy")
if (typeof result === 'function') callback = result;
self.deployContract(contract, callback);
self.deployContract(contract, errors, callback);
}
const className = contract.className;
@ -75,23 +76,22 @@ class Deployment {
contractDeploys[className].push(deploy);
})
async.auto(contractDeploys, (_err, _results) => {
if (_err) {
async.auto(contractDeploys, (err, _results) => {
if (err) {
console.dir("error deploying contracts")
console.dir(_err)
console.dir(err)
}
if (errors.length) {
_err = __("Error deploying contracts. Please fix errors to continue.");
this.logger.error(_err);
err = __("Error deploying contracts. Please fix errors to continue.");
this.events.emit("outputError", __("Error deploying contracts, please check console"));
return done(_err);
return done(err);
}
if (contracts.length === 0) {
this.logger.info(__("no contracts found"));
return done();
}
this.logger.info(__("finished deploying contracts"));
done(_err);
done(err);
});
}

View File

@ -4,6 +4,7 @@ import "./src/remix-debug-debugtest";
export * from "./src/callbacks";
export * from "./src/contract";
export * from "./src/embark";
export * from "./src/contractsConfig";
export * from "./src/embarkConfig";
export * from "./src/logger";
export * from "./src/maybe";

View File

@ -0,0 +1,16 @@
export interface ContractsConfig {
deploy: { [name: string]: ContractConfig }
gas: string | number;
tracking: boolean | string;
}
export interface ContractConfig {
address?: string;
args?: Array<any>;
instanceOf?: string;
gas?: number;
gasPrice?: number;
silent?: boolean;
track?: boolean;
deploy?: boolean;
}

View File

@ -1,5 +1,6 @@
export interface Logger {
info(text: string): void;
warn(text: string): void;
trace(text: string): void;
error(text: string, ...args: Array<string | Error>): void;
}

View File

@ -219,7 +219,12 @@ class EmbarkController {
let _contractsConfig = await engine.events.request2("config:contractsConfig");
let contractsConfig = cloneDeep(_contractsConfig);
let [contractsList, contractDependencies] = await engine.events.request2("contracts:build", contractsConfig, compiledContracts);
try {
await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies);
}
catch (err) {
engine.logger.error(err);
}
} else if (fileType === 'asset') {
engine.events.request('pipeline:generateAll', () => {
console.dir("outputDone")
@ -238,7 +243,12 @@ class EmbarkController {
let _contractsConfig = await engine.events.request2("config:contractsConfig");
let contractsConfig = cloneDeep(_contractsConfig);
let [contractsList, contractDependencies] = await engine.events.request2("contracts:build", contractsConfig, compiledContracts);
try {
await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies);
}
catch (err) {
engine.logger.error(err);
}
console.dir("deployment done")
await engine.events.request2("watcher:start")
@ -805,7 +815,12 @@ class EmbarkController {
let _contractsConfig = await engine.events.request2("config:contractsConfig");
let contractsConfig = cloneDeep(_contractsConfig);
let [contractsList, contractDependencies] = await engine.events.request2("contracts:build", contractsConfig, compiledContracts);
try {
await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies);
}
catch (err) {
engine.logger.error(err);
}
console.dir("deployment done")
await engine.events.request2('pipeline:generateAll');

View File

@ -1,3 +1,5 @@
import {__} from 'embark-i18n';
const async = require('async');
const Web3 = require('web3');
const embarkJsUtils = require('embarkjs').Utils;
@ -7,6 +9,7 @@ class EthereumBlockchainClient {
constructor(embark, options) {
this.embark = embark;
this.events = embark.events;
this.logger = embark.logger;
this.embark.registerActionForEvent("deployment:contract:deployed", this.addContractJSONToPipeline.bind(this));
this.embark.registerActionForEvent('deployment:contract:beforeDeploy', this.determineArguments.bind(this));
@ -52,7 +55,8 @@ class EthereumBlockchainClient {
}, true, (err, receipt) => {
contract.deployedAddress = receipt.contractAddress;
contract.transactionHash = receipt.transactionHash;
done();
contract.log(`${contract.className.bold.cyan} ${__('deployed at').green} ${receipt.contractAddress.bold.cyan} ${__("using").green} ${receipt.gasUsed} ${__("gas").green} (txHash: ${receipt.transactionHash.bold.cyan})`);
done(err, receipt);
}, (hash) => {
console.dir('hash is ' + hash);
});