feat: introduce function support for deploy lifecycle hooks

Prior to this commits deployment lifecycle hooks had been defined as Array<string> due
to historical reasons (contracts.js) used to be a json file back in the days.

`deployIf`, `onDeploy` and `afterDeploy` can now be defined as (async)
function and have access to several dependencies such as contract instances and web3.
However, in order to have needed dependencies registered in the dependencies object,
all lifecycle hook dependencies need to be listed in the `deps` property
as shown below.

Also note that this is NOT a breaking change. Existing deployment lifecycle
hooks written in Array<string> style still work.

All three lifecycle hooks can now be defined as (async) functions and get an dependency
object with a shape like this:

```
interface DeploymentLifecycleHookDependencies {
  contracts: Map<string, ContractInstance>;
  web3: Web3Instance
}
```

`deployIf` lifecycle hook has to return a promise (or be defined using async/await and return
a value) like this:

```
contracts: {
  MyToken: {...},
  SimpleStorage: {
    deps: ['MyToken'], // this is needed to make `MyToken` available within `dependencies`
    deployIf: async (dependencies) => {
      return dependencies.contracts.MyToken_address;
    }
  },
}
```

Vanilla promises (instead of async/await) can be used as well:

```
contracts: {
  MyToken: {...},
  SimpleStorage: {
    deps: ['MyToken'],
    deployIf: (dependencies) => {
      return new Promise(resolve => resolve(dependencies.contracts.MyToken_address);
    }
  },
}
```

`onDeploy` as well, returns either a promise or is used using async/await:

```
contracts: {
  SimpleStorage: {
    onDeploy: async (dependencies) => {
      const simpleStorage = dependencies.contracts.SimpleStorage;
      const value = await simpleStorage.methods.get().call();
      console.log(value);
    }
  },
}
```

`afterDeploy` has automatically access to all configured and deployed contracts of the dapp:

```
contracts: {
  SimpleStorage: {...},
  MyToken: {...},
  afterDeploy: (dependencies) => {
    console.log('Done!');
  }
}
```

Closes #1029
This commit is contained in:
Pascal Precht 2018-11-19 07:53:34 +01:00 committed by Pascal Precht
parent 53191447f5
commit 8b68beca17
3 changed files with 139 additions and 52 deletions

View File

