├── data └── setup.js ├── .npmignore ├── .gitignore ├── .editorconfig ├── test ├── allTests.js ├── utils.js ├── consoleExec.test.js ├── gethOptions.test.js ├── logger.test.js ├── cleanup.test.js ├── gethPath.test.js ├── basic.test.js └── customDataDir.test.js ├── gulpfile.js ├── .travis.yml ├── CONTRIBUTING.md ├── package.json ├── bin └── geth-private ├── README.md └── index.js /data/setup.js: -------------------------------------------------------------------------------- 1 | personal.newAccount("1234"); 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | gulpfile.js 3 | .travis.yml 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | test/data 5 | test/bin 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | -------------------------------------------------------------------------------- /test/allTests.js: -------------------------------------------------------------------------------- 1 | var test = module.exports = {}; 2 | 3 | [ 4 | 'basic', 5 | 'logger', 6 | 'cleanup', 7 | 'gethOptions', 8 | 'gethPath', 9 | 'customDataDir', 10 | 'consoleExec', 11 | ].forEach(function(name) { 12 | test[name] = require(`./${name}.test`); 13 | }); 14 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | path = require('path'); 3 | 4 | var mocha = require('gulp-mocha'); 5 | 6 | 7 | gulp.task('test', function () { 8 | return gulp.src(['./test/allTests.js'], { read: false }) 9 | .pipe(mocha({ 10 | timeout: 60000, 11 | ui: 'exports', 12 | reporter: 'spec' 13 | })) 14 | ; 15 | }); 16 | 17 | 18 | gulp.task('default', ['test']); 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | cache: 5 | directories: 6 | - ~/.ethash 7 | 8 | language: node_js 9 | 10 | node_js: 11 | - "8" 12 | 13 | before_install: 14 | - sudo add-apt-repository -y ppa:ethereum/ethereum 15 | - sudo apt-get update 16 | - sudo apt-get install -y geth 17 | - mkdir -p ~/.ethereum 18 | - mkdir -p ~/.ethash 19 | - geth makedag 0 ~/.ethash 20 | 21 | script: 22 | - "npm test" 23 | 24 | notifications: 25 | email: 26 | - ram@hiddentao.com 27 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var shell = require('shelljs'); 4 | 5 | 6 | const GETH = require('which').sync('geth'); 7 | 8 | 9 | 10 | exports.canAttach = function(dataDir) { 11 | let ret = shell.exec(`${GETH} --exec 'eth.coinbase' attach ipc://${dataDir}/geth.ipc`, { 12 | silent: true, 13 | async: false, 14 | }); 15 | 16 | return ret.code === 0; 17 | }; 18 | 19 | 20 | exports.gethExecJs = function(dataDir, jsToExecute) { 21 | let ret = shell.exec(`${GETH} --exec '${jsToExecute}' attach ipc://${dataDir}/geth.ipc`, { 22 | silent: true, 23 | async: false, 24 | }); 25 | 26 | if (ret.code !== 0) { 27 | throw new Error('Exec error: ' + ret.stderr); 28 | } 29 | 30 | return ret.stdout; 31 | }; 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to geth-private 2 | 3 | This guide guidelines for those wishing to contribute. 4 | 5 | ## Contributor license agreement 6 | 7 | By submitting code as an individual or as an entity you agree that your code is [licensed the same as geth-private](README.md). 8 | 9 | ## Issues and pull requests 10 | 11 | Issues and merge requests should be in English and contain appropriate language for audiences of all ages. 12 | 13 | We will only accept a merge requests which meets the following criteria: 14 | 15 | * Includes proper tests and all tests pass (unless it contains a test exposing a bug in existing code) 16 | * Can be merged without problems (if not please use: `git rebase master`) 17 | * Does not break any existing functionality 18 | * Fixes one specific issue or implements one specific feature (do not combine things, send separate merge requests if needed) 19 | * Keeps the code base clean and well structured 20 | * Contains functionality we think other users will benefit from too 21 | * Doesn't add unnessecary configuration options since they complicate future changes 22 | 23 | -------------------------------------------------------------------------------- /test/consoleExec.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var Q = require('bluebird'), 5 | chai = require('chai'), 6 | tmp = require('tmp'), 7 | path = require('path'), 8 | shell = require('shelljs'), 9 | Web3 = require('web3'); 10 | 11 | var expect = chai.expect, 12 | should = chai.should(); 13 | 14 | chai.use(require("chai-as-promised")); 15 | 16 | 17 | var testUtils = require('./utils'); 18 | 19 | var source = require('../'); 20 | 21 | 22 | 23 | module.exports = { 24 | before: function(done) { 25 | this.inst = source({ 26 | 27 | }); 28 | 29 | this.inst.start() 30 | .asCallback(done); 31 | }, 32 | after: function(done) { 33 | Q.resolve().then(() => { 34 | if (this.inst.isRunning) { 35 | return this.inst.stop(); 36 | } 37 | }) 38 | .asCallback(done); 39 | }, 40 | 'execute bad console command': function() { 41 | console.warn('Test will not work until https://github.com/ethereum/go-ethereum/issues/2470 is resolved '); 42 | }, 43 | 'execute good console command': function(done) { 44 | this.inst.consoleExec('web3.toDecimal(\'0x15\')') 45 | .then((val) => { 46 | val.should.eql("21"); 47 | }) 48 | .asCallback(done); 49 | }, 50 | }; 51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geth-private", 3 | "version": "3.0.0", 4 | "description": "Quickly setup a local, private Ethereum blockchain.", 5 | "main": "index.js", 6 | "bin": "bin/geth-private", 7 | "scripts": { 8 | "test": "gulp test" 9 | }, 10 | "author": { 11 | "name": "Ramesh Nair", 12 | "email": "ram@hiddentao.com" 13 | }, 14 | "license": "MIT", 15 | "dependencies": { 16 | "bluebird": "^3.5.0", 17 | "chalk": "^1.1.3", 18 | "shelljs": "^0.6.1", 19 | "tmp": "0.0.28", 20 | "which": "^1.3.0", 21 | "yargs": "^4.8.1" 22 | }, 23 | "readmeFilename": "README.md", 24 | "bugs": { 25 | "url": "https://github.com/hiddentao/geth-private/issues" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/hiddentao/geth-private.git" 30 | }, 31 | "keywords": [ 32 | "ethereum", 33 | "bitcoin", 34 | "blockchain", 35 | "crypto", 36 | "currency", 37 | "cryptocurrency", 38 | "private", 39 | "local", 40 | "testing" 41 | ], 42 | "devDependencies": { 43 | "chai": "^3.5.0", 44 | "chai-as-promised": "^5.3.0", 45 | "gulp": "^3.9.1", 46 | "gulp-mocha": "^2.2.0", 47 | "lodash": "^4.17.11", 48 | "sinon": "^1.17.7", 49 | "web3": "^0.17.0-beta" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/gethOptions.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var Q = require('bluebird'), 5 | chai = require('chai'), 6 | tmp = require('tmp'), 7 | path = require('path'), 8 | shell = require('shelljs'), 9 | Web3 = require('web3'); 10 | 11 | var expect = chai.expect, 12 | should = chai.should(); 13 | 14 | 15 | var testUtils = require('./utils'); 16 | 17 | var source = require('../'); 18 | 19 | 20 | 21 | 22 | module.exports = { 23 | afterEach: function(done) { 24 | Q.resolve() 25 | .then(() => { 26 | if (this.inst && this.inst.isRunning) { 27 | return this.inst.stop(); 28 | } 29 | }) 30 | .asCallback(done); 31 | }, 32 | 'override': function(done) { 33 | this.inst = source({ 34 | gethOptions: { 35 | rpc: false, 36 | identity: 'testnode123', 37 | port: 44323, 38 | rpcport: 58545, 39 | }, 40 | }); 41 | 42 | this.inst.start() 43 | .then(() => { 44 | let out = testUtils.gethExecJs(this.inst.dataDir, `admin.nodeInfo`); 45 | out.should.contain('testnode123'); 46 | }) 47 | .then(() => { 48 | var web3 = new Web3(); 49 | web3.setProvider(new web3.providers.HttpProvider('http://localhost:58545')); 50 | 51 | web3.eth.coinbase.should.eql(this.inst.account); 52 | }) 53 | .asCallback(done); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /bin/geth-private: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | 6 | const yargs = require('yargs'), 7 | chalk = require('chalk'); 8 | 9 | const geth = require('..'); 10 | 11 | const packageJson = require('../package.json'); 12 | 13 | 14 | // CLI options 15 | const argv = yargs 16 | .usage('Usage: $0 [options]') 17 | .describe('gethPath', 'Path to geth executable to use instead of default') 18 | .help('h') 19 | .alias('h', 'help') 20 | .boolean('v', 'Verbose logging') 21 | .describe('version', 'Output version.') 22 | .epilogue('All other options get passed onto the geth executable.') 23 | .parse(process.argv.slice(1)); 24 | 25 | if (argv.version) { 26 | return console.log(`geth-private ${packageJson.version}`); 27 | } 28 | 29 | var gethOptions = {}; 30 | 31 | var nonGethOptionKeys = [ 32 | '_', '$0', 'v', 'h', 'help', 'version', 'gethPath' 33 | ] 34 | 35 | for (let key in argv) { 36 | if (0 > nonGethOptionKeys.indexOf(key)) { 37 | gethOptions[key] = argv[key]; 38 | } 39 | } 40 | 41 | 42 | var inst = geth({ 43 | verbose: !!argv.v, 44 | gethPath: argv.gethPath || null, 45 | gethOptions: gethOptions 46 | }) 47 | 48 | inst.start() 49 | .then(function() { 50 | console.log(chalk.yellow(`Geth is now running (pid: ${inst.pid}).\n`)); 51 | console.log(chalk.yellow(`Data folder:\t${inst.dataDir}`)); 52 | console.log(chalk.yellow(`\Account:\t${inst.account}`)); 53 | console.log(chalk.yellow(`\nIPC:\tgeth attach ipc://${inst.dataDir}/geth.ipc`)); 54 | }) 55 | .catch(function(err) { 56 | console.error(err); 57 | }); 58 | -------------------------------------------------------------------------------- /test/logger.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var Q = require('bluebird'), 5 | chai = require('chai'), 6 | sinon = require('sinon'), 7 | tmp = require('tmp'), 8 | path = require('path'), 9 | shell = require('shelljs'), 10 | Web3 = require('web3'); 11 | 12 | var expect = chai.expect, 13 | should = chai.should(); 14 | 15 | 16 | var testUtils = require('./utils'); 17 | 18 | var source = require('../'); 19 | 20 | 21 | 22 | module.exports = { 23 | beforeEach: function() { 24 | this.mocker = sinon.sandbox.create(); 25 | }, 26 | afterEach: function(done) { 27 | this.mocker.restore(); 28 | if (this.inst && this.inst.isRunning) { 29 | this.inst.stop().asCallback(done); 30 | } else { 31 | done(); 32 | } 33 | }, 34 | default: function(done) { 35 | const infoSpy = this.mocker.stub(console, 'info'); 36 | 37 | this.inst = source({ 38 | verbose: true, 39 | }); 40 | 41 | this.inst.start() 42 | .then(() => { 43 | infoSpy.should.have.been.called; 44 | }) 45 | .asCallback(done); 46 | }, 47 | custom: function(done) { 48 | const logger = { 49 | debug: this.mocker.spy(), 50 | info: this.mocker.spy(), 51 | error: this.mocker.spy(), 52 | }; 53 | 54 | this.inst = source({ 55 | verbose: true, 56 | logger: logger, 57 | }); 58 | 59 | this.inst.start() 60 | .then(() => { 61 | logger.debug.should.have.been.called; 62 | logger.info.should.have.been.called; 63 | }) 64 | .asCallback(done); 65 | }, 66 | }; 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/cleanup.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var Q = require('bluebird'), 5 | chai = require('chai'), 6 | tmp = require('tmp'), 7 | path = require('path'), 8 | shell = require('shelljs'), 9 | Web3 = require('web3'); 10 | 11 | var expect = chai.expect, 12 | should = chai.should(); 13 | 14 | 15 | var testUtils = require('./utils'); 16 | 17 | var source = require('../'); 18 | 19 | 20 | 21 | module.exports = { 22 | beforeEach: function(done) { 23 | this.inst = source({ 24 | 25 | }); 26 | 27 | this.inst.start() 28 | .then(() => { 29 | testUtils.canAttach(this.inst.dataDir).should.be.true; 30 | }) 31 | .asCallback(done); 32 | }, 33 | afterEach: function(done) { 34 | Q.resolve() 35 | .then(() => { 36 | if (this.inst && this.inst.isRunning) { 37 | return this.inst.stop(); 38 | } 39 | }) 40 | .asCallback(done); 41 | }, 42 | 'can stop geth': function(done) { 43 | this.inst.stop() 44 | .then((ret) => { 45 | expect(ret.signal === 'SIGTERM' || ret.signal === null).to.be.true; 46 | 47 | testUtils.canAttach(this.inst.dataDir).should.be.false; 48 | }) 49 | .asCallback(done); 50 | }, 51 | 'can stop using kill': function(done) { 52 | this.inst.stop({ 53 | kill: true, 54 | }) 55 | .then((ret) => { 56 | expect(ret.signal).to.eql('SIGKILL'); 57 | 58 | testUtils.canAttach(this.inst.dataDir).should.be.false; 59 | }) 60 | .asCallback(done); 61 | }, 62 | 'auto-deletes data folder': function(done) { 63 | this.inst.stop() 64 | .then(() => { 65 | shell.test('-e', path.join(this.inst.dataDir, 'genesis.json')).should.be.false; 66 | }) 67 | .asCallback(done); 68 | }, 69 | }; 70 | 71 | -------------------------------------------------------------------------------- /test/gethPath.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var Q = require('bluebird'), 5 | chai = require('chai'), 6 | tmp = require('tmp'), 7 | path = require('path'), 8 | shell = require('shelljs'), 9 | which = require('which'), 10 | Web3 = require('web3'); 11 | 12 | var expect = chai.expect, 13 | should = chai.should(); 14 | 15 | 16 | var testUtils = require('./utils'); 17 | 18 | var source = require('../'); 19 | 20 | 21 | 22 | 23 | module.exports = { 24 | 'bad path': function(done) { 25 | this.inst = source({ 26 | gethPath: '/usr/bin/doesnotexist', 27 | }); 28 | 29 | this.inst.start() 30 | .then(() => { 31 | throw new Error('Should not be here'); 32 | }) 33 | .catch((err) => { 34 | err += ''; 35 | 36 | err.should.contain('Startup error'); 37 | }) 38 | .asCallback(done); 39 | }, 40 | 41 | 'good path': { 42 | beforeEach: function() { 43 | var origGethPath = which.sync('geth'); 44 | this.binDir = path.join(__dirname, 'bin'); 45 | 46 | shell.rm('-rf', this.binDir); 47 | shell.mkdir('-p', this.binDir); 48 | 49 | this.gethPath = path.join(this.binDir, 'geth'); 50 | 51 | shell.cp(origGethPath, this.gethPath); 52 | }, 53 | 54 | afterEach: function(done) { 55 | shell.rm('-rf', this.binDir); 56 | 57 | if (this.inst) { 58 | this.inst.stop().asCallback(done); 59 | } else { 60 | done(); 61 | } 62 | }, 63 | 64 | default: function(done) { 65 | this.inst = source({ 66 | // verbose: true, 67 | gethPath: this.gethPath, 68 | }); 69 | 70 | this.inst.start().asCallback(done); 71 | }, 72 | 73 | 'path with spaces': function(done) { 74 | var newDir = path.join(this.binDir, 'child dir'); 75 | 76 | shell.mkdir('-p', newDir); 77 | 78 | var newGethPath = path.join(newDir, 'geth'); 79 | 80 | shell.cp(this.gethPath, newGethPath); 81 | 82 | this.inst = source({ 83 | // verbose: true, 84 | gethPath: newGethPath, 85 | }); 86 | 87 | this.inst.start().asCallback(done); 88 | }, 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var Q = require('bluebird'), 5 | chai = require('chai'), 6 | tmp = require('tmp'), 7 | path = require('path'), 8 | shell = require('shelljs'), 9 | Web3 = require('web3'); 10 | 11 | var expect = chai.expect, 12 | should = chai.should(); 13 | 14 | 15 | var testUtils = require('./utils'); 16 | 17 | var source = require('../'); 18 | 19 | 20 | 21 | module.exports = { 22 | before: function() { 23 | this.inst = source(); 24 | }, 25 | 'not started': function() { 26 | this.inst.isRunning.should.be.false; 27 | expect(this.inst.account).to.be.undefined; 28 | }, 29 | 'not stoppable': function(done) { 30 | this.inst.stop() 31 | .catch((err) => { 32 | err.toString().should.contain('Not started'); 33 | }) 34 | .asCallback(done); 35 | }, 36 | 'startup error': { 37 | beforeEach: function(done) { 38 | this.origInst = source(); 39 | 40 | this.origInst.start().asCallback(done); 41 | }, 42 | afterEach: function(done) { 43 | this.origInst.stop().asCallback(done); 44 | }, 45 | default: function(done) { 46 | this.inst = source({ 47 | // verbose: true 48 | }); 49 | 50 | this.inst.start() 51 | .then(() => { 52 | throw new Error('unexpected'); 53 | }) 54 | .catch((err) => { 55 | err = '' + err; 56 | err.should.contain('address already in use'); 57 | }) 58 | .asCallback(done); 59 | }, 60 | }, 61 | 'once started': { 62 | before: function(done) { 63 | this.inst.start() 64 | .asCallback(done); 65 | }, 66 | after: function(done) { 67 | Q.try(() => { 68 | if (this.inst.isRunning) { 69 | return this.inst.stop(); 70 | } 71 | }) 72 | .asCallback(done); 73 | }, 74 | 'is running': function() { 75 | this.inst.isRunning.should.be.true; 76 | expect(this.inst.pid > 0).to.be.true; 77 | }, 78 | 'httpRpcEndpoint': function() { 79 | (this.inst.httpRpcEndpoint || '').should.eql(`http://localhost:58545`); 80 | }, 81 | 'data dir': function() { 82 | expect((this.inst.dataDir || '').length > 0).to.be.true; 83 | }, 84 | 'attach console': { 85 | 'check coinbase': function() { 86 | let out = testUtils.gethExecJs(this.inst.dataDir, `eth.coinbase`); 87 | 88 | JSON.parse(out).should.be.eql(this.inst.account); 89 | }, 90 | }, 91 | 'rpc': { 92 | before: function() { 93 | this.web3 = new Web3(); 94 | this.web3.setProvider(new this.web3.providers.HttpProvider(`http://localhost:58545`)); 95 | }, 96 | 'get coinbase': function() { 97 | this.web3.eth.coinbase.should.eql(this.inst.account); 98 | }, 99 | } 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /test/customDataDir.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | var Q = require('bluebird'), 5 | chai = require('chai'), 6 | tmp = require('tmp'), 7 | path = require('path'), 8 | shell = require('shelljs'), 9 | Web3 = require('web3'); 10 | 11 | var expect = chai.expect, 12 | should = chai.should(); 13 | 14 | 15 | var testUtils = require('./utils'); 16 | 17 | var source = require('../'); 18 | 19 | 20 | 21 | 22 | 23 | module.exports = { 24 | 'default': { 25 | beforeEach: function() { 26 | this.datadir = tmp.dirSync().name; 27 | // delete it straight away as we will get geth-private to create it for us 28 | shell.rm('-rf', this.datadir); 29 | 30 | this.inst = source({ 31 | gethOptions: { 32 | datadir: this.datadir, 33 | }, 34 | }); 35 | }, 36 | afterEach: function(done) { 37 | Q.resolve() 38 | .then(() => { 39 | if (this.inst && this.inst.isRunning) { 40 | return this.inst.stop(); 41 | } 42 | }) 43 | .then(() => { 44 | shell.rm('-rf', this.datadir); 45 | }) 46 | .asCallback(done); 47 | }, 48 | 'will create it if it doesn\'t exist': function(done) { 49 | this.inst.start() 50 | .then(() => { 51 | shell.test('-e', this.datadir).should.be.true; 52 | }) 53 | .asCallback(done); 54 | }, 55 | 'can re-use it': function(done) { 56 | let account = null; 57 | 58 | this.inst.start() 59 | .then(() => { 60 | account = this.inst.account; 61 | 62 | return this.inst.stop(); 63 | }) 64 | .then(() => { 65 | shell.test('-e', this.datadir).should.be.true; 66 | 67 | return this.inst.start(); 68 | }) 69 | .then(() => { 70 | this.inst.account.should.eql(account); 71 | }) 72 | .asCallback(done); 73 | }, 74 | }, 75 | 'relative paths': { 76 | beforeEach: function() { 77 | let dirName = 'test/data'; 78 | 79 | this.datadir = path.join(process.cwd(), dirName); 80 | // delete it straight away as we will get geth-private to create it for us 81 | shell.rm('-rf', this.datadir); 82 | 83 | this.inst = source({ 84 | gethOptions: { 85 | datadir: dirName, /* relative path only */ 86 | }, 87 | }); 88 | }, 89 | afterEach: function(done) { 90 | Q.resolve() 91 | .then(() => { 92 | if (this.inst && this.inst.isRunning) { 93 | return this.inst.stop(); 94 | } 95 | }) 96 | .then(() => { 97 | shell.rm('-rf', this.datadir); 98 | }) 99 | .asCallback(done); 100 | }, 101 | 'resolves relative paths': function(done) { 102 | this.inst.start() 103 | .then(() => { 104 | shell.test('-e', this.datadir).should.be.true; 105 | 106 | this.inst.dataDir.should.eql(this.datadir); 107 | }) 108 | .asCallback(done); 109 | }, 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geth-private 2 | 3 | [![Build Status](https://secure.travis-ci.org/hiddentao/geth-private.png?branch=master)](http://travis-ci.org/hiddentao/geth-private) [![NPM module](https://badge.fury.io/js/geth-private.png)](https://badge.fury.io/js/geth-private) [![Follow on Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/hiddentao) 4 | 5 | Quickly setup a local, private Ethereum blockchain. 6 | 7 | Features: 8 | 9 | * Programmatic as well as command-line interface 10 | * Automatically enables IPC and RPC/CORS access 11 | * Override all options passed to the `geth` executable. 12 | * Execute console commands against the running geth instance. 13 | * Logging capture 14 | * Works with [Mist wallet](https://github.com/ethereum/mist) 15 | 16 | ## Requirements: 17 | 18 | * Node.js v4 or above 19 | * [Geth 1.8+](https://github.com/ethereum/go-ethereum) 20 | 21 | ## Installation 22 | 23 | I recommend installing geth-private as a global module so that the CLI becomes 24 | available in your PATH: 25 | 26 | ```bash 27 | $ npm install -g geth-private 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### via command-line 33 | 34 | **Quickstart** 35 | 36 | ```bash 37 | $ geth-private 38 | ``` 39 | 40 | You should see something like: 41 | 42 | ```bash 43 | geth is now running (pid: 2428). 44 | 45 | Etherbase: 8864324ac84c3b6c507591dfabeffdc1ad02e09b 46 | Data folder: /var/folders/br6x6mlx113235/T/tmp-242211yX 47 | 48 | To attach: geth attach ipc:///var/folders/br6x6mlx113235/T/tmp-242211yX/geth.ipc 49 | ``` 50 | 51 | *Note: geth-private runs Geth on port 60303 (and HTTP RPC on port 58545) by default with networkid 33333* 52 | 53 | Run the `attach` command given to attach a console to this running geth 54 | instance. By default [web3](https://github.com/ethereum/web3.js) RPC is also 55 | enabled on port 58545. 56 | 57 | Once it's running launch the Ethereum/Mist wallet with the `--rpc http://localhost:58545` CLI option - it should be able to 58 | connect to your geth instance. 59 | 60 | 61 | **Options** 62 | 63 | ``` 64 | Usage: geth-private [options] 65 | 66 | Options: 67 | --gethPath Path to geth executable to use instead of default 68 | -v Verbose logging 69 | -h, --help Show help [boolean] 70 | --version Output version. 71 | 72 | All other options get passed onto the geth executable. 73 | ``` 74 | 75 | You can also pass options directly to geth. For example, you can customize 76 | network identity, port, etc: 77 | 78 | ```bash 79 | $ geth-private --port 10023 --networkid 54234 --identity testnetwork 80 | ``` 81 | 82 | By default geth-private stores its keystore and blockchain data inside a 83 | temporarily generated folder, which gets automatically deleted once it exits. 84 | You can override this behaviour by providing a custom location using the 85 | `datadir` option: 86 | 87 | ```bash 88 | $ geth-private --datadir /path/to/data/folder 89 | ``` 90 | 91 | When geth-private exits it won't auto-delete this data folder since you 92 | manually specified it. This allows you to re-use once created keys and 93 | accounts easily. 94 | 95 | 96 | ### via API 97 | 98 | 99 | ```js 100 | var geth = require('geth-private'); 101 | 102 | var inst = geth(); 103 | 104 | inst.start() 105 | .then(function() { 106 | // do some work 107 | }); 108 | .then(function() { 109 | // stop it 110 | return inst.stop(); 111 | }); 112 | .catch(function(err) { 113 | console.error(err); 114 | }) 115 | 116 | ``` 117 | 118 | Same as for the CLI, you can customize it by passing options during construction: 119 | 120 | ```js 121 | var geth = require('geth-private'); 122 | 123 | var inst = geth({ 124 | balance: 10, 125 | gethPath: '/path/to/geth', 126 | verbose: true, 127 | gethOptions: { 128 | /* 129 | These options get passed to the geth command-line 130 | 131 | e.g. 132 | 133 | mine: true 134 | rpc: false, 135 | identity: 'testnetwork123' 136 | */ 137 | }, 138 | }); 139 | 140 | inst.start().then(...); 141 | ``` 142 | 143 | You can execute web3 commands against the running geth instance: 144 | 145 | ```js 146 | var inst = geth(); 147 | 148 | inst.start() 149 | .then(() => { 150 | return inst.consoleExec('web3.version.api'); 151 | }) 152 | .then((version) => { 153 | console.log(version); 154 | }) 155 | ... 156 | ``` 157 | 158 | ### Mining 159 | 160 | To start and stop mining: 161 | 162 | ```js 163 | var inst = geth(); 164 | 165 | inst.start() 166 | .then(() => { 167 | return inst.consoleExec('miner.start()'); 168 | }) 169 | ... 170 | .then(() => { 171 | return inst.consoleExec('miner.stop()'); 172 | }) 173 | ... 174 | ``` 175 | 176 | If you've never mined before then Geth will first generate a [DAG](https://github.com/ethereum/wiki/wiki/Ethash-DAG), which 177 | could take a while. Use the `-v` option to Geth's logging. 178 | 179 | 180 | 181 | ## Logging capture 182 | 183 | When using the programmatic API you can capture all output logging by passing 184 | a custom logging object: 185 | 186 | ```js 187 | var inst = geth({ 188 | verbose: true, 189 | logger: { 190 | debug: function() {...}, 191 | info: function() {...}, 192 | error: function() {...} 193 | } 194 | }); 195 | 196 | inst.start(); 197 | ``` 198 | 199 | 200 | ## Development 201 | 202 | To run the tests: 203 | 204 | ```bash 205 | $ npm install 206 | $ npm test 207 | ``` 208 | 209 | ## Contributions 210 | 211 | Contributions are welcome. Please see CONTRIBUTING.md. 212 | 213 | 214 | ## License 215 | 216 | MIT 217 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require('path'), 4 | chalk = require('chalk'), 5 | Q = require('bluebird'), 6 | tmp = require('tmp'), 7 | fs = require('fs'), 8 | child_process = require('child_process'), 9 | shell = require('shelljs'), 10 | which = require('which'); 11 | 12 | 13 | class Geth { 14 | constructor(options) { 15 | options = options || {}; 16 | 17 | // options for geth 18 | this._gethOptions = Object.assign({ 19 | networkid: 33333, 20 | port: 60303, 21 | rpc: true, 22 | rpcport: 58545, 23 | rpccorsdomain: "*", 24 | rpcapi: "admin,db,eth,debug,miner,net,shh,txpool,personal,web3", 25 | maxpeers: 0, 26 | nodiscover: true, 27 | dev: true, 28 | // reduce overhead 29 | minerthreads: 1, 30 | lightkdf: true, 31 | cache: 16, 32 | // logging 33 | // verbosity: 6, 34 | }, options.gethOptions, { 35 | rpc: true, 36 | }); 37 | 38 | // path to geth 39 | this._geth = options.gethPath; 40 | 41 | // genesis options 42 | this._genesisOptions = options.genesisBlock || null; 43 | 44 | if (!this._geth) { 45 | try { 46 | this._geth = which.sync('geth'); 47 | } catch (err) { 48 | throw new Error('Unable to find "geth" executable in PATH'); 49 | } 50 | } 51 | 52 | // logging 53 | this._verbose = !!options.verbose; 54 | this._logger = options.logger || console; 55 | } 56 | 57 | start() { 58 | if (this.isRunning) { 59 | throw new Error('Already running'); 60 | } 61 | 62 | this._log(`Starting...`); 63 | 64 | return this._createDataDir() 65 | .then(() => this._startGeth()) 66 | .then((ret) => { 67 | this._proc = ret.proc; 68 | 69 | return this._loadAccountInfo(); 70 | }); 71 | ; 72 | } 73 | 74 | 75 | stop(options) { 76 | return Q.try(() => { 77 | if (!this._proc) { 78 | throw new Error("Not started"); 79 | } 80 | 81 | options = Object.assign({ 82 | kill: false, 83 | }, options); 84 | 85 | return new Q((resolve) => { 86 | this._proc.on('exit', (code, signal) => { 87 | this._log(`Stopped.`); 88 | 89 | this._proc = null; 90 | 91 | if (this._tmpDataDir) { 92 | this._log(`Destroying data...`); 93 | 94 | shell.rm('-rf', this._gethOptions.datadir); 95 | } 96 | 97 | resolve({ 98 | code: code, 99 | signal: signal, 100 | }); 101 | }); 102 | 103 | this._log(`Stopping...`); 104 | 105 | this._proc.kill(options.kill ? 'SIGKILL' : 'SIGTERM'); 106 | }); 107 | }); 108 | } 109 | 110 | /** 111 | * Execute a command in the JS console of the running geth instance. 112 | * @param {String} jsCommand 113 | * @return {Promise} 114 | */ 115 | consoleExec (jsCommand) { 116 | return Q.try(() => { 117 | if (!this._proc) { 118 | throw new Error("Not started"); 119 | } 120 | 121 | this._log(`Execute in console: ${jsCommand}`); 122 | 123 | return this._exec( 124 | this._buildGethCommandLine({ 125 | command: ['--exec', `"${jsCommand}"`, 'attach', 126 | this._formatPathForCli(`ipc://${this.dataDir}/geth.ipc`) 127 | ] 128 | }) 129 | ).then((ret) => { 130 | return ret.stdout; 131 | }); 132 | }); 133 | } 134 | 135 | get httpRpcEndpoint () { 136 | return `http://localhost:${this._gethOptions.rpcport}`; 137 | } 138 | 139 | get dataDir () { 140 | return this._gethOptions.datadir; 141 | } 142 | 143 | get isRunning () { 144 | return !!this._proc; 145 | } 146 | 147 | get account () { 148 | return this._account; 149 | } 150 | 151 | get pid () { 152 | return this._proc.pid; 153 | } 154 | 155 | _loadAccountInfo () { 156 | return this.consoleExec('eth.coinbase').then(account => { 157 | this._account = JSON.parse(account); 158 | }); 159 | } 160 | 161 | _createDataDir () { 162 | return Q.try(() => { 163 | let options = this._gethOptions; 164 | 165 | // need to create temporary data dir? 166 | if (!options.datadir) { 167 | options.datadir = this._tmpDataDir = tmp.dirSync().name; 168 | 169 | this._log(`Created temporary data dir: ${options.datadir}`); 170 | } 171 | // else let's check the given one 172 | else { 173 | // resolve path (against current app folder) 174 | options.datadir = path.resolve(process.cwd(), options.datadir); 175 | 176 | // if not found then try to create it 177 | if (!shell.test('-e', options.datadir)) { 178 | this._log(`Creating data dir: ${options.datadir}`); 179 | 180 | shell.mkdir('-p', options.datadir); 181 | } 182 | } 183 | }); 184 | } 185 | 186 | 187 | _buildGethCommandLine(opts) { 188 | opts = Object.assign({ 189 | command: [], 190 | quoteStrings: true 191 | }, opts); 192 | 193 | let gethOptions = this._gethOptions; 194 | 195 | let str = []; 196 | for (let key in gethOptions) { 197 | let val = gethOptions[key]; 198 | 199 | if (null !== val && false !== val) { 200 | str.push(`--${key}`); 201 | 202 | if (typeof val === "string") { 203 | if ('datadir' === key) { 204 | val = this._formatPathForCli(val); 205 | } else { 206 | val = opts.quoteStrings ? `"${val}"` : val; 207 | } 208 | str.push(val); 209 | } else if (typeof val !== "boolean") { 210 | str.push(`${val}`); 211 | } 212 | } 213 | } 214 | 215 | return [this._geth].concat(str, opts.command); 216 | } 217 | 218 | 219 | /** 220 | * @return {Promise} 221 | */ 222 | _startGeth() { 223 | const gethcli = this._buildGethCommandLine({ 224 | quoteStrings: false, 225 | }); 226 | 227 | return this._exec(gethcli, { 228 | longRunning: true 229 | }) 230 | } 231 | 232 | 233 | /** 234 | * @return {Promise} 235 | */ 236 | _exec (cli, options) { 237 | options = Object.assign({ 238 | longRunning: false, 239 | }, options); 240 | 241 | // execute a command 242 | if (!options.longRunning) { 243 | return new Q((resolve, reject) => { 244 | cli[0] = this._formatPathForCli(cli[0]); 245 | 246 | const cmdStr = cli.join(' '); 247 | 248 | this._log(`Executing geth command: ${cmdStr}`); 249 | 250 | child_process.exec(cmdStr, (err, stdout, stderr) => { 251 | if (err) { 252 | err = new Error(`Execution failed: ${err}`); 253 | 254 | this._logError(err); 255 | 256 | reject(err); 257 | } else { 258 | resolve({ 259 | stdout: stdout.trim(), 260 | stderr: stderr.trim(), 261 | }); 262 | } 263 | }); 264 | }); 265 | } 266 | // start a node instance 267 | else { 268 | return new Q((resolve, reject) => { 269 | this._log(`Starting geth process: ${cli.join(' ')}`); 270 | 271 | let isRunning = false, 272 | successTimer = null; 273 | 274 | const proc = child_process.spawn(cli[0], cli.slice(1),{ 275 | detached: false, 276 | shell: false, 277 | stdio:['ignore', 'pipe', 'pipe'], 278 | }); 279 | 280 | const ret = { 281 | stdout: '', 282 | stderr: '', 283 | }; 284 | 285 | const _handleError = (err) => { 286 | if (isRunning) { 287 | return; 288 | } 289 | 290 | clearTimeout(successTimer); 291 | 292 | err = new Error(`Startup error: ${err}`); 293 | 294 | this._logError(err); 295 | 296 | Object.assign(err, ret); 297 | 298 | return reject(err); 299 | }; 300 | 301 | const _handleOutput = (stream) => (buf) => { 302 | const str = buf.toString(); 303 | 304 | ret[stream] += str; 305 | 306 | this._logNode(str); 307 | 308 | if (str.match(/fatal/igm)) { 309 | _handleError(str); 310 | } 311 | }; 312 | 313 | proc.on('error', _handleError); 314 | proc.stdout.on('data', _handleOutput('stdout')); 315 | proc.stderr.on('data', _handleOutput('stderr')); 316 | 317 | // after 3 seconds assume startup is successful 318 | successTimer = setTimeout(() => { 319 | this._log('Node successfully started'); 320 | 321 | isRunning = true; 322 | 323 | ret.proc = proc; 324 | 325 | resolve(ret); 326 | }, 3000); 327 | }); 328 | } 329 | } 330 | 331 | 332 | _log () { 333 | if (this._verbose) { 334 | let args = Array.prototype.map.call(arguments, (a) => { 335 | return chalk.cyan(a); 336 | }); 337 | 338 | this._logger.info.apply(this._logger, args); 339 | } 340 | } 341 | 342 | 343 | _logNode (str) { 344 | if (this._verbose) { 345 | this._logger.info(str.trim()); 346 | } 347 | } 348 | 349 | 350 | _logError () { 351 | if (this._verbose) { 352 | let args = Array.prototype.map.call(arguments, (a) => { 353 | return chalk.red(a + ''); 354 | }); 355 | 356 | this._logger.error.apply(this._logger, arguments); 357 | } 358 | } 359 | 360 | 361 | _formatPathForCli (pathStr) { 362 | return (0 <= pathStr.indexOf(' ')) ? `"${pathStr}"` : pathStr; 363 | } 364 | } 365 | 366 | 367 | module.exports = function(options) { 368 | return new Geth(options); 369 | }; 370 | --------------------------------------------------------------------------------