const fs = require('./fs.js'); const File = require('./file.js'); const Plugins = require('./plugins.js'); const utils = require('../utils/utils.js'); const path = require('path'); const deepEqual = require('deep-equal'); const web3 = require('web3'); const constants = require('../constants'); const {canonicalHost, defaultHost} = require('../utils/host'); const cloneDeep = require('lodash.clonedeep'); const DEFAULT_CONFIG_PATH = 'config/'; const unitRegex = /([0-9]+) ([a-zA-Z]+)/; var Config = function(options) { const self = this; this.env = options.env || 'default'; this.blockchainConfig = {}; this.contractsConfig = {}; this.pipelineConfig = {}; this.webServerConfig = options.webServerConfig; this.chainTracker = {}; this.assetFiles = {}; this.contractsFiles = []; this.configDir = options.configDir || DEFAULT_CONFIG_PATH; this.chainsFile = options.chainsFile || './chains.json'; this.plugins = options.plugins; this.logger = options.logger; this.events = options.events; this.embarkConfig = {}; this.context = options.context || [constants.contexts.any]; this.shownNoAccountConfigMsg = false; // flag to ensure "no account config" message is only displayed once to the user this.corsParts = []; this.events.setCommandHandler("config:cors:add", (url) => { this.corsParts.push(url); this._updateBlockchainCors(); }); self.events.setCommandHandler("config:contractsConfig", (cb) => { cb(self.contractsConfig); }); self.events.setCommandHandler("config:contractsConfig:set", (config, cb) => { self.contractsConfig = config; cb(); }); self.events.setCommandHandler("config:contractsFiles", (cb) => { cb(self.contractsFiles); }); // TODO: refactor this so reading the file can be done with a normal resolver or something that takes advantage of the plugin api self.events.setCommandHandler("config:contractsFiles:add", (filename, resolver) => { resolver = resolver || function(callback) { callback(fs.readFileSync(filename).toString()); }; self.contractsFiles.push(new File({filename, type: File.types.custom, path: filename, resolver})); }); }; Config.prototype.loadConfigFiles = function(options) { var interceptLogs = options.interceptLogs; if (options.interceptLogs === undefined) { interceptLogs = true; } if (!fs.existsSync(options.embarkConfig)){ this.logger.error(__('Cannot find file %s Please ensure you are running this command inside the Dapp folder', options.embarkConfig)); process.exit(1); } this.embarkConfig = fs.readJSONSync(options.embarkConfig); this.embarkConfig.plugins = this.embarkConfig.plugins || {}; this.plugins = new Plugins({plugins: this.embarkConfig.plugins, logger: this.logger, interceptLogs: interceptLogs, events: this.events, config: this, context: this.context, env: this.env}); this.plugins.loadPlugins(); this.loadEmbarkConfigFile(); this.loadBlockchainConfigFile(); this.loadStorageConfigFile(); this.loadCommunicationConfigFile(); this.loadNameSystemConfigFile(); this.loadPipelineConfigFile(); this.loadAssetFiles(); this.loadContractsConfigFile(); this.loadExternalContractsFiles(); this.loadWebServerConfigFile(); this.loadChainTrackerFile(); this.loadPluginContractFiles(); this._updateBlockchainCors(); }; Config.prototype.reloadConfig = function() { this.loadEmbarkConfigFile(); this.loadBlockchainConfigFile(); this.loadStorageConfigFile(); this.loadCommunicationConfigFile(); this.loadNameSystemConfigFile(); this.loadPipelineConfigFile(); this.loadAssetFiles(); this.loadContractsConfigFile(); this.loadExternalContractsFiles(); this.loadChainTrackerFile(); this._updateBlockchainCors(); }; Config.prototype._updateBlockchainCors = function(){ let blockchainConfig = this.blockchainConfig; let storageConfig = this.storageConfig; let webServerConfig = this.webServerConfig; let corsParts = cloneDeep(this.corsParts); if(webServerConfig && webServerConfig.host) { corsParts.push(utils.buildUrlFromConfig(webServerConfig)); } if(storageConfig && storageConfig.enabled) { // if getUrl is specified in the config, that needs to be included in cors // instead of the concatenated protocol://host:port if(storageConfig.upload.getUrl) { // remove /ipfs or /bzz: from getUrl if it's there let getUrlParts = storageConfig.upload.getUrl.split('/'); getUrlParts = getUrlParts.slice(0, 3); let host = canonicalHost(getUrlParts[2].split(':')[0]); let port = getUrlParts[2].split(':')[1]; getUrlParts[2] = port ? [host, port].join(':') : host; corsParts.push(getUrlParts.join('/')); } // use our modified getUrl or in case it wasn't specified, use a built url else{ corsParts.push(utils.buildUrlFromConfig(storageConfig.upload)); } } // add whisper cors if(this.communicationConfig && this.communicationConfig.enabled && this.communicationConfig.provider === 'whisper'){ corsParts.push('http://embark'); } let cors = corsParts.join(','); if(blockchainConfig.rpcCorsDomain === 'auto'){ if(cors.length) blockchainConfig.rpcCorsDomain = cors; else blockchainConfig.rpcCorsDomain = ''; } if(blockchainConfig.wsOrigins === 'auto'){ if(cors.length) blockchainConfig.wsOrigins = cors; else blockchainConfig.wsOrigins = ''; } }; Config.prototype._mergeConfig = function(configFilePath, defaultConfig, env, enabledByDefault) { if (!configFilePath) { let configToReturn = defaultConfig['default'] || {}; configToReturn.enabled = enabledByDefault || false; return configToReturn; } // due to embark.json; TODO: refactor this configFilePath = configFilePath.replace('.json','').replace('.js', ''); if (!fs.existsSync(configFilePath + '.js') && !fs.existsSync(configFilePath + '.json')) { // TODO: remove this if if (this.logger) { this.logger.warn(__("no config file found at %s using default config", configFilePath)); } return defaultConfig['default'] || {}; } let config; if (fs.existsSync(configFilePath + '.js')) { delete require.cache[fs.dappPath(configFilePath + '.js')]; config = require(fs.dappPath(configFilePath + '.js')); } else { config = fs.readJSONSync(configFilePath + '.json'); } let configObject = utils.recursiveMerge(defaultConfig, config); if (env) { return utils.recursiveMerge(configObject['default'] || {}, configObject[env]); } return configObject; }; Config.prototype._getFileOrOject = function(object, filePath, property) { if (typeof (this.configDir) === 'object') { return this.configDir[property]; } return utils.joinPath(this.configDir, filePath); }; Config.prototype.loadBlockchainConfigFile = function() { var configObject = { "default": { "enabled": true, "rpcCorsDomain": "auto", "wsOrigins": "auto" } }; let configFilePath = this._getFileOrOject(this.configDir, 'blockchain', 'blockchain'); this.blockchainConfig = this._mergeConfig(configFilePath, configObject, this.env, true); if (!configFilePath) { this.blockchainConfig.default = true; } if (this.blockchainConfig.targetGasLimit && this.blockchainConfig.targetGasLimit.toString().match(unitRegex)) { this.blockchainConfig.targetGasLimit = utils.getWeiBalanceFromString(this.blockchainConfig.targetGasLimit, web3); } if (this.blockchainConfig.gasPrice && this.blockchainConfig.gasPrice.toString().match(unitRegex)) { this.blockchainConfig.gasPrice = utils.getWeiBalanceFromString(this.blockchainConfig.gasPrice, web3); } if (this.blockchainConfig.account && this.blockchainConfig.account.balance && this.blockchainConfig.account.balance.toString().match(unitRegex)) { this.blockchainConfig.account.balance = utils.getWeiBalanceFromString(this.blockchainConfig.account.balance, web3); } if ( !this.shownNoAccountConfigMsg && (/rinkeby|testnet|livenet/).test(this.blockchainConfig.networkType) && !(this.blockchainConfig.account && this.blockchainConfig.account.address && this.blockchainConfig.account.password) && !this.blockchainConfig.isDev && this.env !== 'development' && this.env !== 'test') { this.logger.warn(( '\n=== ' + __('Cannot unlock account - account config missing').bold + ' ===\n' + __('Geth is configured to sync to a testnet/livenet and needs to unlock an account ' + 'to allow your dApp to interact with geth, however, the address and password must ' + 'be specified in your blockchain config. Please update your blockchain config with ' + 'a valid address and password: \n') + ` - config/blockchain.js > ${this.env} > account\n\n`.italic + __('Please also make sure the keystore file for the account is located at: ') + '\n - Mac: ' + `~/Library/Ethereum/${this.env}/keystore`.italic + '\n - Linux: ' + `~/.ethereum/${this.env}/keystore`.italic + '\n - Windows: ' + `%APPDATA%\\Ethereum\\${this.env}\\keystore`.italic) + __('\n\nAlternatively, you could change ' + `config/blockchain.js > ${this.env} > networkType`.italic + __(' to ') + '"custom"\n'.italic).yellow ); this.shownNoAccountConfigMsg = true; } this.events.emit('config:load:blockchain', this.blockchainConfig); }; Config.prototype.loadContractsConfigFile = function() { var defaultVersions = { "web3": "1.0.0-beta", "solc": "0.4.25" }; var versions = utils.recursiveMerge(defaultVersions, this.embarkConfig.versions || {}); var configObject = { "default": { "versions": versions, "deployment": { "host": "localhost", "port": 8545, "type": "rpc" }, "dappConnection": [ "$WEB3", "localhost:8545" ], "gas": "auto", "contracts": { } } }; var contractsConfigs = this.plugins.getPluginsProperty('contractsConfig', 'contractsConfigs'); contractsConfigs.forEach(function(pluginConfig) { configObject = utils.recursiveMerge(configObject, pluginConfig); }); let configFilePath = this._getFileOrOject(this.configDir, 'contracts', 'contracts'); let newContractsConfig = this._mergeConfig(configFilePath, configObject, this.env); if (newContractsConfig.gas.match(unitRegex)) { newContractsConfig.gas = utils.getWeiBalanceFromString(newContractsConfig.gas, web3); } if (newContractsConfig.deployment && 'accounts' in newContractsConfig.deployment) { newContractsConfig.deployment.accounts.forEach((account) => { if (account.balance.match(unitRegex)) { account.balance = utils.getWeiBalanceFromString(account.balance, web3); } }); } Object.keys(newContractsConfig.contracts).forEach(contractName => { let gas = newContractsConfig.contracts[contractName].gas; let gasPrice = newContractsConfig.contracts[contractName].gasPrice; if (gas && gas.match(unitRegex)) { newContractsConfig.contracts[contractName].gas = utils.getWeiBalanceFromString(gas, web3); } if (gasPrice && gasPrice.match(unitRegex)) { newContractsConfig.contracts[contractName].gasPrice = utils.getWeiBalanceFromString(gasPrice, web3); } }); if (!deepEqual(newContractsConfig, this.contractsConfig)) { this.contractsConfig = newContractsConfig; } this.events.emit('config:load:contracts', this.contractsConfig); }; Config.prototype.loadExternalContractsFiles = function() { let contracts = this.contractsConfig.contracts; for (let contractName in contracts) { let contract = contracts[contractName]; if (!contract.file) { continue; } if (contract.file.startsWith('http') || contract.file.startsWith('git')) { const fileObj = utils.getExternalContractUrl(contract.file); if (!fileObj) { return this.logger.error(__("HTTP contract file not found") + ": " + contract.file); } const localFile = fileObj.filePath; this.contractsFiles.push(new File({filename: localFile, type: File.types.http, basedir: '', path: fileObj.url})); } else if (fs.existsSync(contract.file)) { this.contractsFiles.push(new File({filename: contract.file, type: File.types.dapp_file, basedir: '', path: contract.file})); } else if (fs.existsSync(path.join('./node_modules/', contract.file))) { this.contractsFiles.push(new File({filename: path.join('./node_modules/', contract.file), type: File.types.dapp_file, basedir: '', path: path.join('./node_modules/', contract.file)})); } else { this.logger.error(__("contract file not found") + ": " + contract.file); } } }; Config.prototype.loadStorageConfigFile = function() { var versions = utils.recursiveMerge({"ipfs-api": "17.2.4"}, this.embarkConfig.versions || {}); var configObject = { "default": { "versions": versions, "enabled": true, "available_providers": ["ipfs", "swarm"], "ipfs_bin": "ipfs", "upload": { "provider": "ipfs", "protocol": "http", "host" : defaultHost, "port": 5001, "getUrl": "http://localhost:8080/ipfs/" }, "dappConnection": [{"provider": "ipfs", "host": "localhost", "port": 5001, "getUrl": "http://localhost:8080/ipfs/"}] } }; let configFilePath = this._getFileOrOject(this.configDir, 'storage', 'storage'); this.storageConfig = this._mergeConfig(configFilePath, configObject, this.env); }; Config.prototype.loadNameSystemConfigFile = function() { // todo: spec out names for registration in the file itself for a dev chain var configObject = { "default": { "enabled": false } }; let configFilePath = this._getFileOrOject(this.configDir, 'namesystem', 'namesystem'); this.namesystemConfig = this._mergeConfig(configFilePath, configObject, this.env); }; Config.prototype.loadCommunicationConfigFile = function() { var configObject = { "default": { "enabled": true, "provider": "whisper", "available_providers": ["whisper"], "connection": { "host": defaultHost, "port": 8546, "type": "ws" } } }; let configFilePath = this._getFileOrOject(this.configDir, 'communication', 'communication'); this.communicationConfig = this._mergeConfig(configFilePath, configObject, this.env); }; Config.prototype.loadWebServerConfigFile = function() { var configObject = { "enabled": true, "host": defaultHost, "openBrowser": true, "port": 8000, "enableCatchAll": true }; let configFilePath = this._getFileOrOject(this.configDir, 'webserver', 'webserver'); let webServerConfig = this._mergeConfig(configFilePath, configObject, false); if (configFilePath === false) { this.webServerConfig = {enabled: false}; return; } if (this.webServerConfig) { // cli flags to `embark run` should override configFile and defaults (configObject) this.webServerConfig = utils.recursiveMerge(webServerConfig, this.webServerConfig); } else { this.webServerConfig = webServerConfig; } this.events.emit('config:load:webserver', this.webServerConfig); }; Config.prototype.loadEmbarkConfigFile = function() { var configObject = { options: { solc: { "optimize": true, "optimize-runs": 200 } } }; this.embarkConfig = utils.recursiveMerge(configObject, this.embarkConfig); const contracts = this.embarkConfig.contracts; const newContractsFiles = this.loadFiles(contracts); if (!this.contractFiles || newContractsFiles.length !== this.contractFiles.length || !deepEqual(newContractsFiles, this.contractFiles)) { this.contractsFiles = this.contractsFiles.concat(newContractsFiles).filter((file, index, arr) => { return !arr.some((file2, index2) => { return file.filename === file2.filename && index < index2; }); }); } // determine contract 'root' directories this.contractDirectories = contracts.map((dir) => { return dir.split("**")[0]; }).map((dir) => { return dir.split("*.")[0]; }); this.contractDirectories.push(constants.httpContractsDirectory); this.buildDir = this.embarkConfig.buildDir; this.configDir = this.embarkConfig.config; }; Config.prototype.loadPipelineConfigFile = function() { const defaultPipelineConfig = { typescript: false }; let pipelineConfigPath = this._getFileOrOject(this.configDir, 'pipeline', 'pipeline'); // Embark applications in "simple" mode that aren't aware of `pipeline.js` configuration capabilities // won't have a pipeline config path so we need to perform this safety check here, otherwise the // next expression is going to throw. if (pipelineConfigPath !== undefined) { // At this point, `pipelineConfigPath` could be either `config/pipeline` or a filepath including its extension. // We need to make sure that we always have an extension. pipelineConfigPath = `${fs.dappPath(pipelineConfigPath)}${path.extname(pipelineConfigPath) === '.js' ? '' : '.js'}`; } let pipelineConfig = defaultPipelineConfig; if (pipelineConfigPath && fs.existsSync(pipelineConfigPath)) { delete require.cache[pipelineConfigPath]; pipelineConfig = utils.recursiveMerge( utils.recursiveMerge(true, pipelineConfig), require(pipelineConfigPath) ); } this.pipelineConfig = pipelineConfig; }; Config.prototype.loadAssetFiles = function () { Object.keys(this.embarkConfig.app).forEach(targetFile => { this.assetFiles[targetFile] = this.loadFiles(this.embarkConfig.app[targetFile]); }); }; Config.prototype.loadChainTrackerFile = function() { if (!fs.existsSync(this.chainsFile)) { this.logger.info(this.chainsFile + ' ' + __('file not found, creating it...')); fs.writeJSONSync(this.chainsFile, {}); } this.chainTracker = fs.readJSONSync(this.chainsFile); }; function findMatchingExpression(filename, filesExpressions) { for (let fileExpression of filesExpressions) { var matchingFiles = utils.filesMatchingPattern(fileExpression); for (let matchFile of matchingFiles) { if (matchFile === filename) { return path.dirname(fileExpression).replace(/\*/g, ''); } } } return path.dirname(filename); } Config.prototype.loadFiles = function(files) { var self = this; var originalFiles = utils.filesMatchingPattern(files); var readFiles = []; originalFiles.filter(function(file) { return (file[0] === '$' || file.indexOf('.') >= 0); }).filter(function(file) { let basedir = findMatchingExpression(file, files); readFiles.push(new File({filename: file, type: File.types.dapp_file, basedir: basedir, path: file})); }); var filesFromPlugins = []; var filePlugins = self.plugins.getPluginsFor('pipelineFiles'); filePlugins.forEach(function(plugin) { try { var fileObjects = plugin.runFilePipeline(); for (var i=0; i < fileObjects.length; i++) { var fileObject = fileObjects[i]; filesFromPlugins.push(fileObject); } } catch(err) { self.logger.error(err.message); } }); filesFromPlugins.filter(function(file) { if ((file.intendedPath && utils.fileMatchesPattern(files, file.intendedPath)) || utils.fileMatchesPattern(files, file.file)) { readFiles.push(file); } }); return readFiles; }; // NOTE: this doesn't work for internal modules Config.prototype.loadPluginContractFiles = function() { var self = this; var contractsPlugins = this.plugins.getPluginsFor('contractFiles'); contractsPlugins.forEach(function(plugin) { plugin.contractsFiles.forEach(function(file) { var filename = file.replace('./',''); self.contractsFiles.push(new File({filename: filename, pluginPath: plugin.pluginPath, type: File.types.custom, path: filename, resolver: function(callback) { callback(plugin.loadPluginFile(file)); }})); }); }); }; module.exports = Config;