mirror of https://github.com/embarklabs/embark.git
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:
parent
53191447f5
commit
8b68beca17
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue