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); newContractsConfig.contracts[contractName].onDeploy = onDeploy.map(replaceZeroAddressShorthand);
} }
}); });
const afterDeploy = newContractsConfig.contracts.afterDeploy; const afterDeploy = newContractsConfig.contracts.afterDeploy;
if (afterDeploy && afterDeploy.length) { if (Array.isArray(afterDeploy)) {
newContractsConfig.contracts.afterDeploy = afterDeploy.map(replaceZeroAddressShorthand); newContractsConfig.contracts.afterDeploy = afterDeploy.map(replaceZeroAddressShorthand);
} }

View File

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

View File

@ -82,23 +82,33 @@ class SpecialConfigs {
registerAfterDeployAction() { registerAfterDeployAction() {
const self = this; const self = this;
this.embark.registerActionForEvent("contracts:deploy:afterAll", (cb) => { this.embark.registerActionForEvent("contracts:deploy:afterAll", async (cb) => {
let afterDeployCmds = self.config.contractsConfig.afterDeploy || []; if (typeof self.config.contractsConfig.afterDeploy === 'function') {
async.mapLimit(afterDeployCmds, 1, (cmd, nextMapCb) => { try {
async.waterfall([ const dependencies = await this.getAfterDeployLifecycleHookDependencies();
function replaceWithAddresses(next) { await self.config.contractsConfig.afterDeploy(dependencies);
self.replaceWithAddresses(cmd, next); cb();
}, } catch (err) {
self.replaceWithENSAddress.bind(self) return cb(new Error(`Error registering afterDeploy lifecycle hook: ${err.message}`));
], nextMapCb);
}, (err, onDeployCode) => {
if (err) {
self.logger.trace(err);
return cb(new Error("error running afterDeploy"));
} }
} 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() { registerOnDeployAction() {
const self = this; const self = this;
this.embark.registerActionForEvent("deploy:contract:deployed", (params, cb) => { this.embark.registerActionForEvent("deploy:contract:deployed", async (params, cb) => {
let contract = params.contract; let contract = params.contract;
if (!contract.onDeploy || contract.deploy === false) { if (!contract.onDeploy || contract.deploy === false) {
@ -133,54 +143,129 @@ class SpecialConfigs {
self.logger.info(__('executing onDeploy commands')); self.logger.info(__('executing onDeploy commands'));
} }
let onDeployCmds = contract.onDeploy; if (typeof contract.onDeploy === 'function') {
async.mapLimit(onDeployCmds, 1, (cmd, nextMapCb) => { try {
async.waterfall([ const dependencies = await this.getOnDeployLifecycleHookDependencies(contract);
function replaceWithAddresses(next) { await contract.onDeploy(dependencies);
self.replaceWithAddresses(cmd, next); cb();
}, } catch (err) {
self.replaceWithENSAddress.bind(self) return cb(new Error(`Error when registering onDeploy hook for ${contract.name}: ${err.message}`));
], (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));
} }
} 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() { registerDeployIfAction() {
const self = this; 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; let cmd = params.contract.deployIf;
const contract = params.contract;
if (!cmd) { if (!cmd) {
return cb(params); return cb(params);
} }
self.events.request('runcode:eval', cmd, (err, result) => { if (typeof cmd === 'function') {
if (err) { try {
self.logger.error(params.contract.className + ' deployIf directive has an error; contract will not deploy'); const dependencies = await this.getOnDeployLifecycleHookDependencies(contract);
self.logger.error(err.message || err); params.shouldDeploy = await contract.deployIf(dependencies);
params.shouldDeploy = false; cb(params);
} else if (!result) { } catch (err) {
self.logger.info(params.contract.className + ' deployIf directive returned false; contract will not deploy'); return cb(new Error(`Error when registering deployIf hook for ${contract.name}: ${err.message}`));
params.shouldDeploy = false;
} }
} 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; module.exports = SpecialConfigs;