contract_deployer.js
1 let async = require('async'); 2 //require("../utils/debug_util.js")(__filename, async); 3 let utils = require('../../utils/utils.js'); 4 5 class ContractDeployer { 6 constructor(options) { 7 const self = this; 8 this.logger = options.logger; 9 this.events = options.events; 10 this.plugins = options.plugins; 11 12 self.events.setCommandHandler('deploy:contract', (contract, cb) => { 13 self.checkAndDeployContract(contract, null, cb); 14 }); 15 } 16 17 // TODO: determining the arguments could also be in a module since it's not 18 // part of ta 'normal' contract deployment 19 determineArguments(suppliedArgs, contract, accounts, callback) { 20 const self = this; 21 22 let args = suppliedArgs; 23 if (!Array.isArray(args)) { 24 args = []; 25 let abi = contract.abiDefinition.find((abi) => abi.type === 'constructor'); 26 27 for (let input of abi.inputs) { 28 let inputValue = suppliedArgs[input.name]; 29 if (!inputValue) { 30 this.logger.error(__("{{inputName}} has not been defined for {{className}} constructor", {inputName: input.name, className: contract.className})); 31 } 32 args.push(inputValue || ""); 33 } 34 } 35 36 function parseArg(arg, cb) { 37 const match = arg.match(/\$accounts\[([0-9]+)]/); 38 if (match) { 39 if (!accounts[match[1]]) { 40 return cb(__('No corresponding account at index %d', match[1])); 41 } 42 return cb(null, accounts[match[1]]); 43 } 44 let contractName = arg.substr(1); 45 self.events.request('contracts:contract', contractName, (referedContract) => { 46 // Because we're referring to a contract that is not being deployed (ie. an interface), 47 // we still need to provide a valid address so that the ABI checker won't fail. 48 cb(null, (referedContract.deployedAddress || '0x0000000000000000000000000000000000000000')); 49 }); 50 } 51 52 async.map(args, (arg, nextEachCb) => { 53 if (arg[0] === "$") { 54 parseArg(arg, nextEachCb); 55 } else if (Array.isArray(arg)) { 56 async.map(arg, (sub_arg, nextSubEachCb) => { 57 if (sub_arg[0] === "$") { 58 parseArg(sub_arg, nextSubEachCb); 59 } else if(typeof sub_arg === 'string' && sub_arg.indexOf('.eth') === sub_arg.length - 4) { 60 self.events.request("ens:resolve", sub_arg, (err, name) => { 61 if(err) { 62 return nextSubEachCb(err); 63 } 64 return nextSubEachCb(err, name); 65 }); 66 } else { 67 nextSubEachCb(null, sub_arg); 68 } 69 }, (err, subRealArgs) => { 70 nextEachCb(null, subRealArgs); 71 }); 72 } else if(typeof arg === 'string' && arg.indexOf('.eth') === arg.length - 4) { 73 self.events.request("ens:resolve", arg, (err, name) => { 74 if(err) { 75 return nextEachCb(err); 76 } 77 return nextEachCb(err, name); 78 }); 79 } else { 80 nextEachCb(null, arg); 81 } 82 }, callback); 83 } 84 85 checkAndDeployContract(contract, params, callback) { 86 let self = this; 87 contract.error = false; 88 let accounts = []; 89 let deploymentAccount; 90 91 if (contract.deploy === false) { 92 self.events.emit("deploy:contract:undeployed", contract); 93 return callback(); 94 } 95 96 async.waterfall([ 97 function requestBlockchainConnector(callback) { 98 self.events.request("blockchain:object", (blockchain) => { 99 self.blockchain = blockchain; 100 callback(); 101 }); 102 }, 103 104 // TODO: can potentially go to a beforeDeploy plugin 105 function getAccounts(next) { 106 deploymentAccount = self.blockchain.defaultAccount(); 107 self.blockchain.getAccounts(function (err, _accounts) { 108 if (err) { 109 return next(new Error(err)); 110 } 111 accounts = _accounts; 112 113 // applying deployer account configuration, if any 114 if (typeof contract.fromIndex === 'number') { 115 deploymentAccount = accounts[contract.fromIndex]; 116 if (deploymentAccount === undefined) { 117 return next(__("error deploying") + " " + contract.className + ": " + __("no account found at index") + " " + contract.fromIndex + __(" check the config")); 118 } 119 } 120 if (typeof contract.from === 'string' && typeof contract.fromIndex !== 'undefined') { 121 self.logger.warn(__('Both "from" and "fromIndex" are defined for contract') + ' "' + contract.className + '". ' + __('Using "from" as deployer account.')); 122 } 123 if (typeof contract.from === 'string') { 124 deploymentAccount = contract.from; 125 } 126 127 deploymentAccount = deploymentAccount || accounts[0]; 128 contract.deploymentAccount = deploymentAccount; 129 next(); 130 }); 131 }, 132 function applyArgumentPlugins(next) { 133 self.plugins.emitAndRunActionsForEvent('deploy:contract:arguments', {contract: contract}, (_params) => { 134 next(); 135 }); 136 }, 137 function _determineArguments(next) { 138 self.determineArguments(params || contract.args, contract, accounts, (err, realArgs) => { 139 if (err) { 140 return next(err); 141 } 142 contract.realArgs = realArgs; 143 next(); 144 }); 145 }, 146 function deployIt(next) { 147 let skipBytecodeCheck = false; 148 if (contract.address !== undefined) { 149 try { 150 utils.toChecksumAddress(contract.address); 151 } catch(e) { 152 self.logger.error(__("error deploying %s", contract.className)); 153 self.logger.error(e.message); 154 contract.error = e.message; 155 self.events.emit("deploy:contract:error", contract); 156 return next(e.message); 157 } 158 contract.deployedAddress = contract.address; 159 skipBytecodeCheck = true; 160 } 161 162 self.plugins.emitAndRunActionsForEvent('deploy:contract:shouldDeploy', {contract: contract, shouldDeploy: true}, function(_err, params) { 163 let trackedContract = params.contract; 164 if (!params.shouldDeploy) { 165 return self.willNotDeployContract(contract, trackedContract, next); 166 } 167 if (!trackedContract.address) { 168 return self.deployContract(contract, next); 169 } 170 // deploy the contract regardless if track field is defined and set to false 171 if (trackedContract.track === false) { 172 self.logFunction(contract)(contract.className.bold.cyan + __(" will be redeployed").green); 173 return self.deployContract(contract, next); 174 } 175 176 self.blockchain.getCode(trackedContract.address, function(_getCodeErr, codeInChain) { 177 if (codeInChain.length > 3 || skipBytecodeCheck) { // it is "0x" or "0x0" for empty code, depending on web3 version 178 self.contractAlreadyDeployed(contract, trackedContract, next); 179 } else { 180 self.deployContract(contract, next); 181 } 182 }); 183 }); 184 } 185 ], callback); 186 } 187 188 willNotDeployContract(contract, trackedContract, callback) { 189 contract.deploy = false; 190 this.events.emit("deploy:contract:undeployed", contract); 191 callback(); 192 } 193 194 contractAlreadyDeployed(contract, trackedContract, callback) { 195 const self = this; 196 this.logFunction(contract)(contract.className.bold.cyan + __(" already deployed at ").green + trackedContract.address.bold.cyan); 197 contract.deployedAddress = trackedContract.address; 198 self.events.emit("deploy:contract:deployed", contract); 199 200 // TODO: can be moved into a afterDeploy event 201 // just need to figure out the gasLimit coupling issue 202 self.events.request('code-generator:contract:vanilla', contract, contract._gasLimit || false, (contractCode) => { 203 self.events.request('runcode:eval', contractCode, () => {}, true); 204 return callback(); 205 }); 206 } 207 208 logFunction(contract) { 209 return contract.silent ? this.logger.trace.bind(this.logger) : this.logger.info.bind(this.logger); 210 } 211 212 deployContract(contract, callback) { 213 let self = this; 214 let deployObject; 215 216 async.waterfall([ 217 function doLinking(next) { 218 let contractCode = contract.code; 219 self.events.request('contracts:list', (_err, contracts) => { 220 for (let contractObj of contracts) { 221 let filename = contractObj.filename; 222 let deployedAddress = contractObj.deployedAddress; 223 if (deployedAddress) { 224 deployedAddress = deployedAddress.substr(2); 225 } 226 let linkReference = '__' + filename + ":" + contractObj.className; 227 if (contractCode.indexOf(linkReference.substr(0, 38)) < 0) { // substr to simulate the cut that solc does 228 continue; 229 } 230 if (linkReference.length > 40) { 231 return next(new Error(__("{{linkReference}} is too long, try reducing the path of the contract ({{filename}}) and/or its name {{contractName}}", {linkReference: linkReference, filename: filename, contractName: contractObj.className}))); 232 } 233 let toReplace = linkReference + "_".repeat(40 - linkReference.length); 234 if (deployedAddress === undefined) { 235 let libraryName = contractObj.className; 236 return next(new Error(__("{{contractName}} needs {{libraryName}} but an address was not found, did you deploy it or configured an address?", {contractName: contract.className, libraryName: libraryName}))); 237 } 238 contractCode = contractCode.replace(new RegExp(toReplace, "g"), deployedAddress); 239 } 240 // saving code changes back to the contract object 241 contract.code = contractCode; 242 self.events.request('contracts:setBytecode', contract.className, contractCode); 243 next(); 244 }); 245 }, 246 function applyBeforeDeploy(next) { 247 self.plugins.emitAndRunActionsForEvent('deploy:contract:beforeDeploy', {contract: contract}, (_params) => { 248 next(); 249 }); 250 }, 251 function getGasPriceForNetwork(next) { 252 self.events.request("blockchain:gasPrice", (err, gasPrice) => { 253 if (err) { 254 return next(new Error(__("could not get the gas price"))); 255 } 256 contract.gasPrice = contract.gasPrice || gasPrice; 257 next(); 258 }); 259 }, 260 function createDeployObject(next) { 261 let contractCode = contract.code; 262 let contractObject = self.blockchain.ContractObject({abi: contract.abiDefinition}); 263 let contractParams = (contract.realArgs || contract.args).slice(); 264 265 try { 266 const dataCode = contractCode.startsWith('0x') ? contractCode : "0x" + contractCode; 267 deployObject = self.blockchain.deployContractObject(contractObject, {arguments: contractParams, data: dataCode}); 268 } catch(e) { 269 if (e.message.indexOf('Invalid number of parameters for "undefined"') >= 0) { 270 return next(new Error(__("attempted to deploy %s without specifying parameters", contract.className)) + ". " + __("check if there are any params defined for this contract in this environment in the contracts configuration file")); 271 } 272 return next(new Error(e)); 273 } 274 next(); 275 }, 276 function estimateCorrectGas(next) { 277 if (contract.gas === 'auto') { 278 return self.blockchain.estimateDeployContractGas(deployObject, (err, gasValue) => { 279 if (err) { 280 return next(err); 281 } 282 let increase_per = 1 + (Math.random() / 10.0); 283 contract.gas = Math.floor(gasValue * increase_per); 284 next(); 285 }); 286 } 287 next(); 288 }, 289 function deployTheContract(next) { 290 let estimatedCost = contract.gas * contract.gasPrice; 291 self.logFunction(contract)(__("deploying") + " " + contract.className.bold.cyan + " " + __("with").green + " " + contract.gas + " " + __("gas at the price of").green + " " + contract.gasPrice + " " + __("Wei, estimated cost:").green + " " + estimatedCost + " Wei".green); 292 293 self.blockchain.deployContractFromObject(deployObject, { 294 from: contract.deploymentAccount, 295 gas: contract.gas, 296 gasPrice: contract.gasPrice 297 }, function(error, receipt) { 298 if (error) { 299 contract.error = error.message; 300 self.events.emit("deploy:contract:error", contract); 301 if (error.message && error.message.indexOf('replacement transaction underpriced') !== -1) { 302 self.logger.warn("replacement transaction underpriced: This warning typically means a transaction exactly like this one is still pending on the blockchain"); 303 } 304 return next(new Error("error deploying =" + contract.className + "= due to error: " + error.message)); 305 } 306 self.logFunction(contract)(contract.className.bold.cyan + " " + __("deployed at").green + " " + receipt.contractAddress.bold.cyan + " " + __("using").green + " " + receipt.gasUsed + " " + __("gas").green); 307 contract.deployedAddress = receipt.contractAddress; 308 contract.transactionHash = receipt.transactionHash; 309 receipt.className = contract.className; 310 self.events.emit("deploy:contract:receipt", receipt); 311 self.events.emit("deploy:contract:deployed", contract); 312 313 // TODO: can be moved into a afterDeploy event 314 // just need to figure out the gasLimit coupling issue 315 self.events.request('code-generator:contract:vanilla', contract, contract._gasLimit || false, (contractCode) => { 316 self.events.request('runcode:eval', contractCode, () => {}, true); 317 self.plugins.runActionsForEvent('deploy:contract:deployed', {contract: contract}, () => { 318 return next(null, receipt); 319 }); 320 }); 321 }); 322 } 323 ], callback); 324 } 325 326 } 327 328 module.exports = ContractDeployer;