Skip to content

Commit c9e445e

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 cabfa93 commit c9e445e

File tree

3 files changed

+141
-50
lines changed

3 files changed

+141
-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: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,17 @@ 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+
contract.deps.forEach(name => {
414+
self.contractDependencies[className].push(name);
415+
});
416+
}
417+
410418
// look in code for dependencies
411419
let libMatches = (contract.code.match(/:(.*?)(?=_)/g) || []);
412420
for (let match of libMatches) {
413-
self.contractDependencies[className] = self.contractDependencies[className] || [];
414421
self.contractDependencies[className].push(match.substr(1));
415422
}
416423

@@ -427,14 +434,12 @@ class ContractsManager {
427434
for (let j = 0; j < ref.length; j++) {
428435
let arg = ref[j];
429436
if (arg[0] === "$" && !arg.startsWith('$accounts')) {
430-
self.contractDependencies[className] = self.contractDependencies[className] || [];
431437
self.contractDependencies[className].push(arg.substr(1));
432438
self.checkDependency(className, arg.substr(1));
433439
}
434440
if (Array.isArray(arg)) {
435441
for (let sub_arg of arg) {
436442
if (sub_arg[0] === "$" && !sub_arg.startsWith('$accounts')) {
437-
self.contractDependencies[className] = self.contractDependencies[className] || [];
438443
self.contractDependencies[className].push(sub_arg.substr(1));
439444
self.checkDependency(className, sub_arg.substr(1));
440445
}
@@ -443,7 +448,7 @@ class ContractsManager {
443448
}
444449

445450
// look in onDeploy for dependencies
446-
if (contract.onDeploy !== [] && contract.onDeploy !== undefined) {
451+
if (Array.isArray(contract.onDeploy)) {
447452
let regex = /\$\w+/g;
448453
contract.onDeploy.map((cmd) => {
449454
if (cmd.indexOf('$accounts') > -1) {
@@ -454,7 +459,6 @@ class ContractsManager {
454459
// Contract self-referencing. In onDeploy, it should be available
455460
return;
456461
}
457-
self.contractDependencies[className] = self.contractDependencies[className] || [];
458462
self.contractDependencies[className].push(match.substr(1));
459463
});
460464
});

src/lib/modules/specialconfigs/index.js

Lines changed: 130 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,131 @@ 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+
156+
let onDeployCmds = contract.onDeploy;
157+
async.mapLimit(onDeployCmds, 1, (cmd, nextMapCb) => {
158+
async.waterfall([
159+
function replaceWithAddresses(next) {
160+
self.replaceWithAddresses(cmd, next);
161+
},
162+
self.replaceWithENSAddress.bind(self)
163+
], (err, code) => {
164+
if (err) {
165+
self.logger.error(err.message || err);
166+
return nextMapCb(); // Don't return error as we just skip the failing command
167+
}
168+
nextMapCb(null, code);
169+
});
170+
}, (err, onDeployCode) => {
144171
if (err) {
145-
self.logger.error(err.message || err);
146-
return nextMapCb(); // Don't return error as we just skip the failing command
172+
return cb(new Error("error running onDeploy for " + contract.className.cyan));
147173
}
148-
nextMapCb(null, code);
149-
});
150-
}, (err, onDeployCode) => {
151-
if (err) {
152-
return cb(new Error("error running onDeploy for " + contract.className.cyan));
153-
}
154174

155-
self.runOnDeployCode(onDeployCode, cb, contract.silent);
156-
});
175+
self.runOnDeployCode(onDeployCode, cb, contract.silent);
176+
});
177+
}
157178
});
158179
}
159180

160181
registerDeployIfAction() {
161182
const self = this;
162183

163-
self.embark.registerActionForEvent("deploy:contract:shouldDeploy", (params, cb) => {
184+
self.embark.registerActionForEvent("deploy:contract:shouldDeploy", async (params, cb) => {
164185
let cmd = params.contract.deployIf;
186+
const contract = params.contract;
165187
if (!cmd) {
166188
return cb(params);
167189
}
168190

169-
self.events.request('runcode:eval', cmd, (err, result) => {
191+
if (typeof cmd === 'function') {
192+
try {
193+
const dependencies = await this.getOnDeployLifecycleHookDependencies(contract);
194+
params.shouldDeploy = await contract.deployIf(dependencies);
195+
cb(params);
196+
} catch (err) {
197+
return cb(new Error(`Error when registering deployIf hook for ${contract.name}: ${err.message}`));
198+
}
199+
} else {
200+
201+
self.events.request('runcode:eval', cmd, (err, result) => {
202+
if (err) {
203+
self.logger.error(params.contract.className + ' deployIf directive has an error; contract will not deploy');
204+
self.logger.error(err.message || err);
205+
params.shouldDeploy = false;
206+
} else if (!result) {
207+
self.logger.info(params.contract.className + ' deployIf directive returned false; contract will not deploy');
208+
params.shouldDeploy = false;
209+
}
210+
211+
cb(params);
212+
});
213+
}
214+
});
215+
}
216+
217+
getOnDeployLifecycleHookDependencies(contractConfig) {
218+
let dependencyNames = contractConfig.deps || [];
219+
dependencyNames.push(contractConfig.className);
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'));
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+
let dependencies = { contracts: {}, web3 };
265+
266+
contractInstances.forEach(contractInstance => {
267+
dependencies.contracts[contractInstance.className] = contractInstance.instance;
268+
});
269+
return dependencies;
270+
}
184271
}
185272

186273
module.exports = SpecialConfigs;

0 commit comments

Comments
 (0)