Skip to content

Commit

Permalink
feat: introduce function support for deploy lifecycle hooks
Browse files Browse the repository at this point in the history
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
  • Loading branch information
0x-r4bbit committed Nov 20, 2018
1 parent 5319144 commit 8b68bec
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 50 deletions.
4 changes: 2 additions & 2 deletions src/lib/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
12 changes: 7 additions & 5 deletions src/lib/modules/contracts_manager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand All @@ -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));
}
Expand All @@ -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) {
Expand All @@ -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));
});
});
Expand Down
171 changes: 128 additions & 43 deletions src/lib/modules/specialconfigs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
});
}

Expand All @@ -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) {
Expand All @@ -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 (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) {
self.logger.error(err.message || err);
return nextMapCb(); // Don't return error as we just skip the failing command
return cb(new Error("error running onDeploy for " + contract.className.cyan));
}
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 (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 {

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) {
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;
reject(err);
}
this.events.request('blockchain:get', web3 => resolve(this.assembleLifecycleHookDependencies(contractInstances, web3)));
});
});
}

cb(params);
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;

0 comments on commit 8b68bec

Please sign in to comment.