@ -330,14 +330,14 @@ Config.prototype.loadContractsConfigFile = function() {
});
}
if (onDeploy && onDeploy.length) {
if (Array.isArray(onDeploy)) {
newContractsConfig.contracts[contractName].onDeploy = onDeploy.map(replaceZeroAddressShorthand);
}
});
const afterDeploy = newContractsConfig.contracts.afterDeploy;
if (afterDeploy && afterDeploy.length) {
if (Array.isArray(afterDeploy)) {
newContractsConfig.contracts.afterDeploy = afterDeploy.map(replaceZeroAddressShorthand);
}

View File

@ -407,10 +407,15 @@ class ContractsManager {
for (className in self.contracts) {
contract = self.contracts[className];
self.contractDependencies[className] = self.contractDependencies[className] || [];
if (Array.isArray(contract.deps)) {
self.contractDependencies[className] = self.contractDependencies[className].concat(contract.deps);
}
// 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));
}
@ -427,14 +432,12 @@ class ContractsManager {
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));
}
@ -443,7 +446,7 @@ class ContractsManager {
}
// look in onDeploy for dependencies
if (contract.onDeploy !== [] && contract.onDeploy !== undefined) {
if (Array.isArray(contract.onDeploy)) {
let regex = /\$\w+/g;
contract.onDeploy.map((cmd) => {
if (cmd.indexOf('$accounts') > -1) {
@ -454,7 +457,6 @@ class ContractsManager {
// Contract self-referencing. In onDeploy, it should be available
return;
}
self.contractDependencies[className] = self.contractDependencies[className] || [];
self.contractDependencies[className].push(match.substr(1));
});
});

View File

@ -82,23 +82,33 @@ class SpecialConfigs {
registerAfterDeployAction() {
const self = this;
this.embark.registerActionForEvent("contracts:deploy:afterAll", (cb) => {
let afterDeployCmds = self.config.contractsConfig.afterDeploy || [];
async.mapLimit(afterDeployCmds, 1, (cmd, nextMapCb) => {
async.waterfall([
function replaceWithAddresses(next) {
self.replaceWithAddresses(cmd, next);
},
self.replaceWithENSAddress.bind(self)
], nextMapCb);
}, (err, onDeployCode) => {
if (err) {
self.logger.trace(err);
return cb(new Error("error running afterDeploy"));
this.embark.registerActionForEvent("contracts:deploy:afterAll", async (cb) => {
if (typeof self.config.contractsConfig.afterDeploy === 'function') {
try {
const dependencies = await this.getAfterDeployLifecycleHookDependencies();
await self.config.contractsConfig.afterDeploy(dependencies);
cb();
} catch (err) {
return cb(new Error(`Error registering afterDeploy lifecycle hook: ${err.message}`));
}
} else {
let afterDeployCmds = self.config.contractsConfig.afterDeploy || [];
async.mapLimit(afterDeployCmds, 1, (cmd, nextMapCb) => {
async.waterfall([
function replaceWithAddresses(next) {
self.replaceWithAddresses(cmd, next);
},
self.replaceWithENSAddress.bind(self)
], nextMapCb);
}, (err, onDeployCode) => {
if (err) {
self.logger.trace(err);
return cb(new Error("error running afterDeploy"));
}
self.runOnDeployCode(onDeployCode, cb);
});
self.runOnDeployCode(onDeployCode, cb);
});
}
});
}
@ -122,7 +132,7 @@ class SpecialConfigs {
registerOnDeployAction() {
const self = this;
this.embark.registerActionForEvent("deploy:contract:deployed", (params, cb) => {
this.embark.registerActionForEvent("deploy:contract:deployed", async (params, cb) => {
let contract = params.contract;
if (!contract.onDeploy || contract.deploy === false) {
@ -133,54 +143,129 @@ class SpecialConfigs {
self.logger.info(__('executing onDeploy commands'));
}
let onDeployCmds = contract.onDeploy;
async.mapLimit(onDeployCmds, 1, (cmd, nextMapCb) => {
async.waterfall([
function replaceWithAddresses(next) {
self.replaceWithAddresses(cmd, next);
},
self.replaceWithENSAddress.bind(self)
], (err, code) => {
if (err) {
self.logger.error(err.message || err);
return nextMapCb(); // Don't return error as we just skip the failing command
}
nextMapCb(null, code);
});
}, (err, onDeployCode) => {
if (err) {
return cb(new Error("error running onDeploy for " + contract.className.cyan));
if (typeof contract.onDeploy === 'function') {
try {
const dependencies = await this.getOnDeployLifecycleHookDependencies(contract);
await contract.onDeploy(dependencies);
cb();
} catch (err) {
return cb(new Error(`Error when registering onDeploy hook for ${contract.name}: ${err.message}`));
}
} else {
let onDeployCmds = contract.onDeploy;
async.mapLimit(onDeployCmds, 1, (cmd, nextMapCb) => {
async.waterfall([
function replaceWithAddresses(next) {
self.replaceWithAddresses(cmd, next);
},
self.replaceWithENSAddress.bind(self)
], (err, code) => {
if (err) {
self.logger.error(err.message || err);
return nextMapCb(); // Don't return error as we just skip the failing command
}
nextMapCb(null, code);
});
}, (err, onDeployCode) => {
if (err) {
return cb(new Error("error running onDeploy for " + contract.className.cyan));
}
self.runOnDeployCode(onDeployCode, cb, contract.silent);
});
self.runOnDeployCode(onDeployCode, cb, contract.silent);
});
}
});
}
registerDeployIfAction() {
const self = this;
self.embark.registerActionForEvent("deploy:contract:shouldDeploy", (params, cb) => {
self.embark.registerActionForEvent("deploy:contract:shouldDeploy", async (params, cb) => {
let cmd = params.contract.deployIf;
const contract = params.contract;
if (!cmd) {
return cb(params);
}
self.events.request('runcode:eval', cmd, (err, result) => {
if (err) {
self.logger.error(params.contract.className + ' deployIf directive has an error; contract will not deploy');
self.logger.error(err.message || err);
params.shouldDeploy = false;
} else if (!result) {
self.logger.info(params.contract.className + ' deployIf directive returned false; contract will not deploy');
params.shouldDeploy = false;
if (typeof cmd === 'function') {
try {
const dependencies = await this.getOnDeployLifecycleHookDependencies(contract);
params.shouldDeploy = await contract.deployIf(dependencies);
cb(params);
} catch (err) {
return cb(new Error(`Error when registering deployIf hook for ${contract.name}: ${err.message}`));
}
} else {
cb(params);
self.events.request('runcode:eval', cmd, (err, result) => {
if (err) {
self.logger.error(params.contract.className + ' deployIf directive has an error; contract will not deploy');
self.logger.error(err.message || err);
params.shouldDeploy = false;
} else if (!result) {
self.logger.info(params.contract.className + ' deployIf directive returned false; contract will not deploy');
params.shouldDeploy = false;
}
cb(params);
});
}
});
}
getOnDeployLifecycleHookDependencies(contractConfig) {
let dependencyNames = contractConfig.deps || [];
dependencyNames.push(contractConfig.className);
dependencyNames = [...new Set(dependencyNames)];
return new Promise((resolve, reject) => {
async.map(dependencyNames, (contractName, next) => {
this.events.request('contracts:contract', contractName, (contractRecipe) => {
if (!contractRecipe) {
next(new Error(`ReferredContractDoesNotExist: ${contractName}`));
}
this.events.request('blockchain:contract:create', {
abi: contractRecipe.abiDefinition,
address: contractRecipe.deployedAddress
}, contractInstance => {
next(null, { className: contractRecipe.className, instance: contractInstance });
});
});
}, (err, contractInstances) => {
if (err) {
reject(err);
}
this.events.request('blockchain:get', web3 => resolve(this.assembleLifecycleHookDependencies(contractInstances, web3)));
});
});
}
getAfterDeployLifecycleHookDependencies() {
return new Promise((resolve, reject) => {
this.events.request('contracts:list', (err, contracts) => {
async.map(contracts, (contract, next) => {
this.events.request('blockchain:contract:create', {
abi: contract.abiDefinition,
address: contract.deployedAddress
}, contractInstance => {
next(null, { className: contract.className, instance: contractInstance });
});
}, (err, contractInstances) => {
if (err) {
reject(err);
}
this.events.request('blockchain:get', web3 => resolve(this.assembleLifecycleHookDependencies(contractInstances, web3)));
});
});
});
}
assembleLifecycleHookDependencies(contractInstances, web3) {
return contractInstances.reduce((dependencies, contractInstance) => {
dependencies.contracts[contractInstance.className] = contractInstance.instance;
return dependencies;
}, { contracts: {}, web3 });
}
}
module.exports = SpecialConfigs;