Skip to content

Commit 8b68bec

Browse files
committed
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
1 parent 5319144 commit 8b68bec

File tree

3 files changed

+137
-50
lines changed

3 files changed

+137
-50
lines changed

src/lib/core/config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,14 @@ Config.prototype.loadContractsConfigFile = function() {
330330
});
331331
}
332332

333-
if (onDeploy && onDeploy.length) {
333+
if (Array.isArray(onDeploy)) {
334334
newContractsConfig.contracts[contractName].onDeploy = onDeploy.map(replaceZeroAddressShorthand);
335335
}
336336
});
337337

338338
const afterDeploy = newContractsConfig.contracts.afterDeploy;
339339

340-
if (afterDeploy && afterDeploy.length) {
340+
if (Array.isArray(afterDeploy)) {
341341
newContractsConfig.contracts.afterDeploy = afterDeploy.map(replaceZeroAddressShorthand);
342342
}
343343

src/lib/modules/contracts_manager/index.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,15 @@ class ContractsManager {
407407
for (className in self.contracts) {
408408
contract = self.contracts[className];
409409

410+
self.contractDependencies[className] = self.contractDependencies[className] || [];
411+
412+
if (Array.isArray(contract.deps)) {
413+
self.contractDependencies[className] = self.contractDependencies[className].concat(contract.deps);
414+
}
415+
410416
// look in code for dependencies
411417
let libMatches = (contract.code.match(/:(.*?)(?=_)/g) || []);
412418
for (let match of libMatches) {
413-
self.contractDependencies[className] = self.contractDependencies[className] || [];
414419
self.contractDependencies[className].push(match.substr(1));
415420
}
416421

@@ -427,14 +432,12 @@ class ContractsManager {
427432
for (let j = 0; j < ref.length; j++) {
428433
let arg = ref[j];
429434
if (arg[0] === "$" && !arg.startsWith('$accounts')) {
430-
self.contractDependencies[className] = self.contractDependencies[className] || [];
431435
self.contractDependencies[className].push(arg.substr(1));
432436
self.checkDependency(className, arg.substr(1));
433437
}
434438
if (Array.isArray(arg)) {
435439
for (let sub_arg of arg) {
436440
if (sub_arg[0] === "$" && !sub_arg.startsWith('$accounts')) {
437-
self.contractDependencies[className] = self.contractDependencies[className] || [];
438441
self.contractDependencies[className].push(sub_arg.substr(1));
439442
self.checkDependency(className, sub_arg.substr(1));
440443
}
@@ -443,7 +446,7 @@ class ContractsManager {
443446
}
444447

445448
// look in onDeploy for dependencies
446-
if (contract.onDeploy !== [] && contract.onDeploy !== undefined) {
449+
if (Array.isArray(contract.onDeploy)) {
447450
let regex = /\$\w+/g;
448451
contract.onDeploy.map((cmd) => {
449452
if (cmd.indexOf('$accounts') > -1) {
@@ -454,7 +457,6 @@ class ContractsManager {
454457
// Contract self-referencing. In onDeploy, it should be available
455458
return;
456459
}
457-
self.contractDependencies[className] = self.contractDependencies[className] || [];
458460
self.contractDependencies[className].push(match.substr(1));
459461
});
460462
});

src/lib/modules/specialconfigs/index.js

Lines changed: 128 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,33 @@ class SpecialConfigs {
8282
registerAfterDeployAction() {
8383
const self = this;
8484

85-
this.embark.registerActionForEvent("contracts:deploy:afterAll", (cb) => {
86-
let afterDeployCmds = self.config.contractsConfig.afterDeploy || [];
87-
async.mapLimit(afterDeployCmds, 1, (cmd, nextMapCb) => {
88-
async.waterfall([
89-
function replaceWithAddresses(next) {
90-
self.replaceWithAddresses(cmd, next);
91-
},
92-
self.replaceWithENSAddress.bind(self)
93-
], nextMapCb);
94-
}, (err, onDeployCode) => {
95-
if (err) {
96-
self.logger.trace(err);
97-
return cb(new Error("error running afterDeploy"));
85+
this.embark.registerActionForEvent("contracts:deploy:afterAll", async (cb) => {
86+
if (typeof self.config.contractsConfig.afterDeploy === 'function') {
87+
try {
88+
const dependencies = await this.getAfterDeployLifecycleHookDependencies();
89+
await self.config.contractsConfig.afterDeploy(dependencies);
90+
cb();
91+
} catch (err) {
92+
return cb(new Error(`Error registering afterDeploy lifecycle hook: ${err.message}`));
9893
}
94+
} else {
95+
let afterDeployCmds = self.config.contractsConfig.afterDeploy || [];
96+
async.mapLimit(afterDeployCmds, 1, (cmd, nextMapCb) => {
97+
async.waterfall([
98+
function replaceWithAddresses(next) {
99+
self.replaceWithAddresses(cmd, next);
100+
},
101+
self.replaceWithENSAddress.bind(self)
102+
], nextMapCb);
103+
}, (err, onDeployCode) => {
104+
if (err) {
105+
self.logger.trace(err);
106+
return cb(new Error("error running afterDeploy"));
107+
}
99108

100-
self.runOnDeployCode(onDeployCode, cb);
101-
});
109+
self.runOnDeployCode(onDeployCode, cb);
110+
});
111+
}
102112
});
103113
}
104114

@@ -122,7 +132,7 @@ class SpecialConfigs {
122132
registerOnDeployAction() {
123133
const self = this;
124134

125-
this.embark.registerActionForEvent("deploy:contract:deployed", (params, cb) => {
135+
this.embark.registerActionForEvent("deploy:contract:deployed", async (params, cb) => {
126136
let contract = params.contract;
127137

128138
if (!contract.onDeploy || contract.deploy === false) {
@@ -133,54 +143,129 @@ class SpecialConfigs {
133143
self.logger.info(__('executing onDeploy commands'));
134144
}
135145

136-
let onDeployCmds = contract.onDeploy;
137-
async.mapLimit(onDeployCmds, 1, (cmd, nextMapCb) => {
138-
async.waterfall([
139-
function replaceWithAddresses(next) {
140-
self.replaceWithAddresses(cmd, next);
141-
},
142-
self.replaceWithENSAddress.bind(self)
143-
], (err, code) => {
146+
if (typeof contract.onDeploy === 'function') {
147+
try {
148+
const dependencies = await this.getOnDeployLifecycleHookDependencies(contract);
149+
await contract.onDeploy(dependencies);
150+
cb();
151+
} catch (err) {
152+
return cb(new Error(`Error when registering onDeploy hook for ${contract.name}: ${err.message}`));
153+
}
154+
} else {
155+
let onDeployCmds = contract.onDeploy;
156+
async.mapLimit(onDeployCmds, 1, (cmd, nextMapCb) => {
157+
async.waterfall([
158+
function replaceWithAddresses(next) {
159+
self.replaceWithAddresses(cmd, next);
160+
},
161+
self.replaceWithENSAddress.bind(self)
162+
], (err, code) => {
163+
if (err) {
164+
self.logger.error(err.message || err);
165+
return nextMapCb(); // Don't return error as we just skip the failing command
166+
}
167+
nextMapCb(null, code);
168+
});
169+
}, (err, onDeployCode) => {
144170
if (err) {
145-
self.logger.error(err.message || err);
146-
return nextMapCb(); // Don't return error as we just skip the failing command
171+
return cb(new Error("error running onDeploy for " + contract.className.cyan));
147172
}
148-
nextMapCb(null, code);
149-
});
150-
}, (err, onDeployCode) => {
151-
if (err) {
152-
return cb(new Error("error running onDeploy for " + contract.className.cyan));
153-
}
154173

155-
self.runOnDeployCode(onDeployCode, cb, contract.silent);
156-
});
174+
self.runOnDeployCode(onDeployCode, cb, contract.silent);
175+
});
176+
}
157177
});
158178
}
159179

160180
registerDeployIfAction() {
161181
const self = this;
162182

163-
self.embark.registerActionForEvent("deploy:contract:shouldDeploy", (params, cb) => {
183+
self.embark.registerActionForEvent("deploy:contract:shouldDeploy", async (params, cb) => {
164184
let cmd = params.contract.deployIf;
185+
const contract = params.contract;
165186
if (!cmd) {
166187
return cb(params);
167188
}
168189

169-
self.events.request('runcode:eval', cmd, (err, result) => {
190+
if (typeof cmd === 'function') {
191+
try {
192+
const dependencies = await this.getOnDeployLifecycleHookDependencies(contract);
193+
params.shouldDeploy = await contract.deployIf(dependencies);
194+
cb(params);
195+
} catch (err) {
196+
return cb(new Error(`Error when registering deployIf hook for ${contract.name}: ${err.message}`));
197+
}
198+
} else {
199+
200+
self.events.request('runcode:eval', cmd, (err, result) => {
201+
if (err) {
202+
self.logger.error(params.contract.className + ' deployIf directive has an error; contract will not deploy');
203+
self.logger.error(err.message || err);
204+
params.shouldDeploy = false;
205+
} else if (!result) {
206+
self.logger.info(params.contract.className + ' deployIf directive returned false; contract will not deploy');
207+
params.shouldDeploy = false;
208+
}
209+
210+
cb(params);
211+
});
212+
}
213+
});
214+
}
215+
216+
getOnDeployLifecycleHookDependencies(contractConfig) {
217+
let dependencyNames = contractConfig.deps || [];
218+
dependencyNames.push(contractConfig.className);
219+
dependencyNames = [...new Set(dependencyNames)];
220+
221+
return new Promise((resolve, reject) => {
222+
async.map(dependencyNames, (contractName, next) => {
223+
this.events.request('contracts:contract', contractName, (contractRecipe) => {
224+
if (!contractRecipe) {
225+
next(new Error(`ReferredContractDoesNotExist: ${contractName}`));
226+
}
227+
this.events.request('blockchain:contract:create', {
228+
abi: contractRecipe.abiDefinition,
229+
address: contractRecipe.deployedAddress
230+
}, contractInstance => {
231+
next(null, { className: contractRecipe.className, instance: contractInstance });
232+
});
233+
});
234+
}, (err, contractInstances) => {
170235
if (err) {
171-
self.logger.error(params.contract.className + ' deployIf directive has an error; contract will not deploy');
172-
self.logger.error(err.message || err);
173-
params.shouldDeploy = false;
174-
} else if (!result) {
175-
self.logger.info(params.contract.className + ' deployIf directive returned false; contract will not deploy');
176-
params.shouldDeploy = false;
236+
reject(err);
177237
}
238+
this.events.request('blockchain:get', web3 => resolve(this.assembleLifecycleHookDependencies(contractInstances, web3)));
239+
});
240+
});
241+
}
178242

179-
cb(params);
243+
getAfterDeployLifecycleHookDependencies() {
244+
return new Promise((resolve, reject) => {
245+
this.events.request('contracts:list', (err, contracts) => {
246+
async.map(contracts, (contract, next) => {
247+
this.events.request('blockchain:contract:create', {
248+
abi: contract.abiDefinition,
249+
address: contract.deployedAddress
250+
}, contractInstance => {
251+
next(null, { className: contract.className, instance: contractInstance });
252+
});
253+
}, (err, contractInstances) => {
254+
if (err) {
255+
reject(err);
256+
}
257+
this.events.request('blockchain:get', web3 => resolve(this.assembleLifecycleHookDependencies(contractInstances, web3)));
258+
});
180259
});
181260
});
182261
}
183262

263+
assembleLifecycleHookDependencies(contractInstances, web3) {
264+
return contractInstances.reduce((dependencies, contractInstance) => {
265+
dependencies.contracts[contractInstance.className] = contractInstance.instance;
266+
return dependencies;
267+
}, { contracts: {}, web3 });
268+
}
184269
}
185270

186271
module.exports = SpecialConfigs;

0 commit comments

Comments
 (0)