From 8b68beca17566f8b7707b656eea6b87edd8a9957 Mon Sep 17 00:00:00 2001 From: Pascal Precht Date: Mon, 19 Nov 2018 07:53:34 +0100 Subject: [PATCH] feat: introduce function support for deploy lifecycle hooks Prior to this commits deployment lifecycle hooks had been defined as Array 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 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; 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 --- src/lib/core/config.js | 4 +- src/lib/modules/contracts_manager/index.js | 12 +- src/lib/modules/specialconfigs/index.js | 175 +++++++++++++++------ 3 files changed, 139 insertions(+), 52 deletions(-) diff --git a/src/lib/core/config.js b/src/lib/core/config.js index 32172d42c..e9dff55fa 100644 --- a/src/lib/core/config.js +++ b/src/lib/core/config.js @@ -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); } diff --git a/src/lib/modules/contracts_manager/index.js b/src/lib/modules/contracts_manager/index.js index c212a8ef6..b8f3f74da 100644 --- a/src/lib/modules/contracts_manager/index.js +++ b/src/lib/modules/contracts_manager/index.js @@ -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)); }); }); diff --git a/src/lib/modules/specialconfigs/index.js b/src/lib/modules/specialconfigs/index.js index 393dd0d59..fb039f0d5 100644 --- a/src/lib/modules/specialconfigs/index.js +++ b/src/lib/modules/specialconfigs/index.js @@ -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;