diff --git a/index.js b/index.js index d0b1d4e..f376be0 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,8 @@ module.exports = { - // ENS is deterministically deployed to this address - ens: '0x5f6f7e8cc7346a11ca2def8f827b7a0b612c56a1' -} \ No newline at end of file + // ENS is deterministically deployed to this address + ens: '0x5f6f7e8cc7346a11ca2def8f827b7a0b612c56a1', + ConsoleReporter: require('./src/reporters/ConsoleReporter'), + commands: { + devchain: require('./src/commands/devchain'), + }, +} diff --git a/package.json b/package.json index 9c29841..94816c0 100755 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "@aragon/aragen", - "version": "3.0.0", + "version": "3.0.0-beta.1", "description": "Generate Aragon local dev environment snapshots", "main": "index.js", + "bin": { + "aragen": "./src/cli.js" + }, "scripts": { "prepublishOnly": "npm run start", "start": "npm run start-ganache-bg && npm run get-repos && npm run gen", @@ -33,6 +36,18 @@ "url": "https://github.com/aragon/aragen/issues" }, "homepage": "https://github.com/aragon/aragen#readme", + "dependencies": { + "@babel/polyfill": "^7.0.0", + "chalk": "^2.1.0", + "figures": "^2.0.0", + "ganache-core": "^2.2.1", + "listr": "^0.13.0", + "ncp": "^2.0.0", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.2", + "web3": "^1.0.0-beta.34", + "yargs": "^12.0.2" + }, "devDependencies": { "@aragon/cli": "^4.0.0-rc.14", "ganache-cli": "^6.2.1", diff --git a/readme.md b/readme.md index 839a874..f9b1653 100644 --- a/readme.md +++ b/readme.md @@ -36,7 +36,7 @@ To use directly with ganache-cli: ``` npm install @aragon/aragen -ganache-cli --db node_modules/@aragon/aragen/aragon-ganache -m "explain tackle mirror kit van hammer degree position ginger unfair soup bonus" -i 15 -l 100000000 +npx aragen devchain ``` If you wish to access from code, for example to run ganache-core directly: diff --git a/scripts/start-ganache b/scripts/start-ganache index 6f99940..8c98d43 100755 --- a/scripts/start-ganache +++ b/scripts/start-ganache @@ -1,6 +1,10 @@ #!/bin/bash +cd $(dirname $0)/.. +BASEPATH=$(pwd) +mnemonic=$(node -p "require(require('path').join(\"${BASEPATH}\", 'src/helpers/ganache-vars')).MNEMONIC") + rm -rf aragon-ganache mkdir aragon-ganache set -e; -npx ganache-cli -m "explain tackle mirror kit van hammer degree position ginger unfair soup bonus" -i 15 -l 100000000 --db aragon-ganache +npx ganache-cli -m "${mnemonic}" -i 15 -l 100000000 --db aragon-ganache diff --git a/src/cli.js b/src/cli.js new file mode 100755 index 0000000..849a6c9 --- /dev/null +++ b/src/cli.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +require('@babel/polyfill') +const ConsoleReporter = require('./reporters/ConsoleReporter') + +// Set up commands +const cmd = require('yargs').commandDir('./commands') + +cmd.alias('h', 'help') +cmd.alias('v', 'version') + +// Configure CLI behavior +cmd.demandCommand(1, 'You need to specify a command') + +// Set global options +cmd.option('silent', { + description: 'Silence output to terminal', + default: false, +}) + +cmd.option('debug', { + description: 'Show more output to terminal', + default: false, + coerce: debug => { + if (debug || process.env.DEBUG) { + global.DEBUG_MODE = true + return true + } + }, +}) + +// Run +const reporter = new ConsoleReporter() +// reporter.debug(JSON.stringify(process.argv)) +cmd + .fail((msg, err, yargs) => { + if (!err) yargs.showHelp() + reporter.error(msg || err.message || 'An error occurred') + reporter.debug(err && err.stack) + }) + .parse(process.argv.slice(2), { + reporter, + }) diff --git a/src/commands/devchain.js b/src/commands/devchain.js new file mode 100644 index 0000000..7819316 --- /dev/null +++ b/src/commands/devchain.js @@ -0,0 +1,180 @@ +const TaskList = require('listr') +const ncp = require('ncp') +const ganache = require('ganache-core') +const Web3 = require('web3') +const { promisify } = require('util') +const os = require('os') +const path = require('path') +const rimraf = require('rimraf') +const mkdirp = require('mkdirp') +const chalk = require('chalk') +const fs = require('fs') +const listrOpts = require('../helpers/listr-options') +const pjson = require('../../package.json') + +const { BLOCK_GAS_LIMIT, MNEMONIC } = require('../helpers/ganache-vars') + +exports.command = 'devchain' +exports.describe = + 'Open a test chain for development and pass arguments to ganache' +exports.builder = { + port: { + description: 'The port to run the local chain on', + default: 8545, + }, + reset: { + type: 'boolean', + default: false, + description: 'Reset devchain to snapshot', + }, + accounts: { + default: 2, + description: 'Number of accounts to print', + }, + verbose: { + default: false, + type: 'boolean', + description: 'Enable verbose devchain output', + }, +} + +exports.task = async function({ + port = 8545, + verbose = false, + reset = false, + showAccounts = 2, + reporter, + silent, + debug, +}) { + const removeDir = promisify(rimraf) + const mkDir = promisify(mkdirp) + const recursiveCopy = promisify(ncp) + + const snapshotPath = path.join( + os.homedir(), + `.aragon/ganache-db-${pjson.version}` + ) + + const tasks = new TaskList( + [ + { + title: 'Setting up a new chain from latest Aragon snapshot', + task: async (ctx, task) => { + await removeDir(snapshotPath) + await mkDir(path.resolve(snapshotPath, '..')) + const snapshot = path.join(__dirname, '../../aragon-ganache') + await recursiveCopy(snapshot, snapshotPath) + }, + enabled: () => !fs.existsSync(snapshotPath) || reset, + }, + { + title: 'Starting a local chain from snapshot', + task: async (ctx, task) => { + const server = ganache.server({ + // Start on a different networkID every time to avoid Metamask nonce caching issue: + // https://github.com/aragon/aragon-cli/issues/156 + network_id: parseInt(1e8 * Math.random()), + gasLimit: BLOCK_GAS_LIMIT, + mnemonic: MNEMONIC, + db_path: snapshotPath, + logger: verbose ? { log: reporter.info.bind(reporter) } : undefined, + }) + const listen = () => + new Promise((resolve, reject) => { + server.listen(port, err => { + if (err) return reject(err) + + task.title = `Local chain started at port ${port}` + resolve() + }) + }) + await listen() + + ctx.web3 = new Web3( + new Web3.providers.WebsocketProvider(`ws://localhost:${port}`) + ) + const accounts = await ctx.web3.eth.getAccounts() + + ctx.accounts = accounts.slice(0, parseInt(showAccounts)) + ctx.mnemonic = MNEMONIC + + const ganacheAccounts = server.provider.manager.state.accounts + ctx.privateKeys = ctx.accounts.map(address => ({ + key: ganacheAccounts[address.toLowerCase()].secretKey.toString( + 'hex' + ), + address, + })) + }, + }, + ], + listrOpts(silent, debug) + ) + + return tasks +} + +exports.printAccounts = (reporter, privateKeys) => { + const firstAccountComment = + '(this account is used to deploy DAOs, it has more permissions)' + + const formattedAccounts = privateKeys.map( + ({ address, key }, i) => + chalk.bold( + `Address #${i + 1}: ${address} ${ + i === 0 ? firstAccountComment : '' + }\nPrivate key: ` + ) + key + ) + + reporter.info(`Here are some Ethereum accounts you can use. + The first one will be used for all the actions the CLI performs. + You can use your favorite Ethereum provider or wallet to import their private keys. + \n${formattedAccounts.join('\n')}`) +} + +exports.printMnemonic = (reporter, mnemonic) => { + reporter.info( + `The accounts were generated from the following mnemonic phrase:\n${mnemonic}\n` + ) +} + +exports.printResetNotice = (reporter, reset) => { + if (reset) { + reporter.warning(`The devchain was reset, some steps need to be done to prevent issues: + - Reset the application cache in Aragon Core by going to Settings > Troubleshooting. + - If using Metamask: switch to a different network, and then switch back to the 'Private Network' (this will clear the nonce cache and prevent errors when sending transactions) + `) + } +} + +exports.handler = async ({ + reporter, + port, + reset, + verbose, + accounts, + silent, + debug, +}) => { + const task = await exports.task({ + port, + reset, + verbose, + reporter, + showAccounts: accounts, + silent, + debug, + }) + const { privateKeys, mnemonic } = await task.run() + exports.printAccounts(reporter, privateKeys) + exports.printMnemonic(reporter, mnemonic) + exports.printResetNotice(reporter, reset) + + reporter.info( + `ENS instance deployed at 0x5f6f7e8cc7346a11ca2def8f827b7a0b612c56a1\n` + ) + + reporter.info(`Devchain running: ${chalk.bold('http://localhost:' + port)}.`) +} diff --git a/src/helpers/ganache-vars.js b/src/helpers/ganache-vars.js new file mode 100644 index 0000000..525d0ee --- /dev/null +++ b/src/helpers/ganache-vars.js @@ -0,0 +1,4 @@ +module.exports = { + BLOCK_GAS_LIMIT: 50e6, + MNEMONIC: 'explain tackle mirror kit van hammer degree position ginger unfair soup bonus' +} diff --git a/src/helpers/listr-options.js b/src/helpers/listr-options.js new file mode 100644 index 0000000..f5311c8 --- /dev/null +++ b/src/helpers/listr-options.js @@ -0,0 +1,19 @@ +const ListrRenderer = require('../reporters/ListrRenderer') + +/** + * https://github.com/SamVerschueren/listr#options + * https://github.com/SamVerschueren/listr-update-renderer#options + * https://github.com/SamVerschueren/listr-verbose-renderer#options + * + * @param {boolean} silent Option silent + * @param {boolean} debug Option debug + * @returns {Object} listr options object + */ +function listrOpts(silent, debug) { + return { + renderer: ListrRenderer(silent, debug), + dateFormat: false, + } +} + +module.exports = listrOpts diff --git a/src/reporters/ConsoleReporter.js b/src/reporters/ConsoleReporter.js new file mode 100644 index 0000000..9b85c45 --- /dev/null +++ b/src/reporters/ConsoleReporter.js @@ -0,0 +1,49 @@ +const chalk = require('chalk') +const figures = require('figures') + +module.exports = class ConsoleReporter { + constructor(opts = { silent: false }) { + this.silent = opts.silent + } + + message(category = 'info', message) { + if (this.silent) return + + const color = { + debug: 'magenta', + info: 'blue', + warning: 'yellow', + error: 'red', + success: 'green', + }[category] + const symbol = { + debug: figures.pointer, + info: figures.info, + warning: figures.warning, + error: figures.cross, + success: figures.tick, + }[category] + const icon = chalk[color](symbol) + console.log(` ${icon} ${message}`) + } + + debug(message) { + if (global.DEBUG_MODE) this.message('debug', message) + } + + info(message) { + this.message('info', message) + } + + warning(message) { + this.message('warning', message) + } + + error(message) { + this.message('error', message) + } + + success(message) { + this.message('success', message) + } +} diff --git a/src/reporters/ListrRenderer.js b/src/reporters/ListrRenderer.js new file mode 100644 index 0000000..2bef647 --- /dev/null +++ b/src/reporters/ListrRenderer.js @@ -0,0 +1,9 @@ +const VerboseRenderer = require('listr-verbose-renderer') +const SilentRenderer = require('listr-silent-renderer') +const UpdateRenderer = require('listr-update-renderer') + +module.exports = function(silent, debug) { + if (debug) return VerboseRenderer + if (silent) return SilentRenderer + return UpdateRenderer +}