diff --git a/packages/embark-contracts-manager/package.json b/packages/embark-contracts-manager/package.json index 9d09f93d9..402ec6e0f 100644 --- a/packages/embark-contracts-manager/package.json +++ b/packages/embark-contracts-manager/package.json @@ -26,21 +26,20 @@ }, "main": "./dist/index.js", "scripts": { - "build": "cross-env BABEL_ENV=node babel src --extensions \".js\" --out-dir dist --root-mode upward --source-maps", + "build": "cross-env BABEL_ENV=node babel src --extensions \".js,.ts\" --out-dir dist --root-mode upward --source-maps", "ci": "npm run qa", "clean": "npm run reset", "lint": "npm-run-all lint:*", "lint:js": "eslint src/", - "// lint:ts": "tslint -c tslint.json \"src/**/*.ts\"", + "lint:ts": "tslint -c tslint.json \"src/**/*.ts\"", "package": "npm pack", - "// qa": "npm-run-all lint typecheck build package", - "qa": "npm-run-all lint build package", + "qa": "npm-run-all lint typecheck build package", "reset": "npx rimraf dist embark-*.tgz package", "start": "npm run watch", - "// typecheck": "tsc", + "typecheck": "tsc", "watch": "run-p watch:*", "watch:build": "npm run build -- --verbose --watch", - "// watch:typecheck": "npm run typecheck -- --preserveWatchOutput --watch" + "watch:typecheck": "npm run typecheck -- --preserveWatchOutput --watch" }, "eslintConfig": { "extends": "../../.eslintrc.json" @@ -68,4 +67,4 @@ "npm": ">=6.4.1", "yarn": ">=1.12.3" } -} +} \ No newline at end of file diff --git a/packages/embark-contracts-manager/src/contract.ts b/packages/embark-contracts-manager/src/contract.ts new file mode 100644 index 000000000..4c1724152 --- /dev/null +++ b/packages/embark-contracts-manager/src/contract.ts @@ -0,0 +1,49 @@ +import { ContractConfig, Logger } from "embark"; +const { sha3 } = require("embark-utils"); +import { ABIDefinition } from "web3/eth/abi"; + +export default class Contract { + private logger: Logger; + public abiDefinition?: ABIDefinition[]; + public deployedAddress?: string; + public className: string = ""; + public address?: string; + public args?: any[] = []; + public instanceOf?: string; + public gas?: number; + public gasPrice?: number; + public silent?: boolean = false; + public track?: boolean = true; + public deploy?: boolean = true; + public realRuntimeBytecode: string = ""; + public realArgs: any[] = []; + constructor(logger: Logger, contractConfig: ContractConfig) { + this.logger = logger; + this.address = contractConfig.address; + this.args = contractConfig.args; + this.instanceOf = contractConfig.instanceOf; + this.gas = contractConfig.gas; + this.gasPrice = contractConfig.gasPrice; + this.silent = contractConfig.silent; + this.track = contractConfig.track; + this.deploy = contractConfig.deploy; + } + + /** + * Calculates a hash from runtime bytecode, classname, and deploy arguments. + * Used for uniquely identifying a contract, ie in chains.json. + */ + get hash() { + return sha3(this.realRuntimeBytecode + this.className + (this.realArgs || this.args).join(",")); + } + + /** + * Logs a message to the console. Logs with loglevel trace if contract has it's silent + * property set (in the config or internally, ie ENS contracts). Otherwise, logs with + * info log level. + * @param {string} message message to log to the console + */ + public log(message: string) { + this.silent ? this.logger.trace(message) : this.logger.info(message); + } +} diff --git a/packages/embark-contracts-manager/src/index.js b/packages/embark-contracts-manager/src/index.js index de7a91218..b9a6e41f5 100644 --- a/packages/embark-contracts-manager/src/index.js +++ b/packages/embark-contracts-manager/src/index.js @@ -1,7 +1,8 @@ -import { __ } from 'embark-i18n'; +import {__} from 'embark-i18n'; +import Contract from './contract'; const async = require('async'); const constants = require('embark-core/constants'); -const { dappPath, proposeAlternative, toposort } = require('embark-utils'); +const {dappPath, proposeAlternative, toposort} = require('embark-utils'); class ContractsManager { constructor(embark, options) { @@ -123,7 +124,7 @@ class ContractsManager { self.events.request("blockchain:contract:create", {abi: contract.abiDefinition, address: contract.deployedAddress}, async (contractObj) => { try { let value = typeof req.body.value === "number" ? req.body.value.toString() : req.body.value; - const gas = await contractObj.methods[req.body.method].apply(this, req.body.inputs).estimateGas({ value }); + const gas = await contractObj.methods[req.body.method].apply(this, req.body.inputs).estimateGas({value}); contractObj.methods[req.body.method].apply(this, req.body.inputs)[funcCall]({ from: account, gasPrice: req.body.gasPrice, @@ -148,7 +149,7 @@ class ContractsManager { return res.send({result: error.message}); } - if(funcCall === 'call') { + if (funcCall === 'call') { contractLog.status = '0x1'; return res.send({result}); } @@ -223,13 +224,13 @@ class ContractsManager { '/embark-api/contract/deploy', (req, res) => { this.logger.trace(`POST request /embark-api/contract/deploy:\n ${JSON.stringify(req.body)}`); - if(typeof req.body.compiledContract !== 'object'){ + if (typeof req.body.compiledContract !== 'object') { return res.send({error: 'Body parameter \'compiledContract\' must be an object'}); } self.compiledContracts = Object.assign(self.compiledContracts, req.body.compiledContract); const contractNames = Object.keys(req.body.compiledContract); self.build((err, _mgr) => { - if(err){ + if (err) { return res.send({error: err.message}); } @@ -243,12 +244,12 @@ class ContractsManager { }); }, (err) => { let responseData = {}; - if(err){ + if (err) { responseData.error = err.message; } else responseData.result = contractNames; - this.logger.trace(`POST response /embark-api/contract/deploy:\n ${JSON.stringify(responseData)}`); - res.send(responseData); + this.logger.trace(`POST response /embark-api/contract/deploy:\n ${JSON.stringify(responseData)}`); + res.send(responseData); }); }, false, false); } @@ -263,6 +264,7 @@ class ContractsManager { self.events.emit("status", __("Building...")); async.eachOf(contractsConfig.contracts, (contract, className, eachCb) => { + contract = new Contract(self.logger, contract); if (!contract.artifact) { contract.className = className; contract.args = contract.args || []; diff --git a/packages/embark-deploy-tracker/src/deploymentChecks.js b/packages/embark-deploy-tracker/src/deploymentChecks.js new file mode 100644 index 000000000..bfce839eb --- /dev/null +++ b/packages/embark-deploy-tracker/src/deploymentChecks.js @@ -0,0 +1,84 @@ +import {__} from 'embark-i18n'; +import {toChecksumAddress} from 'embark-utils'; +import Web3 from "web3"; + +export default class DeploymentChecks { + constructor({trackingFunctions, logger, events, plugins}) { + this.trackingFunctions = trackingFunctions; + this.logger = logger; + this.events = events; + this.plugins = plugins; + this._web3 = null; + } + + get web3() { + return (async () => { + if (!this._web3) { + const provider = await this.events.request2("blockchain:client:provider", "ethereum"); + this._web3 = new Web3(provider); + } + return this._web3; + })(); + } + + async checkContractConfig(params, cb) { + const {contract} = params; + + // previous event action check + if (!params.shouldDeploy) { + return cb(null, params); + } + + // contract config address field set - do not deploy + if (contract.address !== undefined) { + try { + toChecksumAddress(contract.address); + } catch (e) { + return cb(e); + } + contract.deployedAddress = contract.address; + contract.log(contract.className.bold.cyan + __(" already deployed at ").green + contract.deployedAddress.bold.cyan); + params.shouldDeploy = false; + return cb(null, params); + } + + cb(null, params); + } + + async checkIfAlreadyDeployed(params, cb) { + const {contract} = params; + const trackedContract = this.trackingFunctions.getContract(contract); + + // previous event action check + if (!params.shouldDeploy) { + return cb(null, params); + } + + // contract is not already tracked - deploy + if (!trackedContract || !trackedContract.address) { + return cb(null, params); + } + + // tracked contract has track field set - deploy anyway, but tell user + if (trackedContract.track === false || this.trackingFunctions.trackContracts === false) { + contract.log(contract.className.bold.cyan + __(" will be redeployed").green); + return cb(null, params); + } + + // if bytecode for the contract in chains.json exists on chain - don't deploy + const web3 = await this.web3; + let codeInChain = ""; + try { + codeInChain = await web3.eth.getCode(trackedContract.address); + } + catch (err) { + return cb(err); + } + if (codeInChain.length > 3) { // it is "0x" or "0x0" for empty code, depending on web3 version + contract.deployedAddress = trackedContract.address; + contract.log(contract.className.bold.cyan + __(" already deployed at ").green + contract.deployedAddress.bold.cyan); + params.shouldDeploy = false; + } + cb(null, params); + } +} diff --git a/packages/embark-deploy-tracker/src/index.js b/packages/embark-deploy-tracker/src/index.js index 95933d695..6bbb87629 100644 --- a/packages/embark-deploy-tracker/src/index.js +++ b/packages/embark-deploy-tracker/src/index.js @@ -1,130 +1,24 @@ -import { __ } from 'embark-i18n'; -import { dappPath, sha3 } from 'embark-utils'; -const Web3 = require('web3'); +import DeploymentChecks from "./deploymentChecks"; +import TrackingFunctions from "./trackingFunctions"; class DeployTracker { - constructor(embark, options) { - this.logger = embark.logger; - this.events = embark.events; - this.plugins = options.plugins; - this.fs = embark.fs; + constructor(embark, {trackContracts, env, plugins}) { + const {logger, events, fs, config} = embark; this.embark = embark; - this.trackContracts = (options.trackContracts !== false); // TODO: unclear where env comes from // TODO: we should be getting the env from a request to the config - this.env = options.env; - this.chainConfig = {}; - this.chainFile = embark.config.contractsConfig.tracking; - this.events.on("blockchain:started", this.loadChainTrackerFile.bind(this)); - this.embark.registerActionForEvent('deployment:deployContracts:beforeAll', this.setCurrentChain.bind(this)); - this.embark.registerActionForEvent("deployment:contract:deployed", this.trackAndSaveContract.bind(this)); - this.embark.registerActionForEvent("deploy:contract:shouldDeploy", this.checkIfDeploymentIsNeeded.bind(this)); + const trackingFunctions = new TrackingFunctions({config, fs, logger, events, env, trackContracts}); + const deploymentChecks = new DeploymentChecks({trackingFunctions, logger, events, plugins}); + + this.embark.events.on("blockchain:started", trackingFunctions.loadChainTrackerFile.bind(trackingFunctions)); + this.embark.registerActionForEvent('deployment:deployContracts:beforeAll', trackingFunctions.setCurrentChain.bind(trackingFunctions)); + this.embark.registerActionForEvent("deployment:contract:deployed", trackingFunctions.trackAndSaveContract.bind(trackingFunctions)); + this.embark.registerActionForEvent("deployment:contract:shouldDeploy", deploymentChecks.checkContractConfig.bind(deploymentChecks)); + this.embark.registerActionForEvent("deployment:contract:shouldDeploy", deploymentChecks.checkIfAlreadyDeployed.bind(deploymentChecks)); } - - trackAndSaveContract(params, cb) { - if (!this.embark.config.contractsConfig.tracking) return cb(); - let contract = params.contract; - this.trackContract(contract.className, contract.realRuntimeBytecode, contract.realArgs, contract.deployedAddress); - this.save(); - cb(); - } - - checkIfDeploymentIsNeeded(params, cb) { - if (!this.embark.config.contractsConfig.tracking) return; - if (!this.trackContracts) { - return cb(null, params); - } - - let contract = params.contract; - let trackedContract = this.getContract(contract.className, contract.realRuntimeBytecode, contract.realArgs); - if (trackedContract) { - params.contract.address = trackedContract.address; - } - if (params.shouldDeploy && trackedContract) { - params.shouldDeploy = true; - } - cb(null, params); - } - - loadChainTrackerFile() { - if (this.chainFile === false) return; - if (this.chainFile === undefined) this.chainFile = ".embark/chains.json"; - this.chainFile = dappPath(this.chainFile); - if (!this.fs.existsSync(this.chainFile)) { - this.logger.info(this.chainFile + ' ' + __('file not found, creating it...')); - this.fs.outputJSONSync(this.chainFile, {}); - } - - this.chainConfig = this.fs.readJSONSync(this.chainFile); - } - - setCurrentChain(_params, callback) { - if (!this.embark.config.contractsConfig.tracking) return callback(); - if (this.chainFile === false) return callback(); - if (this.chainConfig === false) { - this.currentChain = {contracts: []}; - return callback(); - } - - this.getBlock(0, (err) => { - if (err) { - // Retry with block 1 (Block 0 fails with Ganache-cli using the --fork option) - return this.getBlock(1, callback); - } - callback(); - }); - } - - async getBlock(blockNum, cb) { - let provider = await this.events.request2("blockchain:client:provider", "ethereum"); - var web3 = new Web3(provider); - - try { - let block = await web3.eth.getBlock(blockNum, true); - let chainId = block.hash; - - if (self.chainConfig[chainId] === undefined) { - self.chainConfig[chainId] = { contracts: {} }; - } - - self.currentChain = self.chainConfig[chainId]; - - self.currentChain.name = self.env; - cb(); - } catch (err) { - return cb(err); - } - } - - loadConfig(config) { - this.chainConfig = config; - return this; - } - - trackContract(name, code, args, address) { - if (!this.currentChain) return false; - this.currentChain.contracts[sha3(code + name + args.join(','))] = { name, address }; - } - - getContract(name, code, args) { - if (!this.currentChain) return false; - let contract = this.currentChain.contracts[sha3(code + name + args.join(','))]; - if (contract && contract.address === undefined) { - return false; - } - return contract; - } - - save() { - if (this.chainConfig === false) { - return; - } - this.fs.writeJSONSync(this.chainFile, this.chainConfig, {spaces: 2}); - } - } module.exports = DeployTracker; diff --git a/packages/embark-deploy-tracker/src/trackingFunctions.js b/packages/embark-deploy-tracker/src/trackingFunctions.js new file mode 100644 index 000000000..3c81a65bb --- /dev/null +++ b/packages/embark-deploy-tracker/src/trackingFunctions.js @@ -0,0 +1,111 @@ +import {__} from 'embark-i18n'; +import {dappPath} from 'embark-utils'; +import Web3 from 'web3'; + +export default class TrackingFunctions { + constructor({config, env, fs, events, logger, trackContracts}) { + this.config = config; + this.chainConfig = {}; + this.chainFile = config.contractsConfig.tracking; + this.currentChain = null; + this.env = env; + this.fs = fs; + this.events = events; + this.logger = logger; + this._web3 = null; + this.trackContracts = (trackContracts !== false); + } + + get web3() { + return (async () => { + if (!this._web3) { + const provider = await this.events.request2("blockchain:client:provider", "ethereum"); + this._web3 = new Web3(provider); + } + return this._web3; + })(); + } + + getContract(contract) { + if (!this.currentChain) return false; + let contractInFile = this.currentChain.contracts[contract.hash]; + if (contractInFile && contractInFile.address === undefined) { + return false; + } + return contractInFile; + } + + trackAndSaveContract(params, cb) { + const {contract} = params; + if (!this.chainFile || !this.trackContracts || contract.track === false) return cb(); + this.trackContract(contract); + this.save(); + cb(); + } + + loadChainTrackerFile() { + if (this.chainFile === false) return; + if (this.chainFile === undefined) this.chainFile = ".embark/chains.json"; + this.chainFile = dappPath(this.chainFile); + if (!this.fs.existsSync(this.chainFile)) { + this.logger.info(this.chainFile + ' ' + __('file not found, creating it...')); + this.fs.outputJSONSync(this.chainFile, {}); + this.chainConfig = {}; + return; + } + + this.chainConfig = this.fs.readJSONSync(this.chainFile); + } + + setCurrentChain(_params, callback) { + if (this.chainFile === false) return callback(); + if (this.chainConfig === false) { + this.currentChain = {contracts: []}; + return callback(); + } + + this.getBlock(0, (err) => { + if (err) { + // Retry with block 1 (Block 0 fails with Ganache-cli using the --fork option) + return this.getBlock(1, callback); + } + callback(); + }); + } + + async getBlock(blockNum, cb) { + try { + const web3 = await this.web3; + let block = await web3.eth.getBlock(blockNum, true); + let chainId = block.hash; + + if (this.chainConfig[chainId] === undefined) { + this.chainConfig[chainId] = {contracts: {}}; + } + + this.currentChain = this.chainConfig[chainId]; + + this.currentChain.name = this.env; + cb(); + } catch (err) { + return cb(err); + } + } + + loadConfig(config) { + this.chainConfig = config; + return this; + } + + trackContract(contract) { + if (!this.currentChain) return false; + this.currentChain.contracts[contract.hash] = {name: contract.className, address: contract.deployedAddress}; + } + + save() { + if (this.chainConfig === false) { + return; + } + this.fs.writeJSONSync(this.chainFile, this.chainConfig, {spaces: 2}); + } +} diff --git a/packages/embark-deployment/src/contract_deployer.js b/packages/embark-deployment/src/contract_deployer.js index 0181cafe9..cacaab9d2 100644 --- a/packages/embark-deployment/src/contract_deployer.js +++ b/packages/embark-deployment/src/contract_deployer.js @@ -1,4 +1,4 @@ -import { __ } from 'embark-i18n'; +import {__} from 'embark-i18n'; const async = require('async'); class ContractDeployer { @@ -8,7 +8,7 @@ class ContractDeployer { this.plugins = options.plugins; this.deployer = {}; this.events.setCommandHandler("deployment:deployer:register", (blockchainType, deployerCb) => { - this.deployer[blockchainType] = deployerCb + this.deployer[blockchainType] = deployerCb; }); this.events.setCommandHandler('deployment:contract:deploy', this.deployContract.bind(this)); @@ -28,33 +28,33 @@ class ContractDeployer { }, (next) => { // self.plugins.emitAndRunActionsForEvent('deployment:contract:arguments', {contract: contract}, (_params) => { - this.plugins.emitAndRunActionsForEvent('deployment:contract:shouldDeploy', {contract: contract, shouldDeploy: true}, (_params) => { - next(); + this.plugins.emitAndRunActionsForEvent('deployment:contract:shouldDeploy', {contract: contract, shouldDeploy: true}, (err, params) => { + next(err, params); }); }, - (next) => { - if (contract.deploy === false) { + (params, next) => { + + if (!params.shouldDeploy) { this.events.emit("deployment:contract:undeployed", contract); - return next(); + return next(null, null); } - console.dir("deploying contract"); - console.dir(contract.className); - // this.deployer[contract.blockchainType].apply(this.deployer, [contract, next]) - this.deployer["ethereum"].apply(this.deployer, [contract, next]) - // next(); + // TODO: implement `blockchainType` a la `this.deployer[contract.blockchainType].apply(this.deployer, [contract, next])` + this.deployer["ethereum"].apply(this.deployer, [contract, next]); }, - (next) => { - console.dir("-------> contract deployed") - if (contract.deploy === false) return next(); - console.dir("-------> contract deployed 2") - this.plugins.emitAndRunActionsForEvent('deployment:contract:deployed', {contract: contract}, (_params) => { - next(); + (receipt, next) => { + if (!receipt) return next(); + this.plugins.emitAndRunActionsForEvent('deployment:contract:deployed', {contract, receipt}, (err, _params) => { + next(err); }); } - ], callback); + ], (err) => { + if (err) { + this.events.emit("deploy:contract:error", contract); + } + callback(err); + }); } - } module.exports = ContractDeployer; diff --git a/packages/embark-deployment/src/index.js b/packages/embark-deployment/src/index.js index 8b9e03d5b..7b059c222 100644 --- a/packages/embark-deployment/src/index.js +++ b/packages/embark-deployment/src/index.js @@ -1,4 +1,4 @@ -import { __ } from 'embark-i18n'; +import {__} from 'embark-i18n'; const async = require('async'); const ContractDeployer = require('./contract_deployer.js'); @@ -15,7 +15,8 @@ class Deployment { this.contractDeployer = new ContractDeployer({ events: this.events, - plugins: this.plugins + plugins: this.plugins, + logger: this.logger }); this.events.setCommandHandler('deployment:contracts:deploy', (contractsList, contractDependencies, cb) => { @@ -27,17 +28,17 @@ class Deployment { this.logger.info(__("deploying contracts")); async.waterfall([ // TODO used to be called this.plugins.emitAndRunActionsForEvent("deploy:beforeAll", (err) => { - (next) => { this.plugins.emitAndRunActionsForEvent('deployment:deployContracts:beforeAll', {}, () => { next() }); }, - (next) => { this.deployAll(contracts, contractDependencies, () => { next() }); }, + (next) => {this.plugins.emitAndRunActionsForEvent('deployment:deployContracts:beforeAll', {}, () => {next()});}, + (next) => {this.deployAll(contracts, contractDependencies, next);}, (next) => { this.events.emit('contractsDeployed'); - this.plugins.emitAndRunActionsForEvent('deployment:deployContracts:afterAll', {}, () => { next() }); + this.plugins.emitAndRunActionsForEvent('deployment:deployContracts:afterAll', {}, () => {next()}); console.dir("==== finished deploying"); } ], done); } - deployContract(contract, callback) { + deployContract(contract, errors, callback) { console.dir("requesting to deploy contract") this.events.request('deployment:contract:deploy', contract, (err) => { if (err) { @@ -63,7 +64,7 @@ class Deployment { function deploy(result, callback) { console.dir("== deploy") if (typeof result === 'function') callback = result; - self.deployContract(contract, callback); + self.deployContract(contract, errors, callback); } const className = contract.className; @@ -75,23 +76,22 @@ class Deployment { contractDeploys[className].push(deploy); }) - async.auto(contractDeploys, (_err, _results) => { - if (_err) { + async.auto(contractDeploys, (err, _results) => { + if (err) { console.dir("error deploying contracts") - console.dir(_err) + console.dir(err) } if (errors.length) { - _err = __("Error deploying contracts. Please fix errors to continue."); - this.logger.error(_err); + err = __("Error deploying contracts. Please fix errors to continue."); this.events.emit("outputError", __("Error deploying contracts, please check console")); - return done(_err); + return done(err); } if (contracts.length === 0) { this.logger.info(__("no contracts found")); return done(); } this.logger.info(__("finished deploying contracts")); - done(_err); + done(err); }); } diff --git a/packages/embark-typings/index.d.ts b/packages/embark-typings/index.d.ts index 54e6cf61d..3dae9dc88 100644 --- a/packages/embark-typings/index.d.ts +++ b/packages/embark-typings/index.d.ts @@ -4,6 +4,7 @@ import "./src/remix-debug-debugtest"; export * from "./src/callbacks"; export * from "./src/contract"; export * from "./src/embark"; +export * from "./src/contractsConfig"; export * from "./src/embarkConfig"; export * from "./src/logger"; export * from "./src/maybe"; diff --git a/packages/embark-typings/src/contractsConfig.d.ts b/packages/embark-typings/src/contractsConfig.d.ts new file mode 100644 index 000000000..e58ab5080 --- /dev/null +++ b/packages/embark-typings/src/contractsConfig.d.ts @@ -0,0 +1,16 @@ +export interface ContractsConfig { + deploy: { [name: string]: ContractConfig } + gas: string | number; + tracking: boolean | string; +} + +export interface ContractConfig { + address?: string; + args?: Array; + instanceOf?: string; + gas?: number; + gasPrice?: number; + silent?: boolean; + track?: boolean; + deploy?: boolean; +} \ No newline at end of file diff --git a/packages/embark-typings/src/logger.d.ts b/packages/embark-typings/src/logger.d.ts index b2c7aba81..7e3225fb5 100644 --- a/packages/embark-typings/src/logger.d.ts +++ b/packages/embark-typings/src/logger.d.ts @@ -1,5 +1,6 @@ export interface Logger { info(text: string): void; warn(text: string): void; - error(text: string, ...args: Array): void; + trace(text: string): void; + error(text: string, ...args: Array): void; } diff --git a/packages/embark/src/cmd/cmd_controller.js b/packages/embark/src/cmd/cmd_controller.js index f207e03b5..4747630f5 100644 --- a/packages/embark/src/cmd/cmd_controller.js +++ b/packages/embark/src/cmd/cmd_controller.js @@ -1,6 +1,6 @@ -import { BlockchainClient, Simulator } from 'embark-blockchain-process'; -import { __ } from 'embark-i18n'; -import { dappPath, embarkPath } from 'embark-utils'; +import {BlockchainClient, Simulator} from 'embark-blockchain-process'; +import {__} from 'embark-i18n'; +import {dappPath, embarkPath} from 'embark-utils'; import findUp from 'find-up'; let async = require('async'); const constants = require('embark-core/constants'); @@ -209,7 +209,7 @@ class EmbarkController { engine.events.emit("status", __("Ready").green); }); - engine.events.on('file-event', async ({ fileType, path }) => { + engine.events.on('file-event', async ({fileType, path}) => { // TODO: re-add async.cargo / or use rxjs to use latest request in the queue console.dir("-- before timeout - file changed") @@ -219,7 +219,12 @@ class EmbarkController { let _contractsConfig = await engine.events.request2("config:contractsConfig"); let contractsConfig = cloneDeep(_contractsConfig); let [contractsList, contractDependencies] = await engine.events.request2("contracts:build", contractsConfig, compiledContracts); - await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies); + try { + await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies); + } + catch (err) { + engine.logger.error(err); + } } else if (fileType === 'asset') { engine.events.request('pipeline:generateAll', () => { console.dir("outputDone") @@ -238,7 +243,12 @@ class EmbarkController { let _contractsConfig = await engine.events.request2("config:contractsConfig"); let contractsConfig = cloneDeep(_contractsConfig); let [contractsList, contractDependencies] = await engine.events.request2("contracts:build", contractsConfig, compiledContracts); - await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies); + try { + await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies); + } + catch (err) { + engine.logger.error(err); + } console.dir("deployment done") await engine.events.request2("watcher:start") @@ -470,7 +480,7 @@ class EmbarkController { }); } ], function (err, canExit) { - if(err) { + if (err) { engine.logger.error(err.message || err); } // TODO: this should be moved out and determined somewhere else @@ -499,7 +509,7 @@ class EmbarkController { webpackConfigName: options.webpackConfigName }); - const isSecondaryProcess = (engine) => { return engine.ipc.connected && engine.ipc.isClient(); }; + const isSecondaryProcess = (engine) => {return engine.ipc.connected && engine.ipc.isClient();}; async.waterfall([ function initEngine(callback) { @@ -537,7 +547,7 @@ class EmbarkController { }, function deploy(callback) { // Skip if we are connected to a websocket, the server will do it - if(isSecondaryProcess(engine)) { + if (isSecondaryProcess(engine)) { return callback(); } engine.config.reloadConfig(); @@ -547,7 +557,7 @@ class EmbarkController { }, function waitForWriteFinish(callback) { // Skip if we are connected to a websocket, the server will do it - if(isSecondaryProcess(engine)) { + if (isSecondaryProcess(engine)) { return callback(); } engine.logger.info("Finished deploying".underline); @@ -695,7 +705,7 @@ class EmbarkController { callback(); }, function generateContract(callback) { - engine.events.request('scaffolding:generate:contract', options, function(files) { + engine.events.request('scaffolding:generate:contract', options, function (files) { files.forEach(file => engine.events.request('config:contractsFiles:add', file)); callback(); }); @@ -714,7 +724,7 @@ class EmbarkController { callback(); }, function deploy(callback) { - engine.events.request('deploy:contracts', function(err) { + engine.events.request('deploy:contracts', function (err) { callback(err); }); }, @@ -723,7 +733,7 @@ class EmbarkController { callback(); }); } - ], function(err) { + ], function (err) { if (err) { engine.logger.error(__("Error generating the UI: ")); engine.logger.error(err.message || err); @@ -805,7 +815,12 @@ class EmbarkController { let _contractsConfig = await engine.events.request2("config:contractsConfig"); let contractsConfig = cloneDeep(_contractsConfig); let [contractsList, contractDependencies] = await engine.events.request2("contracts:build", contractsConfig, compiledContracts); - await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies); + try { + await engine.events.request2("deployment:contracts:deploy", contractsList, contractDependencies); + } + catch (err) { + engine.logger.error(err); + } console.dir("deployment done") await engine.events.request2('pipeline:generateAll'); @@ -831,17 +846,17 @@ class EmbarkController { // }); } // function associateToENS(hash, callback) { - // if(!options.ensDomain) { - // return callback(null, hash); - // } - // engine.events.request("storage:ens:associate", - // {name: options.ensDomain, storageHash: hash}, (err) => { - // if (err) { - // return callback(err); - // } - // engine.logger.info(__('ENS association completed for {{hash}} at {{domain}}', {hash, domain: options.ensDomain})); - // callback(); - // }); + // if(!options.ensDomain) { + // return callback(null, hash); + // } + // engine.events.request("storage:ens:associate", + // {name: options.ensDomain, storageHash: hash}, (err) => { + // if (err) { + // return callback(err); + // } + // engine.logger.info(__('ENS association completed for {{hash}} at {{domain}}', {hash, domain: options.ensDomain})); + // callback(); + // }); // } ], function (err) { if (err) { diff --git a/packages/embark/src/lib/core/engine.js b/packages/embark/src/lib/core/engine.js index eef2c92fa..789f0bd21 100644 --- a/packages/embark/src/lib/core/engine.js +++ b/packages/embark/src/lib/core/engine.js @@ -1,4 +1,4 @@ -import { ProcessManager, IPC } from 'embark-core'; +import {ProcessManager, IPC} from 'embark-core'; const async = require('async'); const utils = require('../utils/utils'); @@ -24,7 +24,7 @@ class Engine { } init(_options, callback) { - callback = callback || function() {}; + callback = callback || function () {}; const Events = require('./events.js'); const Config = require('./config.js'); @@ -174,7 +174,14 @@ class Engine { isDev: this.isDev, plugins: this.plugins, ipc: this.ipc - }) + }); + this.registerModule('parity', { + client: this.client, + locale: this.locale, + isDev: this.isDev, + plugins: this.plugins, + ipc: this.ipc + }); } testComponents() { @@ -257,13 +264,17 @@ class Engine { return service.apply(this, [options]); } - blockchainListenerService(_options){ + embarkListenerService(_options) { + this.registerModulePackage('embark-listener'); + } + + blockchainListenerService(_options) { this.registerModulePackage('embark-blockchain-listener', { ipc: this.ipc }); } - coreProcessService(_options){ + coreProcessService(_options) { this.registerModulePackage('embark-core/process', { events: this.events }); @@ -282,7 +293,7 @@ class Engine { } scaffoldingService(_options) { - this.registerModulePackage('embark-scaffolding', {plugins: this.plugins}); + this.registerModulePackage('embark-scaffolding', {plugins: this.plugins}); } serviceMonitor() { @@ -307,25 +318,25 @@ class Engine { codeGeneratorService(_options) { return; // let self = this; -// + // // this.registerModulePackage('embark-code-generator', {plugins: self.plugins, env: self.env}); -// + // // const generateCode = function (modifiedAssets) { - // // self.events.request("module:storage:onReady", () => { - // self.events.request("code-generator:embarkjs:build", () => { - // self.events.emit('code-generator-ready', modifiedAssets); - // }); - // // }); + // // self.events.request("module:storage:onReady", () => { + // self.events.request("code-generator:embarkjs:build", () => { + // self.events.emit('code-generator-ready', modifiedAssets); + // }); + // // }); // }; // const cargo = async.cargo((tasks, callback) => { - // const modifiedAssets = tasks.map(task => task.modifiedAsset).filter(asset => asset); // filter null elements - // generateCode(modifiedAssets); - // self.events.once('outputDone', callback); + // const modifiedAssets = tasks.map(task => task.modifiedAsset).filter(asset => asset); // filter null elements + // generateCode(modifiedAssets); + // self.events.once('outputDone', callback); // }); // const addToCargo = function (modifiedAsset) { - // cargo.push({modifiedAsset}); + // cargo.push({modifiedAsset}); // }; -// + // // this.events.on('contractsDeployed', addToCargo); // this.events.on('blockchainDisabled', addToCargo); // this.events.on('asset-changed', addToCargo); @@ -411,8 +422,8 @@ class Engine { wait: options.wait }); - this.registerModulePackage('embark-whisper', { plugins: this.plugins }); - this.registerModule('web3', { plugins: this.plugins }); + this.registerModulePackage('embark-whisper', {plugins: this.plugins}); + this.registerModule('web3', {plugins: this.plugins}); } libraryManagerService(_options) { diff --git a/packages/embark/src/lib/modules/ethereum-blockchain-client/index.js b/packages/embark/src/lib/modules/ethereum-blockchain-client/index.js index 6d59df2c3..452dcd2be 100644 --- a/packages/embark/src/lib/modules/ethereum-blockchain-client/index.js +++ b/packages/embark/src/lib/modules/ethereum-blockchain-client/index.js @@ -1,3 +1,5 @@ +import {__} from 'embark-i18n'; + const async = require('async'); const Web3 = require('web3'); const embarkJsUtils = require('embarkjs').Utils; @@ -7,6 +9,7 @@ class EthereumBlockchainClient { constructor(embark, options) { this.embark = embark; this.events = embark.events; + this.logger = embark.logger; this.embark.registerActionForEvent("deployment:contract:deployed", this.addContractJSONToPipeline.bind(this)); this.embark.registerActionForEvent('deployment:contract:beforeDeploy', this.determineArguments.bind(this)); @@ -30,7 +33,7 @@ class EthereumBlockchainClient { console.dir("== ethereum contract deployer") let contractObj = new web3.eth.Contract(contract.abiDefinition, contract.address); // let deployObject = this.blockchain.deployContractObject(contractObject, {arguments: contractParams, data: dataCode}); - let contractObject = contractObj.deploy({ arguments: (contract.args || []), data: ("0x" + contract.code) }); + let contractObject = contractObj.deploy({arguments: (contract.args || []), data: ("0x" + contract.code)}); if (contract.gas === 'auto' || !contract.gas) { let gasValue = await contractObject.estimateGas(); @@ -44,7 +47,7 @@ class EthereumBlockchainClient { } // this.blockchain.deployContractFromObject(deployObject, - console.dir({ arguments: contract.args, data: ("0x" + contract.code) }); + console.dir({arguments: contract.args, data: ("0x" + contract.code)}); console.dir("------- send") embarkJsUtils.secureSend(web3, contractObject, { @@ -52,7 +55,8 @@ class EthereumBlockchainClient { }, true, (err, receipt) => { contract.deployedAddress = receipt.contractAddress; contract.transactionHash = receipt.transactionHash; - done(); + contract.log(`${contract.className.bold.cyan} ${__('deployed at').green} ${receipt.contractAddress.bold.cyan} ${__("using").green} ${receipt.gasUsed} ${__("gas").green} (txHash: ${receipt.transactionHash.bold.cyan})`); + done(err, receipt); }, (hash) => { console.dir('hash is ' + hash); }); diff --git a/packages/embark/src/lib/modules/geth/index.js b/packages/embark/src/lib/modules/geth/index.js index 6bcc3d86d..57b35d10b 100644 --- a/packages/embark/src/lib/modules/geth/index.js +++ b/packages/embark/src/lib/modules/geth/index.js @@ -1,6 +1,7 @@ -const { normalizeInput } = require('embark-utils'); -import { BlockchainProcessLauncher } from './blockchainProcessLauncher'; +const {normalizeInput} = require('embark-utils'); +import {BlockchainProcessLauncher} from './blockchainProcessLauncher'; import {ws, rpc} from './check.js'; +const constants = require('embark-core/constants'); class Geth { @@ -16,30 +17,46 @@ class Geth { this.plugins = options.plugins; // let plugin = this.plugins.createPlugin('gethplugin', {}); - this.events.request("blockchain:node:register", "geth", (readyCb) => { - console.dir('registering blockchain node') - console.dir(readyCb) + if (!this.shouldInit()) { + return; + } + + this.events.request("blockchain:node:register", constants.blockchain.clients.geth, (readyCb) => { + console.dir('registering blockchain node'); + console.dir(readyCb); this.events.request('processes:register', 'blockchain', { launchFn: (cb) => { // this.startBlockchainNode(readyCb); this.startBlockchainNode(cb); }, - stopFn: (cb) => { this.stopBlockchainNode(cb); } + stopFn: (cb) => { + this.stopBlockchainNode(cb); + } }); this.events.request("processes:launch", "blockchain", (err) => { - readyCb() + if (err) { + this.logger.error(`Error launching blockchain process: ${err.message || err}`); + } + readyCb(); }); - this.registerServiceCheck() - }) + this.registerServiceCheck(); + }); + } + + shouldInit() { + return ( + this.blockchainConfig.client === constants.blockchain.clients.geth && + this.blockchainConfig.enabled + ); } _getNodeState(err, version, cb) { - if (err) return cb({ name: "Ethereum node not found", status: 'off' }); + if (err) return cb({name: "Ethereum node not found", status: 'off'}); - let nodeName = "go-ethereum" + let nodeName = "go-ethereum"; let versionNumber = version.split("-")[0]; let name = nodeName + " " + versionNumber + " (Ethereum)"; - return cb({ name, status: 'on' }); + return cb({name, status: 'on'}); } // TODO: need to get correct port taking into account the proxy @@ -47,9 +64,9 @@ class Geth { this.events.request("services:register", 'Ethereum', (cb) => { const {rpcHost, rpcPort, wsRPC, wsHost, wsPort} = this.blockchainConfig; if (wsRPC) { - return ws(wsHost, wsPort+10, (err, version) => this._getNodeState(err, version, cb)); + return ws(wsHost, wsPort + 10, (err, version) => this._getNodeState(err, version, cb)); } - rpc(rpcHost, rpcPort+10, (err, version) => this._getNodeState(err, version, cb)); + rpc(rpcHost, rpcPort + 10, (err, version) => this._getNodeState(err, version, cb)); }, 5000, 'off'); } @@ -72,7 +89,7 @@ class Geth { stopBlockchainNode(cb) { const message = __(`The blockchain process has been stopped. It can be restarted by running ${"service blockchain on".bold} in the Embark console.`); - if(!this.blockchainProcess) { + if (!this.blockchainProcess) { return cb(); } diff --git a/packages/embark/src/lib/modules/parity/blockchain.js b/packages/embark/src/lib/modules/parity/blockchain.js new file mode 100644 index 000000000..944971c75 --- /dev/null +++ b/packages/embark/src/lib/modules/parity/blockchain.js @@ -0,0 +1,470 @@ +import {__} from 'embark-i18n'; +const fs = require('fs-extra'); +const async = require('async'); +const {spawn, exec} = require('child_process'); +const path = require('path'); +const constants = require('embark-core/constants'); +const ParityClient = require('./parityClient.js'); +// const ParityClient = require('./parityClient.js'); +// import { Proxy } from './proxy'; +import {IPC} from 'embark-core'; + +import {compact, dappPath, defaultHost, dockerHostSwap, embarkPath, AccountParser} from 'embark-utils'; +const Logger = require('embark-logger'); + +// time between IPC connection attempts (in ms) +const IPC_CONNECT_INTERVAL = 2000; + +/*eslint complexity: ["error", 50]*/ +var Blockchain = function (userConfig, clientClass) { + this.userConfig = userConfig; + this.env = userConfig.env || 'development'; + this.isDev = userConfig.isDev; + this.onReadyCallback = userConfig.onReadyCallback || (() => {}); + this.onExitCallback = userConfig.onExitCallback; + this.logger = userConfig.logger || new Logger({logLevel: 'debug', context: constants.contexts.blockchain}); // do not pass in events as we don't want any log events emitted + this.events = userConfig.events; + this.proxyIpc = null; + this.isStandalone = userConfig.isStandalone; + this.certOptions = userConfig.certOptions; + + + let defaultWsApi = clientClass.DEFAULTS.WS_API; + if (this.isDev) defaultWsApi = clientClass.DEFAULTS.DEV_WS_API; + + this.config = { + silent: this.userConfig.silent, + client: this.userConfig.client, + ethereumClientBin: this.userConfig.ethereumClientBin || this.userConfig.client, + networkType: this.userConfig.networkType || clientClass.DEFAULTS.NETWORK_TYPE, + networkId: this.userConfig.networkId || clientClass.DEFAULTS.NETWORK_ID, + genesisBlock: this.userConfig.genesisBlock || false, + datadir: this.userConfig.datadir, + mineWhenNeeded: this.userConfig.mineWhenNeeded || false, + rpcHost: dockerHostSwap(this.userConfig.rpcHost) || defaultHost, + rpcPort: this.userConfig.rpcPort || 8545, + rpcCorsDomain: this.userConfig.rpcCorsDomain || false, + rpcApi: this.userConfig.rpcApi || clientClass.DEFAULTS.RPC_API, + port: this.userConfig.port || 30303, + nodiscover: this.userConfig.nodiscover || false, + mine: this.userConfig.mine || false, + account: {}, + whisper: (this.userConfig.whisper !== false), + maxpeers: ((this.userConfig.maxpeers === 0) ? 0 : (this.userConfig.maxpeers || 25)), + bootnodes: this.userConfig.bootnodes || "", + wsRPC: (this.userConfig.wsRPC !== false), + wsHost: dockerHostSwap(this.userConfig.wsHost) || defaultHost, + wsPort: this.userConfig.wsPort || 8546, + wsOrigins: this.userConfig.wsOrigins || false, + wsApi: this.userConfig.wsApi || defaultWsApi, + vmdebug: this.userConfig.vmdebug || false, + targetGasLimit: this.userConfig.targetGasLimit || false, + syncMode: this.userConfig.syncMode || this.userConfig.syncmode, + verbosity: this.userConfig.verbosity, + proxy: this.userConfig.proxy + }; + + this.devFunds = null; + + if (this.userConfig.accounts) { + const nodeAccounts = this.userConfig.accounts.find(account => account.nodeAccounts); + if (nodeAccounts) { + this.config.account = { + numAccounts: nodeAccounts.numAddresses || 1, + password: nodeAccounts.password, + balance: nodeAccounts.balance + }; + } + } + + if (this.userConfig === {} || this.userConfig.default || JSON.stringify(this.userConfig) === '{"client":"parity"}') { + if (this.env === 'development') { + this.isDev = true; + } else { + this.config.genesisBlock = embarkPath("templates/boilerplate/config/privatenet/genesis.json"); + } + this.config.datadir = dappPath(".embark/development/datadir"); + this.config.wsOrigins = this.config.wsOrigins || "http://localhost:8000"; + this.config.rpcCorsDomain = this.config.rpcCorsDomain || "http://localhost:8000"; + this.config.targetGasLimit = 8000000; + } + this.config.account.devPassword = path.join(this.config.datadir, "devPassword"); + + const spaceMessage = 'The path for %s in blockchain config contains spaces, please remove them'; + if (this.config.datadir && this.config.datadir.indexOf(' ') > 0) { + this.logger.error(__(spaceMessage, 'datadir')); + process.exit(1); + } + if (this.config.account.password && this.config.account.password.indexOf(' ') > 0) { + this.logger.error(__(spaceMessage, 'accounts.password')); + process.exit(1); + } + if (this.config.genesisBlock && this.config.genesisBlock.indexOf(' ') > 0) { + this.logger.error(__(spaceMessage, 'genesisBlock')); + process.exit(1); + } + this.initProxy(); + this.client = new clientClass({config: this.config, env: this.env, isDev: this.isDev}); + + this.initStandaloneProcess(); +}; + +/** + * Polls for a connection to an IPC server (generally this is set up + * in the Embark process). Once connected, any logs logged to the + * Logger will be shipped off to the IPC server. In the case of `embark + * run`, the BlockchainListener module is listening for these logs. + * + * @returns {void} + */ +Blockchain.prototype.initStandaloneProcess = function () { + if (this.isStandalone) { + let logQueue = []; + + // on every log logged in logger (say that 3x fast), send the log + // to the IPC serve listening (only if we're connected of course) + this.logger.events.on('log', (logLevel, message) => { + if (this.ipc.connected) { + this.ipc.request('blockchain:log', {logLevel, message}); + } else { + logQueue.push({logLevel, message}); + } + }); + + this.ipc = new IPC({ipcRole: 'client'}); + + // Wait for an IPC server to start (ie `embark run`) by polling `.connect()`. + // Do not kill this interval as the IPC server may restart (ie restart + // `embark run` without restarting `embark blockchain`) + setInterval(() => { + if (!this.ipc.connected) { + this.ipc.connect(() => { + if (this.ipc.connected) { + logQueue.forEach(message => {this.ipc.request('blockchain:log', message);}); + logQueue = []; + this.ipc.client.on('process:blockchain:stop', () => { + this.kill(); + process.exit(0); + }); + } + }); + } + }, IPC_CONNECT_INTERVAL); + } +}; + +Blockchain.prototype.initProxy = function () { + if (this.config.proxy) { + this.config.rpcPort += constants.blockchain.servicePortOnProxy; + this.config.wsPort += constants.blockchain.servicePortOnProxy; + } +}; + +Blockchain.prototype.setupProxy = async function () { + // if (!this.proxyIpc) this.proxyIpc = new IPC({ipcRole: 'client'}); + + // const addresses = AccountParser.parseAccountsConfig(this.userConfig.accounts, false, dappPath(), this.logger); + + // let wsProxy; + // if (this.config.wsRPC) { + // wsProxy = new Proxy(this.proxyIpc).serve(this.config.wsHost, this.config.wsPort, true, this.config.wsOrigins, addresses, this.certOptions); + // } + + // [this.rpcProxy, this.wsProxy] = await Promise.all([new Proxy(this.proxyIpc).serve(this.config.rpcHost, this.config.rpcPort, false, null, addresses, this.certOptions), wsProxy]); +}; + +Blockchain.prototype.shutdownProxy = function () { + // if (!this.config.proxy) { + // return; + // } + + // if (this.rpcProxy) this.rpcProxy.close(); + // if (this.wsProxy) this.wsProxy.close(); +}; + +Blockchain.prototype.runCommand = function (cmd, options, callback) { + this.logger.info(__("running: %s", cmd.underline).green); + if (this.config.silent) { + options.silent = true; + } + return exec(cmd, options, callback); +}; + +Blockchain.prototype.run = function () { + var self = this; + this.logger.info("===============================================================================".magenta); + this.logger.info("===============================================================================".magenta); + this.logger.info(__("Embark Blockchain using %s", self.client.prettyName.underline).magenta); + this.logger.info("===============================================================================".magenta); + this.logger.info("===============================================================================".magenta); + + let address = ''; + async.waterfall([ + function checkInstallation(next) { + self.isClientInstalled((err) => { + if (err) { + return next({message: err}); + } + next(); + }); + }, + function init(next) { + if (self.isDev) { + return self.initDevChain((err) => { + next(err); + }); + } + return self.initChainAndGetAddress((err, addr) => { + address = addr; + next(err); + }); + }, + function getMainCommand(next) { + self.client.mainCommand(address, function (cmd, args) { + next(null, cmd, args); + }, true); + } + ], function (err, cmd, args) { + if (err) { + self.logger.error(err.message); + return; + } + args = compact(args); + + let full_cmd = cmd + " " + args.join(' '); + self.logger.info(__("running: %s", full_cmd.underline).green); + self.child = spawn(cmd, args, {cwd: process.cwd()}); + + self.child.on('error', (err) => { + err = err.toString(); + self.logger.error('Blockchain error: ', err); + if (self.env === 'development' && err.indexOf('Failed to unlock') > 0) { + self.logger.error('\n' + __('Development blockchain has changed to use the --dev option.').yellow); + self.logger.error(__('You can reset your workspace to fix the problem with').yellow + ' embark reset'.cyan); + self.logger.error(__('Otherwise, you can change your data directory in blockchain.json (datadir)').yellow); + } + }); + + // TOCHECK I don't understand why stderr and stdout are reverted. + // This happens with Geth and Parity, so it does not seems a client problem + self.child.stdout.on('data', (data) => { + self.logger.info(`${self.client.name} error: ${data}`); + }); + + self.child.stderr.on('data', async (data) => { + data = data.toString(); + if (!self.readyCalled && self.client.isReady(data)) { + self.readyCalled = true; + // if (self.config.proxy) { + // await self.setupProxy(); + // } + self.readyCallback(); + } + self.logger.info(`${self.client.name}: ${data}`); + }); + + self.child.on('exit', (code) => { + let strCode; + if (code) { + strCode = 'with error code ' + code; + } else { + strCode = 'with no error code (manually killed?)'; + } + self.logger.error(self.client.name + ' exited ' + strCode); + if (self.onExitCallback) { + self.onExitCallback(); + } + }); + + self.child.on('uncaughtException', (err) => { + self.logger.error('Uncaught ' + self.client.name + ' exception', err); + if (self.onExitCallback) { + self.onExitCallback(); + } + }); + }); +}; + +Blockchain.prototype.readyCallback = function () { + if (this.onReadyCallback) { + this.onReadyCallback(); + } + if (this.config.mineWhenNeeded && !this.isDev) { + this.miner = this.client.getMiner(); + } +}; + +Blockchain.prototype.kill = function () { + this.shutdownProxy(); + if (this.child) { + this.child.kill(); + } +}; + +Blockchain.prototype.isClientInstalled = function (callback) { + let versionCmd = this.client.determineVersionCommand(); + this.runCommand(versionCmd, {}, (err, stdout, stderr) => { + if (err || !stdout || stderr.indexOf("not found") >= 0 || stdout.indexOf("not found") >= 0) { + return callback(__('Ethereum client bin not found:') + ' ' + this.client.getBinaryPath()); + } + const parsedVersion = this.client.parseVersion(stdout); + const supported = this.client.isSupportedVersion(parsedVersion); + if (supported === undefined) { + this.logger.error((__('WARNING! Ethereum client version could not be determined or compared with version range') + ' ' + this.client.versSupported + __(', for best results please use a supported version')).yellow); + } else if (!supported) { + this.logger.error((__('WARNING! Ethereum client version unsupported, for best results please use a version in range') + ' ' + this.client.versSupported).yellow); + } + callback(); + }); +}; + +Blockchain.prototype.initDevChain = function (callback) { + const self = this; + const ACCOUNTS_ALREADY_PRESENT = 'accounts_already_present'; + // Init the dev chain + self.client.initDevChain(self.config.datadir, (err) => { + if (err) { + return callback(err); + } + + const accountsToCreate = self.config.account && self.config.account.numAccounts; + if (!accountsToCreate) return callback(); + + // Create other accounts + async.waterfall([ + function listAccounts(next) { + self.runCommand(self.client.listAccountsCommand(), {}, (err, stdout, _stderr) => { + if (err || stdout === undefined || stdout.indexOf("Fatal") >= 0) { + console.log(__("no accounts found").green); + return next(); + } + // List current addresses + self.config.unlockAddressList = self.client.parseListAccountsCommandResultToAddressList(stdout); + // Count current addresses and remove the default account from the count (because password can be different) + let addressCount = self.config.unlockAddressList.length; + if (addressCount < accountsToCreate) { + next(null, accountsToCreate - addressCount); + } else { + next(ACCOUNTS_ALREADY_PRESENT); + } + }); + }, + function newAccounts(accountsToCreate, next) { + var accountNumber = 0; + async.whilst( + function () { + return accountNumber < accountsToCreate; + }, + function (callback) { + accountNumber++; + self.runCommand(self.client.newAccountCommand(), {}, (err, stdout, _stderr) => { + if (err) { + return callback(err, accountNumber); + } + self.config.unlockAddressList.push(self.client.parseNewAccountCommandResultToAddress(stdout)); + callback(null, accountNumber); + }); + }, + function (err) { + next(err); + } + ); + } + ], (err) => { + if (err && err !== ACCOUNTS_ALREADY_PRESENT) { + console.log(err); + return callback(err); + } + callback(); + }); + }); +}; + +Blockchain.prototype.initChainAndGetAddress = function (callback) { + const self = this; + let address = null; + const ALREADY_INITIALIZED = 'already'; + + // ensure datadir exists, bypassing the interactive liabilities prompt. + self.datadir = self.config.datadir; + + async.waterfall([ + function makeDir(next) { + fs.mkdirp(self.datadir, (err, _result) => { + next(err); + }); + }, + function listAccounts(next) { + self.runCommand(self.client.listAccountsCommand(), {}, (err, stdout, _stderr) => { + if (err || stdout === undefined || stdout.indexOf("Fatal") >= 0) { + self.logger.info(__("no accounts found").green); + return next(); + } + let firstAccountFound = self.client.parseListAccountsCommandResultToAddress(stdout); + if (firstAccountFound === undefined || firstAccountFound === "") { + console.log(__("no accounts found").green); + return next(); + } + self.logger.info(__("already initialized").green); + address = firstAccountFound; + next(ALREADY_INITIALIZED); + }); + }, + function genesisBlock(next) { + //There's no genesis init with Parity. Custom network are set in the chain property at startup + if (!self.config.genesisBlock || self.client.name === constants.blockchain.clients.parity) { + return next(); + } + self.logger.info(__("initializing genesis block").green); + self.runCommand(self.client.initGenesisCommmand(), {}, (err, _stdout, _stderr) => { + next(err); + }); + }, + function newAccount(next) { + self.runCommand(self.client.newAccountCommand(), {}, (err, stdout, _stderr) => { + if (err) { + return next(err); + } + address = self.client.parseNewAccountCommandResultToAddress(stdout); + next(); + }); + } + ], (err) => { + if (err === ALREADY_INITIALIZED) { + err = null; + } + callback(err, address); + }); +}; + +export function BlockchainClient(userConfig, options) { + if ((userConfig === {} || JSON.stringify(userConfig) === '{"enabled":true}') && options.env !== 'development') { + options.logger.info("===> " + __("warning: running default config on a non-development environment")); + } + // if client is not set in preferences, default is parity + if (!userConfig.client) userConfig.client = constants.blockchain.clients.parity; + // if clientName is set, it overrides preferences + if (options.clientName) userConfig.client = options.clientName; + // Choose correct client instance based on clientName + let clientClass; + switch (userConfig.client) { + case constants.blockchain.clients.parity: + clientClass = ParityClient; + break; + + // case constants.blockchain.clients.parity: + // clientClass = ParityClient; + // break; + default: + console.error(__('Unknown client "%s". Please use one of the following: %s', userConfig.client, Object.keys(constants.blockchain.clients).join(', '))); + process.exit(1); + } + userConfig.isDev = (userConfig.isDev || userConfig.default); + userConfig.env = options.env; + userConfig.onReadyCallback = options.onReadyCallback; + userConfig.onExitCallback = options.onExitCallback; + userConfig.logger = options.logger; + userConfig.certOptions = options.certOptions; + userConfig.isStandalone = options.isStandalone; + return new Blockchain(userConfig, clientClass); +} diff --git a/packages/embark/src/lib/modules/parity/blockchainProcess.js b/packages/embark/src/lib/modules/parity/blockchainProcess.js new file mode 100644 index 000000000..75b82623c --- /dev/null +++ b/packages/embark/src/lib/modules/parity/blockchainProcess.js @@ -0,0 +1,57 @@ +import * as i18n from 'embark-i18n'; +import { ProcessWrapper } from 'embark-core'; +const constants = require('embark-core/constants'); +import { BlockchainClient } from './blockchain'; + +let blockchainProcess; + +class BlockchainProcess extends ProcessWrapper { + constructor(options) { + super(); + this.blockchainConfig = options.blockchainConfig; + this.client = options.client; + this.env = options.env; + this.isDev = options.isDev; + this.certOptions = options.certOptions; + + i18n.setOrDetectLocale(options.locale); + + this.blockchainConfig.silent = true; + this.blockchain = BlockchainClient( + this.blockchainConfig, + { + clientName: this.client, + env: this.env, + certOptions: this.certOptions, + onReadyCallback: this.blockchainReady.bind(this), + onExitCallback: this.blockchainExit.bind(this), + logger: console + } + ); + + this.blockchain.run(); + } + + blockchainReady() { + blockchainProcess.send({result: constants.blockchain.blockchainReady}); + } + + blockchainExit() { + // tell our parent process that ethereum client has exited + blockchainProcess.send({result: constants.blockchain.blockchainExit}); + } + + kill() { + this.blockchain.kill(); + } +} + +process.on('message', (msg) => { + if (msg === 'exit') { + return blockchainProcess.kill(); + } + if (msg.action === constants.blockchain.init) { + blockchainProcess = new BlockchainProcess(msg.options); + return blockchainProcess.send({result: constants.blockchain.initiated}); + } +}); diff --git a/packages/embark/src/lib/modules/parity/blockchainProcessLauncher.js b/packages/embark/src/lib/modules/parity/blockchainProcessLauncher.js new file mode 100644 index 000000000..19ef09afe --- /dev/null +++ b/packages/embark/src/lib/modules/parity/blockchainProcessLauncher.js @@ -0,0 +1,83 @@ +import { __ } from 'embark-i18n'; +import { ProcessLauncher } from 'embark-core'; +import { joinPath } from 'embark-utils'; +const constants = require('embark-core/constants'); + +export class BlockchainProcessLauncher { + + constructor (options) { + this.events = options.events; + this.logger = options.logger; + this.normalizeInput = options.normalizeInput; + this.blockchainConfig = options.blockchainConfig; + this.locale = options.locale; + this.isDev = options.isDev; + this.client = options.client; + this.embark = options.embark; + } + + processEnded(code) { + this.logger.error(__('Blockchain process ended before the end of this process. Try running blockchain in a separate process using `$ embark blockchain`. Code: %s', code)); + } + + startBlockchainNode(readyCb) { + this.logger.info(__('Starting Blockchain node in another process').cyan); + + this.blockchainProcess = new ProcessLauncher({ + name: 'blockchain', + modulePath: joinPath(__dirname, './blockchainProcess.js'), + logger: this.logger, + events: this.events, + silent: this.logger.logLevel !== 'trace', + exitCallback: this.processEnded.bind(this), + embark: this.embark + }); + this.blockchainProcess.send({ + action: constants.blockchain.init, options: { + blockchainConfig: this.blockchainConfig, + client: this.client, + env: this.env, + isDev: this.isDev, + locale: this.locale, + certOptions: this.embark.config.webServerConfig.certOptions, + events: this.events + } + }); + + this.blockchainProcess.once('result', constants.blockchain.blockchainReady, () => { + this.logger.info(__('Blockchain node is ready').cyan); + readyCb() + // this.events.emit(constants.blockchain.blockchainReady); + }); + + this.blockchainProcess.once('result', constants.blockchain.blockchainExit, () => { + // tell everyone that our blockchain process (ie geth) died + this.events.emit(constants.blockchain.blockchainExit); + + // then kill off the blockchain process + this.blockchainProcess.kill(); + }); + + this.events.on('logs:ethereum:enable', () => { + this.blockchainProcess.silent = false; + }); + + this.events.on('logs:ethereum:disable', () => { + this.blockchainProcess.silent = true; + }); + + this.events.on('exit', () => { + this.blockchainProcess.send('exit'); + }); + } + + stopBlockchainNode(cb) { + if(this.blockchainProcess) { + this.events.once(constants.blockchain.blockchainExit, cb); + this.blockchainProcess.exitCallback = () => {}; // don't show error message as the process was killed on purpose + this.blockchainProcess.send('exit'); + } + } + +} + diff --git a/packages/embark/src/lib/modules/parity/check.js b/packages/embark/src/lib/modules/parity/check.js new file mode 100644 index 000000000..6ca48b45a --- /dev/null +++ b/packages/embark/src/lib/modules/parity/check.js @@ -0,0 +1,47 @@ +const WebSocket = require("ws"); +const http = require("http"); + +const LIVENESS_CHECK=`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":42}`; + +const parseAndRespond = (data, cb) => { + const resp = JSON.parse(data); + const [_, version, __] = resp.result.split('/'); + cb(null, version); +} + +const rpc = (host, port, cb) => { + const options = { + hostname: host, // TODO(andremedeiros): get from config + port: port, // TODO(andremedeiros): get from config + method: "POST", + timeout: 1000, + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(LIVENESS_CHECK) + } + }; + + const req = http.request(options, (res) => { + let data = ""; + res.on("data", chunk => data = data + chunk); + res.on("end", () => parseAndRespond(data, cb)); + }); + req.on("error", (e) => cb(e)); + req.write(LIVENESS_CHECK); + req.end(); +} + +const ws = (host, port, cb) => { + const conn = new WebSocket("ws://" + host + ":" + port); + conn.on("message", (data) => { + parseAndRespond(data, cb) + conn.close(); + }); + conn.on("open", () => conn.send(LIVENESS_CHECK)); + conn.on("error", (e) => cb(e)); +} + +module.exports = { + ws, + rpc +} diff --git a/packages/embark/src/lib/modules/parity/index.js b/packages/embark/src/lib/modules/parity/index.js new file mode 100644 index 000000000..224052658 --- /dev/null +++ b/packages/embark/src/lib/modules/parity/index.js @@ -0,0 +1,103 @@ +const {normalizeInput} = require('embark-utils'); +import {BlockchainProcessLauncher} from './blockchainProcessLauncher'; +import {ws, rpc} from './check.js'; +const constants = require('embark-core/constants'); + +class Parity { + + constructor(embark, options) { + this.embark = embark; + this.embarkConfig = embark.config.embarkConfig; + this.blockchainConfig = embark.config.blockchainConfig; + this.locale = options.locale; + this.logger = embark.logger; + this.client = options.client; + this.isDev = options.isDev; + this.events = embark.events; + this.plugins = options.plugins; + // let plugin = this.plugins.createPlugin('gethplugin', {}); + + if (!this.shouldInit()) { + return; + } + + this.events.request("blockchain:node:register", constants.blockchain.clients.parity, (readyCb) => { + console.dir('registering blockchain node'); + console.dir(readyCb); + this.events.request('processes:register', 'blockchain', { + launchFn: (cb) => { + // this.startBlockchainNode(readyCb); + this.startBlockchainNode(cb); + }, + stopFn: (cb) => { + this.stopBlockchainNode(cb); + } + }); + this.events.request("processes:launch", "blockchain", (err) => { + if (err) { + this.logger.error(`Error launching blockchain process: ${err.message || err}`); + } + readyCb(); + }); + this.registerServiceCheck(); + }); + } + + shouldInit() { + return ( + this.blockchainConfig.client === constants.blockchain.clients.geth && + this.blockchainConfig.enabled + ); + } + + _getNodeState(err, version, cb) { + if (err) return cb({name: "Ethereum node not found", status: 'off'}); + + let nodeName = "parity"; + let versionNumber = version.split("-")[0]; + let name = nodeName + " " + versionNumber + " (Ethereum)"; + return cb({name, status: 'on'}); + } + + // TODO: need to get correct port taking into account the proxy + registerServiceCheck() { + this.events.request("services:register", 'Ethereum', (cb) => { + const {rpcHost, rpcPort, wsRPC, wsHost, wsPort} = this.blockchainConfig; + if (wsRPC) { + return ws(wsHost, wsPort + 10, (err, version) => this._getNodeState(err, version, cb)); + } + rpc(rpcHost, rpcPort + 10, (err, version) => this._getNodeState(err, version, cb)); + }, 5000, 'off'); + } + + startBlockchainNode(callback) { + this.blockchainProcess = new BlockchainProcessLauncher({ + events: this.events, + logger: this.logger, + normalizeInput, + blockchainConfig: this.blockchainConfig, + locale: this.locale, + client: this.client, + isDev: this.isDev, + embark: this.embark + }); + + this.blockchainProcess.startBlockchainNode(callback); + } + + stopBlockchainNode(cb) { + const message = __(`The blockchain process has been stopped. It can be restarted by running ${"service blockchain on".bold} in the Embark console.`); + + if (!this.blockchainProcess) { + return cb(); + } + + this.blockchainProcess.stopBlockchainNode(() => { + this.logger.info(message); + cb(); + }); + } + +} + +module.exports = Parity; diff --git a/packages/embark/src/lib/modules/parity/miner.js b/packages/embark/src/lib/modules/parity/miner.js new file mode 100644 index 000000000..a971c9dbb --- /dev/null +++ b/packages/embark/src/lib/modules/parity/miner.js @@ -0,0 +1,319 @@ +const async = require('async'); +const NetcatClient = require('netcat/client'); + +import { ipcPath } from 'embark-utils'; + +//Constants +const minerStart = 'miner_start'; +const minerStop = 'miner_stop'; +const getHashRate = 'miner_getHashrate'; +const getCoinbase = 'eth_coinbase'; +const getBalance = 'eth_getBalance'; +const newBlockFilter = 'eth_newBlockFilter'; +const pendingBlockFilter = 'eth_newPendingTransactionFilter'; +const getChanges = 'eth_getFilterChanges'; +const getBlockCount = 'eth_getBlockTransactionCountByNumber'; + +class GethMiner { + constructor(options) { + const self = this; + // TODO: Find a way to load mining config from YML. + // In the meantime, just set an empty config object + this.config = {}; + this.datadir = options.datadir; + self.interval = null; + self.callback = null; + self.started = null; + + self.commandQueue = async.queue((task, callback) => { + self.callback = callback; + self.client.send(JSON.stringify({"jsonrpc": "2.0", "method": task.method, "params": task.params || [], "id": 1})); + }, 1); + + const defaults = { + interval_ms: 15000, + initial_ether: 15000000000000000000, + mine_pending_txns: true, + mine_periodically: false, + mine_normally: false, + threads: 1 + }; + + for (let key in defaults) { + if (this.config[key] === undefined) { + this.config[key] = defaults[key]; + } + } + + this.client = new NetcatClient(); + this.client.unixSocket(ipcPath('geth.ipc', true)) + .enc('utf8') + .connect() + .on('data', (response) => { + try { + response = JSON.parse(response); + } catch (e) { + console.error(e); + return; + } + if (self.callback) { + self.callback(response.error, response.result); + } + }); + + if (this.config.mine_normally) { + this.startMiner(); + return; + } + + self.stopMiner(() => { + self.fundAccount(function (err) { + if (err) { + console.error(err); + return; + } + if (self.config.mine_periodically) self.start_periodic_mining(); + if (self.config.mine_pending_txns) self.start_transaction_mining(); + }); + }); + + } + + sendCommand(method, params, callback) { + if (typeof params === 'function') { + callback = params; + params = []; + } + if (!callback) { + callback = function () { + }; + } + this.commandQueue.push({method, params: params || []}, callback); + } + + startMiner(callback) { + if (this.started) { + if (callback) { + callback(); + } + return; + } + this.started = true; + this.sendCommand(minerStart, callback); + } + + stopMiner(callback) { + if (!this.started) { + if (callback) { + callback(); + } + return; + } + this.started = false; + this.sendCommand(minerStop, callback); + } + + getCoinbase(callback) { + if (this.coinbase) { + return callback(null, this.coinbase); + } + this.sendCommand(getCoinbase, (err, result) => { + if (err) { + return callback(err); + } + this.coinbase = result; + if (!this.coinbase) { + return callback('Failed getting coinbase account'); + } + callback(null, this.coinbase); + }); + } + + accountFunded(callback) { + const self = this; + self.getCoinbase((err, coinbase) => { + if (err) { + return callback(err); + } + self.sendCommand(getBalance, [coinbase, 'latest'], (err, result) => { + if (err) { + return callback(err); + } + callback(null, parseInt(result, 16) >= self.config.initial_ether); + }); + }); + } + + watchBlocks(filterCommand, callback, delay) { + const self = this; + self.sendCommand(filterCommand, (err, filterId) => { + if (err) { + return callback(err); + } + self.interval = setInterval(() => { + self.sendCommand(getChanges, [filterId], (err, changes) => { + if (err) { + console.error(err); + return; + } + if (!changes || !changes.length) { + return; + } + callback(null, changes); + }); + }, delay || 1000); + }); + } + + mineUntilFunded(callback) { + const self = this; + this.startMiner(); + self.watchBlocks(newBlockFilter, (err) => { + if (err) { + console.error(err); + return; + } + self.accountFunded((err, funded) => { + if (funded) { + clearTimeout(self.interval); + self.stopMiner(); + callback(); + } + }); + }); + } + + fundAccount(callback) { + const self = this; + + self.accountFunded((err, funded) => { + if (err) { + return callback(err); + } + if (funded) { + return callback(); + } + + console.log("== Funding account"); + self.mineUntilFunded(callback); + }); + } + + pendingTransactions(callback) { + const self = this; + self.sendCommand(getBlockCount, ['pending'], (err, hexCount) => { + if (err) { + return callback(err); + } + callback(null, parseInt(hexCount, 16)); + }); + } + + start_periodic_mining() { + const self = this; + const WAIT = 'wait'; + let last_mined_ms = Date.now(); + let timeout_set = false; + let next_block_in_ms; + + self.startMiner(); + self.watchBlocks(newBlockFilter, (err) => { + if (err) { + console.error(err); + return; + } + if (timeout_set) { + return; + } + async.waterfall([ + function checkPendingTransactions(next) { + if (!self.config.mine_pending_txns) { + return next(); + } + self.pendingTransactions((err, count) => { + if (err) { + return next(err); + } + if (count) { + return next(WAIT); + } + next(); + }); + }, + function stopMiner(next) { + timeout_set = true; + + const now = Date.now(); + const ms_since_block = now - last_mined_ms; + last_mined_ms = now; + + if (ms_since_block > self.config.interval_ms) { + next_block_in_ms = 0; + } else { + next_block_in_ms = (self.config.interval_ms - ms_since_block); + } + self.stopMiner(); + console.log("== Looking for next block in " + next_block_in_ms + "ms"); + next(); + }, + function startAfterTimeout(next) { + setTimeout(function () { + console.log("== Looking for next block"); + timeout_set = false; + self.startMiner(); + next(); + }, next_block_in_ms); + } + ], (err) => { + if (err === WAIT) { + return; + } + if (err) { + console.error(err); + } + }); + }); + } + + start_transaction_mining() { + const self = this; + const pendingTrasactionsMessage = "== Pending transactions! Looking for next block..."; + self.watchBlocks(pendingBlockFilter, (err) => { + if (err) { + console.error(err); + return; + } + self.sendCommand(getHashRate, (err, result) => { + if (result > 0) return; + + console.log(pendingTrasactionsMessage); + self.startMiner(); + }); + }, 2000); + + if (self.config.mine_periodically) return; + + self.watchBlocks(newBlockFilter, (err) => { + if (err) { + console.error(err); + return; + } + self.pendingTransactions((err, count) => { + if (err) { + console.error(err); + return; + } + if (!count) { + console.log("== No transactions left. Stopping miner..."); + self.stopMiner(); + } else { + console.log(pendingTrasactionsMessage); + self.startMiner(); + } + }); + }, 2000); + } +} + +module.exports = GethMiner; + diff --git a/packages/embark/src/lib/modules/parity/parityClient.js b/packages/embark/src/lib/modules/parity/parityClient.js new file mode 100644 index 000000000..2398a5914 --- /dev/null +++ b/packages/embark/src/lib/modules/parity/parityClient.js @@ -0,0 +1,407 @@ +import {__} from 'embark-i18n'; +import {dappPath} from 'embark-utils'; +import * as fs from 'fs-extra'; +const async = require('async'); +const path = require('path'); +const os = require('os'); +const semver = require('semver'); +const constants = require('embark-core/constants'); + +const DEFAULTS = { + "BIN": "parity", + "VERSIONS_SUPPORTED": ">=2.0.0", + "NETWORK_TYPE": "dev", + "NETWORK_ID": 17, + "RPC_API": ["web3", "eth", "pubsub", "net", "parity", "private", "parity_pubsub", "traces", "rpc", "shh", "shh_pubsub"], + "WS_API": ["web3", "eth", "pubsub", "net", "parity", "private", "parity_pubsub", "traces", "rpc", "shh", "shh_pubsub"], + "DEV_WS_API": ["web3", "eth", "pubsub", "net", "parity", "private", "parity_pubsub", "traces", "rpc", "shh", "shh_pubsub", "personal"], + "TARGET_GAS_LIMIT": 8000000, + "DEV_ACCOUNT": "0x00a329c0648769a73afac7f9381e08fb43dbea72", + "DEV_WALLET": { + "id": "d9460e00-6895-8f58-f40c-bb57aebe6c00", + "version": 3, + "crypto": { + "cipher": "aes-128-ctr", + "cipherparams": {"iv": "74245f453143f9d06a095c6e6e309d5d"}, + "ciphertext": "2fa611c4aa66452ef81bd1bd288f9d1ed14edf61aa68fc518093f97c791cf719", + "kdf": "pbkdf2", + "kdfparams": {"c": 10240, "dklen": 32, "prf": "hmac-sha256", "salt": "73b74e437a1144eb9a775e196f450a23ab415ce2c17083c225ddbb725f279b98"}, + "mac": "f5882ae121e4597bd133136bf15dcbcc1bb2417a25ad205041a50c59def812a8" + }, + "address": "00a329c0648769a73afac7f9381e08fb43dbea72", + "name": "Development Account", + "meta": "{\"description\":\"Never use this account outside of development chain!\",\"passwordHint\":\"Password is empty string\"}" + } +}; + +const safePush = function (set, value) { + if (set.indexOf(value) === -1) { + set.push(value); + } +}; + +class ParityClient { + + static get DEFAULTS() { + return DEFAULTS; + } + + constructor(options) { + this.config = options && options.hasOwnProperty('config') ? options.config : {}; + this.env = options && options.hasOwnProperty('env') ? options.env : 'development'; + this.isDev = options && options.hasOwnProperty('isDev') ? options.isDev : (this.env === 'development'); + this.name = constants.blockchain.clients.parity; + this.prettyName = "Parity-Ethereum (https://github.com/paritytech/parity-ethereum)"; + this.bin = this.config.ethereumClientBin || DEFAULTS.BIN; + this.versSupported = DEFAULTS.VERSIONS_SUPPORTED; + } + + isReady(data) { + return data.indexOf('Public node URL') > -1; + } + + /** + * Check if the client needs some sort of 'keep alive' transactions to avoid freezing by inactivity + * @returns {boolean} if keep alive is needed + */ + needKeepAlive() { + return false; + } + + commonOptions() { + let config = this.config; + let cmd = []; + + cmd.push(this.determineNetworkType(config)); + + if (config.networkId) { + cmd.push(`--network-id=${config.networkId}`); + } + + if (config.datadir) { + cmd.push(`--base-path=${config.datadir}`); + } + + if (config.syncMode === 'light') { + cmd.push("--light"); + } else if (config.syncMode === 'fast') { + cmd.push("--pruning=fast"); + } else if (config.syncMode === 'full') { + cmd.push("--pruning=archive"); + } + + // In dev mode we store all users passwords in the devPassword file, so Parity can unlock all users from the start + if (this.isDev) cmd.push(`--password=${config.account.devPassword}`); + else if (config.account && config.account.password) { + cmd.push(`--password=${config.account.password}`); + } + + if (Number.isInteger(config.verbosity) && config.verbosity >= 0 && config.verbosity <= 5) { + switch (config.verbosity) { + case 0: // No option to silent Parity, go to less verbose + case 1: + cmd.push("--logging=error"); + break; + case 2: + cmd.push("--logging=warn"); + break; + case 3: + cmd.push("--logging=info"); + break; + case 4: // Debug is the max verbosity for Parity + case 5: + cmd.push("--logging=debug"); + break; + default: + cmd.push("--logging=info"); + break; + } + } + + if (this.runAsArchival(config)) { + cmd.push("--pruning=archive"); + } + + return cmd; + } + + getMiner() { + console.warn(__("Miner requested, but Parity does not embed a miner! Use Geth or install ethminer (https://github.com/ethereum-mining/ethminer)").yellow); + return; + } + + getBinaryPath() { + return this.bin; + } + + determineVersionCommand() { + return this.bin + " --version"; + } + + parseVersion(rawVersionOutput) { + let parsed; + const match = rawVersionOutput.match(/version Parity(?:-Ethereum)?\/(.*?)\//); + if (match) { + parsed = match[1].trim(); + } + return parsed; + } + + runAsArchival(config) { + return config.networkId === 1337 || config.archivalMode; + } + + isSupportedVersion(parsedVersion) { + let test; + try { + let v = semver(parsedVersion); + v = `${v.major}.${v.minor}.${v.patch}`; + test = semver.Range(this.versSupported).test(semver(v)); + if (typeof test !== 'boolean') { + test = undefined; + } + } finally { + // eslint-disable-next-line no-unsafe-finally + return test; + } + } + + determineNetworkType(config) { + if (this.isDev) { + return "--chain=dev"; + } + if (config.networkType === 'rinkeby') { + console.warn(__('Parity does not support the Rinkeby PoA network, switching to Kovan PoA network')); + config.networkType = 'kovan'; + } else if (config.networkType === 'testnet') { + console.warn(__('Parity "testnet" corresponds to Kovan network, switching to Ropsten to be compliant with Geth parameters')); + config.networkType = "ropsten"; + } + if (config.genesisBlock) { + config.networkType = config.genesisBlock; + } + return "--chain=" + config.networkType; + } + + newAccountCommand() { + return this.bin + " " + this.commonOptions().join(' ') + " account new "; + } + + parseNewAccountCommandResultToAddress(data = "") { + return data.replace(/^\n|\n$/g, ""); + } + + listAccountsCommand() { + return this.bin + " " + this.commonOptions().join(' ') + " account list "; + } + + parseListAccountsCommandResultToAddress(data = "") { + return data.replace(/^\n|\n$/g, "").split('\n')[0]; + } + + parseListAccountsCommandResultToAddressList(data = "") { + const list = data.split('\n'); + return list.filter(acc => acc); + } + + parseListAccountsCommandResultToAddressCount(data = "") { + const count = this.parseListAccountsCommandResultToAddressList(data).length; + return (count > 0 ? count : 0); + } + + determineRpcOptions(config) { + let cmd = []; + cmd.push("--port=" + config.port); + cmd.push("--jsonrpc-port=" + config.rpcPort); + cmd.push("--jsonrpc-interface=" + (config.rpcHost === 'localhost' ? 'local' : config.rpcHost)); + if (config.rpcCorsDomain) { + if (config.rpcCorsDomain === '*') { + console.warn('=================================='); + console.warn(__('rpcCorsDomain set to "all"')); + console.warn(__('make sure you know what you are doing')); + console.warn('=================================='); + } + cmd.push("--jsonrpc-cors=" + (config.rpcCorsDomain === '*' ? 'all' : config.rpcCorsDomain)); + } else { + console.warn('=================================='); + console.warn(__('warning: cors is not set')); + console.warn('=================================='); + } + cmd.push("--jsonrpc-hosts=all"); + return cmd; + } + + determineWsOptions(config) { + let cmd = []; + if (config.wsRPC) { + cmd.push("--ws-port=" + config.wsPort); + cmd.push("--ws-interface=" + (config.wsHost === 'localhost' ? 'local' : config.wsHost)); + if (config.wsOrigins) { + const origins = config.wsOrigins.split(','); + if (origins.includes('*') || origins.includes("all")) { + console.warn('=================================='); + console.warn(__('wsOrigins set to "all"')); + console.warn(__('make sure you know what you are doing')); + console.warn('=================================='); + cmd.push("--ws-origins=all"); + } else { + cmd.push("--ws-origins=" + config.wsOrigins); + } + } else { + console.warn('=================================='); + console.warn(__('warning: wsOrigins is not set')); + console.warn('=================================='); + } + cmd.push("--ws-hosts=all"); + } + return cmd; + } + + initDevChain(datadir, callback) { + // Parity requires specific initialization also for the dev chain + const self = this; + const keysDataDir = datadir + '/keys/DevelopmentChain'; + async.waterfall([ + function makeDir(next) { + fs.mkdirp(keysDataDir, (err, _result) => { + next(err); + }); + }, + function createDevAccount(next) { + self.createDevAccount(keysDataDir, next); + }, + function mkDevPasswordDir(next) { + fs.mkdirp(path.dirname(self.config.account.devPassword), (err, _result) => { + next(err); + }); + }, + function getText(next) { + if (!self.config.account.password) { + return next(null, os.EOL + 'dev_password'); + } + fs.readFile(dappPath(self.config.account.password), {encoding: 'utf8'}, (err, content) => { + next(err, os.EOL + content); + }); + }, + function updatePasswordFile(passwordList, next) { + fs.writeFile(self.config.account.devPassword, passwordList, next); + } + ], (err) => { + callback(err); + }); + } + + createDevAccount(keysDataDir, cb) { + const devAccountWallet = keysDataDir + '/dev.wallet'; + fs.writeFile(devAccountWallet, JSON.stringify(DEFAULTS.DEV_WALLET), function (err) { + if (err) { + return cb(err); + } + cb(); + }); + } + + mainCommand(address, done) { + let self = this; + let config = this.config; + let rpc_api = this.config.rpcApi; + let ws_api = this.config.wsApi; + let args = []; + async.series([ + function commonOptions(callback) { + let cmd = self.commonOptions(); + args = args.concat(cmd); + callback(null, cmd); + }, + function rpcOptions(callback) { + let cmd = self.determineRpcOptions(self.config); + args = args.concat(cmd); + callback(null, cmd); + }, + function wsOptions(callback) { + let cmd = self.determineWsOptions(self.config); + args = args.concat(cmd); + callback(null, cmd); + }, + function dontGetPeers(callback) { + if (config.nodiscover) { + args.push("--no-discovery"); + return callback(null, "--no-discovery"); + } + callback(null, ""); + }, + function vmDebug(callback) { + if (config.vmdebug) { + args.push("--tracing on"); + return callback(null, "--tracing on"); + } + callback(null, ""); + }, + function maxPeers(callback) { + let cmd = "--max-peers=" + config.maxpeers; + args.push(cmd); + callback(null, cmd); + }, + function bootnodes(callback) { + if (config.bootnodes && config.bootnodes !== "" && config.bootnodes !== []) { + args.push("--bootnodes=" + config.bootnodes); + return callback(null, "--bootnodes=" + config.bootnodes); + } + callback(""); + }, + function whisper(callback) { + if (config.whisper) { + safePush(rpc_api, 'shh'); + safePush(rpc_api, 'shh_pubsub'); + safePush(ws_api, 'shh'); + safePush(ws_api, 'shh_pubsub'); + args.push("--whisper"); + return callback(null, "--whisper"); + } + callback(""); + }, + function rpcApi(callback) { + args.push('--jsonrpc-apis=' + rpc_api.join(',')); + callback(null, '--jsonrpc-apis=' + rpc_api.join(',')); + }, + function wsApi(callback) { + args.push('--ws-apis=' + ws_api.join(',')); + callback(null, '--ws-apis=' + ws_api.join(',')); + }, + function accountToUnlock(callback) { + if (self.isDev) { + let unlockAddressList = self.config.unlockAddressList ? self.config.unlockAddressList : DEFAULTS.DEV_ACCOUNT; + args.push("--unlock=" + unlockAddressList); + return callback(null, "--unlock=" + unlockAddressList); + } + let accountAddress = ""; + if (config.account && config.account.address) { + accountAddress = config.account.address; + } else { + accountAddress = address; + } + if (accountAddress && !self.isDev) { + args.push("--unlock=" + accountAddress); + return callback(null, "--unlock=" + accountAddress); + } + callback(null, ""); + }, + function gasLimit(callback) { + if (config.targetGasLimit) { + args.push("--gas-floor-target=" + config.targetGasLimit); + return callback(null, "--gas-floor-target=" + config.targetGasLimit); + } + // Default Parity gas limit is 4700000: let's set to the geth default + args.push("--gas-floor-target=" + DEFAULTS.TARGET_GAS_LIMIT); + return callback(null, "--gas-floor-target=" + DEFAULTS.TARGET_GAS_LIMIT); + } + ], function (err) { + if (err) { + throw new Error(err.message); + } + return done(self.bin, args); + }); + } +} + +module.exports = ParityClient;