├── .dockerignore ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── benchmarks ├── data │ ├── block-367238.json │ ├── block-367239.json │ └── block-367240.json └── index.js ├── bin └── bchnode ├── docker-compose.yml ├── docs ├── bus.md ├── development.md ├── index.md ├── node.md ├── release.md ├── scaffold.md ├── services.md └── services │ ├── bitcoind.md │ └── web.md ├── extra └── compose │ ├── Dockerfile │ ├── default.bch-node.conf │ ├── default.bch-node.json │ └── default.bitcoin.conf ├── index.js ├── lib ├── bus.js ├── cli │ ├── bchnode.js │ ├── bchnoded.js │ ├── daemon.js │ └── main.js ├── errors.js ├── index.js ├── logger.js ├── node.js ├── scaffold │ ├── add.js │ ├── call-method.js │ ├── create.js │ ├── default-base-config.js │ ├── default-config.js │ ├── find-config.js │ ├── remove.js │ └── start.js ├── service.js ├── services │ ├── bitcoind.js │ └── web.js └── utils.js ├── package.json ├── regtest ├── bitcoind.js ├── cluster.js ├── data │ ├── .gitignore │ ├── bitcoin.conf │ ├── bitcoind.crt │ ├── bitcoind_no_pass.key │ ├── node1 │ │ └── bitcoin.conf │ ├── node2 │ │ └── bitcoin.conf │ └── node3 │ │ └── bitcoin.conf ├── node.js └── p2p.js ├── scripts └── regtest └── test ├── bus.integration.js ├── bus.unit.js ├── data ├── badbitcoin.conf ├── bitcoin-transactions.json ├── bitcoin.conf ├── default.bitcoin.conf ├── hashes.json ├── livenet-345003.json ├── testnet-blocks.json └── transaction.json ├── index.unit.js ├── logger.unit.js ├── node.unit.js ├── scaffold ├── add.integration.js ├── call-method.unit.js ├── create.integration.js ├── default-base-config.integration.js ├── default-config.integration.js ├── find-config.integration.js ├── remove.integration.js ├── start.integration.js └── start.unit.js ├── services ├── bitcoind.unit.js └── web.unit.js └── utils.unit.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | node_modules/* 3 | coverage/* 4 | .lock-wscript 5 | *.swp 6 | *.Makefile 7 | *.target.gyp.mk 8 | *.node 9 | *.sln 10 | *.sdf 11 | *.vcxproj 12 | *.suo 13 | *.opensdf 14 | *.filters 15 | *.user 16 | *.project 17 | **/*.dylib 18 | **/*.so 19 | **/*.old 20 | **/*.files 21 | **/*.config 22 | **/*.creator 23 | *.log 24 | .DS_Store 25 | bin/bitcoin* 26 | bin/SHA256SUMS.asc 27 | regtest/data/node1/regtest 28 | regtest/data/node2/regtest 29 | regtest/data/node3/regtest 30 | package-lock.json 31 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "browser": true, 4 | "camelcase": false, 5 | "curly": true, 6 | "devel": false, 7 | "eqeqeq": true, 8 | "esnext": true, 9 | "freeze": true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef": true, 13 | "newcap": false, 14 | "noarg": true, 15 | "node": true, 16 | "noempty": true, 17 | "nonew": true, 18 | "quotmark": "single", 19 | "regexp": true, 20 | "smarttabs": false, 21 | "strict": true, 22 | "trailing": true, 23 | "undef": true, 24 | "unused": true, 25 | "maxparams": 4, 26 | "maxstatements": 15, 27 | "maxcomplexity": 10, 28 | "maxdepth": 3, 29 | "maxlen": 120, 30 | "multistr": true, 31 | "predef": [ 32 | "after", 33 | "afterEach", 34 | "before", 35 | "beforeEach", 36 | "describe", 37 | "exports", 38 | "it", 39 | "module", 40 | "require" 41 | ] 42 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | env: 4 | - CXX=g++-4.8 CC=gcc-4.8 5 | addons: 6 | apt: 7 | sources: 8 | - ubuntu-toolchain-r-test 9 | packages: 10 | - g++-4.8 11 | - gcc-4.8 12 | - libzmq3-dev 13 | node_js: 14 | - "v8" 15 | script: 16 | # - npm run regtest/bch 17 | - npm run test 18 | - npm run jshint 19 | after_success: 20 | - npm run coveralls 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.9 2 | 3 | # update apt-get 4 | RUN apt-get update && apt-get install -y dnsutils 5 | 6 | USER root 7 | # Set up non root user 8 | RUN useradd --user-group --create-home --shell /bin/false ows 9 | 10 | # Setup environment variables 11 | ENV NODE_ENV=production 12 | ENV PKG_NAME=bchnode 13 | ENV APP_NAME=bitcoin-cash-services 14 | ENV HOME_PATH=/home/ows 15 | ENV BITCOIN_DATA=/data 16 | 17 | ENV PKG_DIR=$HOME_PATH/$PKG_NAME 18 | ENV APP_DIR=$HOME_PATH/$APP_NAME 19 | 20 | # Set up folder and add install files 21 | RUN mkdir -p $PKG_DIR && mkdir -p $BITCOIN_DATA 22 | COPY package.json $PKG_DIR 23 | WORKDIR $PKG_DIR 24 | 25 | RUN chown -R ows:ows $HOME_PATH && chgrp ows /usr/local/lib/node_modules && chgrp ows /usr/local/bin 26 | 27 | USER ows 28 | RUN npm install -g @owstack/bch-node@0.1.0 29 | 30 | WORKDIR $HOME_PATH 31 | RUN $PKG_NAME create -d $BITCOIN_DATA $APP_NAME 32 | 33 | WORKDIR $APP_DIR 34 | RUN $PKG_NAME install @owstack/bch-explorer-api@0.0.8 35 | RUN $PKG_NAME install @owstack/bch-wallet-service@0.0.10 36 | RUN $PKG_NAME install @owstack/ows-explorer@0.0.3 37 | 38 | USER root 39 | CMD ["bchnode","start"] 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Open Wallet Stack 2 | 3 | Parts of this software are based on Bitcoin Core 4 | Copyright (c) 2009-2015 The Bitcoin Core developers 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bch Node 2 | ============ 3 | 4 | [![NPM Package](https://img.shields.io/npm/v/@owstack/bch-node.svg?style=flat-square)](https://www.npmjs.org/package/@owstack/bch-node) 5 | [![Build Status](https://img.shields.io/travis/owstack/bch-node.svg?branch=master&style=flat-square)](https://travis-ci.org/owstack/bch-node) 6 | [![Coverage Status](https://img.shields.io/coveralls/owstack/bch-node.svg?style=flat-square)](https://coveralls.io/r/owstack/bch-node) 7 | 8 | A Bitcoin Cash full node for building applications and services with Node.js. A node is extensible and can be configured to run additional services. Additional services can be enabled to make a node more useful such as exposing new APIs, running a block explorer and wallet service. 9 | 10 | ## Install 11 | 12 | ```bash 13 | npm install -g @owstack/bch-node 14 | bchnode start 15 | ``` 16 | 17 | ## Prerequisites 18 | 19 | - GNU/Linux x86_32/x86_64, or OSX 64bit *(for bitcoind distributed binaries)* 20 | - Node.js v0.10, v0.12 or v4 21 | - ZeroMQ *(libzmq3-dev for Ubuntu/Debian or zeromq on OSX)* 22 | - ~200GB of disk storage 23 | - ~8GB of RAM 24 | 25 | ## Configuration 26 | 27 | Bch-node includes a Command Line Interface (CLI) for managing, configuring and interfacing with your Bch Node. 28 | 29 | ```bash 30 | bchnode create -d mynode 31 | cd mynode 32 | bchnode install 33 | bchnode install https://github.com/yourname/helloworld 34 | ``` 35 | 36 | This will create a directory with configuration files for your node and install the necessary dependencies. For more information about (and developing) services, please see the [Service Documentation](docs/services.md). 37 | 38 | ## Add-on Services 39 | 40 | There are several add-on services available to extend the functionality of Bch-node: 41 | 42 | - [Explorer API](https://github.com/owstack/bch-explorer-api) 43 | - [OWS Explorer](https://github.com/owstack/ows-explorer) 44 | - [Bch Wallet Service](https://github.com/owstack/bch-wallet-service) 45 | 46 | ## Documentation 47 | 48 | - [Upgrade Notes](docs/upgrade.md) 49 | - [Services](docs/services.md) 50 | - [Bitcoind](docs/services/bitcoind.md) - Interface to Bitcoin Core 51 | - [Web](docs/services/web.md) - Creates an express application over which services can expose their web/API content 52 | - [Development Environment](docs/development.md) - Guide for setting up a development environment 53 | - [Node](docs/node.md) - Details on the node constructor 54 | - [Bus](docs/bus.md) - Overview of the event bus constructor 55 | - [Release Process](docs/release.md) - Information about verifying a release and the release process. 56 | 57 | ## Contributing 58 | 59 | Please send pull requests for bug fixes, code optimization, and ideas for improvement. For more information on how to contribute, please refer to our [CONTRIBUTING](https://github.com/owstack/bch/blob/master/CONTRIBUTING.md) file. 60 | 61 | ## License 62 | 63 | Code released under [the MIT license](https://github.com/owstack/bch-node/blob/master/LICENSE). 64 | 65 | Copyright 2017 Open Wallet Stack 66 | 67 | - bitcoin: Copyright (c) 2009-2015 Bitcoin Core Developers (MIT License) 68 | -------------------------------------------------------------------------------- /benchmarks/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var benchmark = require('benchmark'); 4 | var bitcoin = require('bitcoin'); 5 | var async = require('async'); 6 | var maxTime = 20; 7 | 8 | console.log('Bitcoin Service native interface vs. Bitcoin JSON RPC interface'); 9 | console.log('----------------------------------------------------------------------'); 10 | 11 | // To run the benchmarks a fully synced Bch Core directory is needed. The RPC comands 12 | // can be modified to match the settings in bitcoin.conf. 13 | 14 | var fixtureData = { 15 | blockHashes: [ 16 | '00000000fa7a4acea40e5d0591d64faf48fd862fa3561d111d967fc3a6a94177', 17 | '000000000017e9e0afc4bc55339f60ffffb9cbe883f7348a9fbc198a486d5488', 18 | '000000000019ddb889b534c5d85fca2c91a73feef6fd775cd228dea45353bae1', 19 | '0000000000977ac3d9f5261efc88a3c2d25af92a91350750d00ad67744fa8d03' 20 | ], 21 | txHashes: [ 22 | '5523b432c1bd6c101bee704ad6c560fd09aefc483f8a4998df6741feaa74e6eb', 23 | 'ff48393e7731507c789cfa9cbfae045b10e023ce34ace699a63cdad88c8b43f8', 24 | '5d35c5eebf704877badd0a131b0a86588041997d40dbee8ccff21ca5b7e5e333', 25 | '88842f2cf9d8659c3434f6bc0c515e22d87f33e864e504d2d7117163a572a3aa', 26 | ] 27 | }; 28 | 29 | var bitcoind = require('../').services.Bitcoin({ 30 | node: { 31 | datadir: process.env.HOME + '/.bitcoin', 32 | network: { 33 | name: 'testnet' 34 | } 35 | } 36 | }); 37 | 38 | bitcoind.on('error', function(err) { 39 | console.error(err.message); 40 | }); 41 | 42 | bitcoind.start(function(err) { 43 | if (err) { 44 | throw err; 45 | } 46 | console.log('Bitcoin Core started'); 47 | }); 48 | 49 | bitcoind.on('ready', function() { 50 | 51 | console.log('Bitcoin Core ready'); 52 | 53 | var client = new bitcoin.Client({ 54 | host: 'localhost', 55 | port: 18332, 56 | user: 'bitcoin', 57 | pass: 'local321' 58 | }); 59 | 60 | async.series([ 61 | function(next) { 62 | 63 | var c = 0; 64 | var hashesLength = fixtureData.blockHashes.length; 65 | var txLength = fixtureData.txHashes.length; 66 | 67 | function bitcoindGetBlockNative(deffered) { 68 | if (c >= hashesLength) { 69 | c = 0; 70 | } 71 | var hash = fixtureData.blockHashes[c]; 72 | bitcoind.getBlock(hash, function(err, block) { 73 | if (err) { 74 | throw err; 75 | } 76 | deffered.resolve(); 77 | }); 78 | c++; 79 | } 80 | 81 | function bitcoindGetBlockJsonRpc(deffered) { 82 | if (c >= hashesLength) { 83 | c = 0; 84 | } 85 | var hash = fixtureData.blockHashes[c]; 86 | client.getBlock(hash, false, function(err, block) { 87 | if (err) { 88 | throw err; 89 | } 90 | deffered.resolve(); 91 | }); 92 | c++; 93 | } 94 | 95 | function bitcoinGetTransactionNative(deffered) { 96 | if (c >= txLength) { 97 | c = 0; 98 | } 99 | var hash = fixtureData.txHashes[c]; 100 | bitcoind.getTransaction(hash, true, function(err, tx) { 101 | if (err) { 102 | throw err; 103 | } 104 | deffered.resolve(); 105 | }); 106 | c++; 107 | } 108 | 109 | function bitcoinGetTransactionJsonRpc(deffered) { 110 | if (c >= txLength) { 111 | c = 0; 112 | } 113 | var hash = fixtureData.txHashes[c]; 114 | client.getRawTransaction(hash, function(err, tx) { 115 | if (err) { 116 | throw err; 117 | } 118 | deffered.resolve(); 119 | }); 120 | c++; 121 | } 122 | 123 | var suite = new benchmark.Suite(); 124 | 125 | suite.add('bitcoind getblock (native)', bitcoindGetBlockNative, { 126 | defer: true, 127 | maxTime: maxTime 128 | }); 129 | 130 | suite.add('bitcoind getblock (json rpc)', bitcoindGetBlockJsonRpc, { 131 | defer: true, 132 | maxTime: maxTime 133 | }); 134 | 135 | suite.add('bitcoind gettransaction (native)', bitcoinGetTransactionNative, { 136 | defer: true, 137 | maxTime: maxTime 138 | }); 139 | 140 | suite.add('bitcoind gettransaction (json rpc)', bitcoinGetTransactionJsonRpc, { 141 | defer: true, 142 | maxTime: maxTime 143 | }); 144 | 145 | suite 146 | .on('cycle', function(event) { 147 | console.log(String(event.target)); 148 | }) 149 | .on('complete', function() { 150 | console.log('Fastest is ' + this.filter('fastest').pluck('name')); 151 | console.log('----------------------------------------------------------------------'); 152 | next(); 153 | }) 154 | .run(); 155 | } 156 | ], function(err) { 157 | if (err) { 158 | throw err; 159 | } 160 | console.log('Finished'); 161 | bitcoind.stop(function(err) { 162 | if (err) { 163 | console.error('Fail to stop services: ' + err); 164 | process.exit(1); 165 | } 166 | process.exit(0); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /bin/bchnode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var bchnode = require('../lib/cli/bchnode'); 4 | bchnode(); 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | services: 3 | bitcoin-abc-conf: 4 | build: extra/compose 5 | volumes: 6 | - bitcoin-abc-data-dir:/data 7 | - bch-conf-dir:/home/ows/config 8 | bitcoin-abc: 9 | image: "owstack/bitcoin-abc:0.14.6-ows" 10 | user: root 11 | command: "/usr/local/bin/bitcoind -datadir=/data" 12 | ports: 13 | - "8332:8332" 14 | - "8333:8333" 15 | - "18332:18332" 16 | - "18333:18333" 17 | - "28332:28332" 18 | - "28333:28333" 19 | volumes: 20 | - bitcoin-abc-data-dir:/data 21 | depends_on: 22 | - "bitcoin-abc-conf" 23 | bch-node: 24 | build: . 25 | ports: 26 | - "3001:3001" 27 | depends_on: 28 | - "bitcoin-abc" 29 | volumes: 30 | - bch-conf-dir:/home/ows/config 31 | command: "bchnode start -c /home/ows/config" 32 | volumes: 33 | bitcoin-abc-data-dir: 34 | bch-conf-dir: 35 | -------------------------------------------------------------------------------- /docs/bus.md: -------------------------------------------------------------------------------- 1 | # Bus 2 | The bus provides a way to subscribe to events from any of the services running. It's implemented abstract from transport specific implementation. The primary use of the bus in Bch Node is for subscribing to events via a web socket. 3 | 4 | ## Opening/Closing 5 | 6 | ```javascript 7 | 8 | // a node is needed to be able to open a bus 9 | var node = new Node(configuration); 10 | 11 | // will create a new bus that is ready to subscribe to events 12 | var bus = node.openBus(); 13 | 14 | // will remove all event listeners 15 | bus.close(); 16 | ``` 17 | 18 | ## Subscribing/Unsubscribing 19 | 20 | ```javascript 21 | 22 | // subscribe to all transaction events 23 | bus.subscribe('bitcoind/rawtransaction'); 24 | 25 | // to subscribe to new block hashes 26 | bus.subscribe('bitcoind/hashblock'); 27 | 28 | // unsubscribe 29 | bus.unsubscribe('bitcoind/rawtransaction'); 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Setting up Development Environment 2 | 3 | ## Install Node.js 4 | 5 | Install Node.js by your favorite method, or use Node Version Manager by following directions at https://github.com/creationix/nvm 6 | 7 | ```bash 8 | nvm install v4 9 | ``` 10 | 11 | ## Fork and Download Repositories 12 | 13 | To develop bch-node: 14 | 15 | ```bash 16 | cd ~ 17 | git clone git@github.com:/bch-node.git 18 | git clone git@github.com:/bch-lib.git 19 | ``` 20 | 21 | To develop bitcoin or to compile from source: 22 | 23 | ```bash 24 | git clone git@github.com:/bitcoin.git 25 | git fetch origin : 26 | git checkout 27 | ``` 28 | **Note**: See bitcoin documentation for building bitcoin on your platform. 29 | 30 | 31 | ## Install Development Dependencies 32 | 33 | For Ubuntu: 34 | ```bash 35 | sudo apt-get install libzmq3-dev 36 | sudo apt-get install build-essential 37 | ``` 38 | **Note**: Make sure that libzmq-dev is not installed, it should be removed when installing libzmq3-dev. 39 | 40 | 41 | For Mac OS X: 42 | ```bash 43 | brew install zeromq 44 | ``` 45 | 46 | ## Install and Symlink 47 | 48 | ```bash 49 | cd bch-lib 50 | npm install 51 | cd ../bch-node 52 | npm install 53 | ``` 54 | **Note**: If you get a message about not being able to download bitcoin distribution, you'll need to compile bitcoind from source, and setup your configuration to use that version. 55 | 56 | 57 | We now will setup symlinks in `bch-node` *(repeat this for any other modules you're planning on developing)*: 58 | ```bash 59 | cd node_modules 60 | rm -rf bch-lib 61 | ln -s ~/bch-lib 62 | rm -rf bitcoind-rpc 63 | ln -s ~/bitcoind-rpc 64 | ``` 65 | 66 | And if you're compiling or developing bitcoin: 67 | ```bash 68 | cd ../bin 69 | ln -sf ~/bitcoin/src/bitcoind 70 | ``` 71 | 72 | ## Run Tests 73 | 74 | If you do not already have mocha installed: 75 | ```bash 76 | npm install mocha -g 77 | ``` 78 | 79 | To run all test suites: 80 | ```bash 81 | cd bch-node 82 | npm run regtest 83 | npm run test 84 | ``` 85 | 86 | To run a specific unit test in watch mode: 87 | ```bash 88 | mocha -w -R spec test/services/bitcoind.unit.js 89 | ``` 90 | 91 | To run a specific regtest: 92 | ```bash 93 | mocha -R spec regtest/bitcoind.js 94 | ``` 95 | 96 | ## Running a Development Node 97 | 98 | To test running the node, you can setup a configuration that will specify development versions of all of the services: 99 | 100 | ```bash 101 | cd ~ 102 | mkdir devnode 103 | cd devnode 104 | mkdir node_modules 105 | touch bch-node.json 106 | touch package.json 107 | ``` 108 | 109 | Edit `bch-node.json` with something similar to: 110 | ```json 111 | { 112 | "network": "livenet", 113 | "port": 3001, 114 | "services": [ 115 | "bitcoind", 116 | "web", 117 | "explorer-api", 118 | "ows-explorer", 119 | "" 120 | ], 121 | "servicesConfig": { 122 | "bitcoind": { 123 | "spawn": { 124 | "datadir": "/home//.bitcoin", 125 | "exec": "/home//bitcoin/src/bitcoind" 126 | } 127 | }, 128 | "explorer-api": { 129 | "module": "bch-explorer-api" 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | **Note**: To install services [bch-explorer-api](https://github.com/owstack/bch-explorer-api) and [ows-explorer](https://github.com/owstack/ows-explorer) you'll need to clone the repositories locally. 136 | 137 | Setup symlinks for all of the services and dependencies: 138 | 139 | ```bash 140 | cd node_modules 141 | ln -s ~/bch-lib 142 | ln -s ~/bch-node 143 | ln -s ~/bch-explorer-api 144 | ln -s ~/ows-explorer 145 | ``` 146 | 147 | Make sure that the `/bitcoin.conf` has the necessary settings, for example: 148 | ``` 149 | server=1 150 | whitelist=127.0.0.1 151 | txindex=1 152 | addressindex=1 153 | timestampindex=1 154 | spentindex=1 155 | zmqpubrawtx=tcp://127.0.0.1:28332 156 | zmqpubhashblock=tcp://127.0.0.1:28332 157 | rpcallowip=127.0.0.1 158 | rpcuser=bitcoin 159 | rpcpassword=local321 160 | ``` 161 | 162 | From within the `devnode` directory with the configuration file, start the node: 163 | ```bash 164 | ../bch-node/bin/bch-node start 165 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/node.md: -------------------------------------------------------------------------------- 1 | # Node 2 | A node represents a collection of services that are loaded together. For more information about services, please see the [Services Documentation](services.md). 3 | 4 | ## API Documentation 5 | - `start()` - Will start the node's services in the correct order based on the dependencies of a service. 6 | - `stop()` - Will stop the node's services. 7 | - `openBus()` - Will create a new event bus to subscribe to events. 8 | - `getAllAPIMethods()` - Returns information about all of the API methods from the services. 9 | - `getAllPublishEvents()` - Returns information about publish events. 10 | - `getServiceOrder()` - Returns an array of service modules. 11 | - `services..` - Additional API methods exposed by each service. The services for the node are defined when the node instance is constructed. 12 | 13 | ## Example Usage 14 | 15 | ```js 16 | 17 | var index = require('@owstack/bch-node'); 18 | var Bitcoin = index.services.Bitcoin; 19 | var Node = index.Node; 20 | 21 | var configuration = { 22 | datadir: '/home/user/.bitcoin', 23 | network: 'testnet', 24 | services: [ 25 | { 26 | name: 'bitcoind', 27 | module: Bitcoin, 28 | config: {} 29 | } 30 | ] 31 | }; 32 | 33 | var node = new Node(configuration); 34 | 35 | node.start(function() { 36 | //start the node so the node.on('ready') is actually called. 37 | }); 38 | 39 | node.on('ready', function() { 40 | console.log('Bitcoin Node Ready'); 41 | }); 42 | 43 | node.on('error', function(err) { 44 | console.error(err); 45 | }); 46 | 47 | // shutdown the node 48 | node.stop(function() { 49 | // the shutdown is complete 50 | }); 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Binaries for bitcoind are distributed for convenience and built deterministically with Gitian, signatures for bitcoind are located at the [gitian.sigs](https://github.com/owstack/gitian.sigs) respository. 4 | 5 | ## How to Release 6 | 7 | When publishing to npm, the .gitignore file is used to exclude files from the npm publishing process. Be sure that the bch-node directory has only the directories and files that you would like to publish to npm. You might need to run the commands below on each platform that you intend to publish (e.g. Mac and Linux). 8 | 9 | To make a release, bump the `version` of the `package.json`: 10 | 11 | ```bash 12 | git checkout master 13 | git pull upstream master 14 | npm install 15 | npm run test 16 | npm run regtest 17 | npm run jshint 18 | git commit -a -m "Bump package version to " 19 | git push upstream master 20 | npm publish 21 | ``` 22 | 23 | Create a release tag and push it to the Open Wallet Stack Github repo: 24 | 25 | ```bash 26 | git tag -s v -m 'v' 27 | git push upstream v 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/scaffold.md: -------------------------------------------------------------------------------- 1 | # Scaffold 2 | A collection of functions for creating, managing, starting, stopping and interacting with a Bch node. 3 | 4 | ## Install 5 | This function will add a service to a node by installing the necessary dependencies and modifying the `bch-node.json` configuration. 6 | 7 | ## Start 8 | This function will load a configuration file `bch-node.json` and instantiate and start a node based on the configuration. 9 | 10 | ## Find Config 11 | This function will recursively find a configuration `bch-node.json` file in parent directories and return the result. 12 | 13 | ## Default Config 14 | This function will return a default configuration with the default services based on environment variables, and will default to using the standard `/home/user/.bitcoin` data directory. 15 | 16 | ## Uninstall 17 | This function will remove a service from a node by uninstalling the necessary dependencies and modifying the `bch-node.json` configuration. 18 | 19 | ## Call Method 20 | This function will call an API method on a node via the JSON-RPC interface. 21 | -------------------------------------------------------------------------------- /docs/services.md: -------------------------------------------------------------------------------- 1 | # Services 2 | Bch Node has a service module system that can start up additional services that can include additional: 3 | - Blockchain indexes (e.g. querying balances for addresses) 4 | - API methods 5 | - HTTP routes 6 | - Event types to publish and subscribe 7 | 8 | The `bch-node.json` file describes which services will load for a node: 9 | 10 | ```json 11 | { 12 | "services": [ 13 | "bitcoind", "web" 14 | ] 15 | } 16 | ``` 17 | 18 | Services correspond with a Node.js module as described in 'package.json', for example: 19 | 20 | ```json 21 | { 22 | "dependencies": { 23 | "@owstack/bch-lib": "^0.13.7", 24 | "@owstack/bch-node": "^0.2.0", 25 | "@owstack/bch-explorer-api": "^3.0.0" 26 | } 27 | } 28 | ``` 29 | 30 | _Note:_ If you already have a bch-node database, and you want to query data from previous blocks in the blockchain, you will need to reindex. Reindexing right now means deleting your bch-node database and resyncing. 31 | 32 | ## Using Services Programmatically 33 | If, instead, you would like to run a custom node, you can include services by including them in your configuration object when initializing a new node. 34 | 35 | ```js 36 | //Require bch-node 37 | var bchNode = require('@owstack/bch-node'); 38 | 39 | //Services 40 | var Bitcoin = bchNode.services.Bitcoin; 41 | var Web = bchNode.services.Web; 42 | 43 | var myNode = new bchNode.Node({ 44 | network: 'regtest' 45 | services: [ 46 | { 47 | name: 'bitcoind', 48 | module: Bitcoin, 49 | config: { 50 | spawn: { 51 | datadir: '/home//.bitcoin', 52 | exec: '/home//bch-node/bin/bitcoind' 53 | } 54 | } 55 | }, 56 | { 57 | name: 'web', 58 | module: Web, 59 | config: { 60 | port: 3001 61 | } 62 | } 63 | ] 64 | }); 65 | ``` 66 | 67 | Now that you've loaded your services you can access them via `myNode.services..`. For example if you wanted to check the balance of an address, you could access the address service like so. 68 | 69 | ```js 70 | myNode.services.bitcoind.getAddressBalance('1HB5XMLmzFVj8ALj6mfBsbifRoD4miY36v', false, function(err, total) { 71 | console.log(total.balance); //Satoshi amount of this address 72 | }); 73 | ``` 74 | 75 | ## Writing a Service 76 | A new service can be created by inheriting from `Node.Service` and implementing these methods and properties: 77 | - `Service.dependencies` - An array of services that are needed, this will determine the order that services are started on the node. 78 | - `Service.prototype.start()` - Called to start up the service. 79 | - `Service.prototype.stop()` - Called to stop the service. 80 | - `Service.prototype.blockHandler()` - Will be called when a block is added or removed from the chain, and is useful for updating a database view/index. 81 | - `Service.prototype.getAPIMethods()` - Describes which API methods that this service includes, these methods can then be called over the JSON-RPC API, as well as the command-line utility. 82 | - `Service.prototype.getPublishEvents()` - Describes which events can be subscribed to for this service, useful to subscribe to events over the included web socket API. 83 | - `Service.prototype.setupRoutes()` - A service can extend HTTP routes on an express application by implementing this method. 84 | 85 | The `package.json` for the service module can either export the `Node.Service` directly, or specify a specific module to load by including `"owsNode": "lib/ows-node.js"`. 86 | 87 | Please take a look at some of the existing services for implementation specifics. 88 | 89 | -------------------------------------------------------------------------------- /docs/services/bitcoind.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Service 2 | 3 | The Bitcoin Service is a Node.js interface to [Bitcoin Core](https://github.com/bitcoin/bitcoin) for querying information about the bitcoin block chain. It will manage starting and stopping `bitcoind` or connect to several running `bitcoind` processes. It uses a branch of a [branch of Bitcoin Core](https://github.com/owstack/bitcoin/tree/0.12.1-bch) with additional indexes for querying information about addresses and blocks. Results are cached for performance and there are several additional API methods added for common queries. 4 | 5 | ## Configuration 6 | 7 | The default configuration will include a "spawn" configuration in "bitcoind". This defines the location of the block chain database and the location of the `bitcoind` daemon executable. The below configuration points to a local clone of `bitcoin`, and will start `bitcoind` automatically with your Node.js application. 8 | 9 | ```json 10 | "servicesConfig": { 11 | "bitcoind": { 12 | "spawn": { 13 | "datadir": "/home/bch/.bitcoin", 14 | "exec": "/home/bch/bitcoin/src/bitcoind" 15 | } 16 | } 17 | } 18 | ``` 19 | 20 | It's also possible to connect to separately managed `bitcoind` processes with round-robin quering, for example: 21 | 22 | ```json 23 | "servicesConfig": { 24 | "bitcoind": { 25 | "connect": [ 26 | { 27 | "rpchost": "127.0.0.1", 28 | "rpcport": 30521, 29 | "rpcuser": "bitcoin", 30 | "rpcpassword": "local321", 31 | "zmqpubrawtx": "tcp://127.0.0.1:30611" 32 | }, 33 | { 34 | "rpchost": "127.0.0.1", 35 | "rpcport": 30522, 36 | "rpcuser": "bitcoin", 37 | "rpcpassword": "local321", 38 | "zmqpubrawtx": "tcp://127.0.0.1:30622" 39 | }, 40 | { 41 | "rpchost": "127.0.0.1", 42 | "rpcport": 30523, 43 | "rpcuser": "bitcoin", 44 | "rpcpassword": "local321", 45 | "zmqpubrawtx": "tcp://127.0.0.1:30633" 46 | } 47 | ] 48 | } 49 | } 50 | ``` 51 | 52 | **Note**: For detailed example configuration see [`regtest/cluster.js`](regtest/cluster.js) 53 | 54 | 55 | ## API Documentation 56 | Methods are available by directly interfacing with the service: 57 | 58 | ```js 59 | node.services.bitcoind. 60 | ``` 61 | 62 | ### Chain 63 | 64 | **Getting Latest Blocks** 65 | 66 | ```js 67 | // gives the block hashes sorted from low to high within a range of timestamps 68 | var high = 1460393372; // Mon Apr 11 2016 12:49:25 GMT-0400 (EDT) 69 | var low = 1460306965; // Mon Apr 10 2016 12:49:25 GMT-0400 (EDT) 70 | node.services.bitcoind.getBlockHashesByTimestamp(high, low, function(err, blockHashes) { 71 | //... 72 | }); 73 | 74 | // get the current tip of the chain 75 | node.services.bitcoind.getBestBlockHash(function(err, blockHash) { 76 | //... 77 | }) 78 | ``` 79 | 80 | **Getting Synchronization and Node Status** 81 | 82 | ```js 83 | // gives a boolean if the daemon is fully synced (not the initial block download) 84 | node.services.bitcoind.isSynced(function(err, synced) { 85 | //... 86 | }) 87 | 88 | // gives the current estimate of blockchain download as a percentage 89 | node.services.bitcoind.syncPercentage(function(err, percent) { 90 | //... 91 | }); 92 | 93 | // gives information about the chain including total number of blocks 94 | node.services.bitcoind.getInfo(function(err, info) { 95 | //... 96 | }); 97 | ``` 98 | 99 | **Generate Blocks** 100 | 101 | ```js 102 | // will generate a block for the "regtest" network (development purposes) 103 | var numberOfBlocks = 10; 104 | node.services.bitcoind.generateBlock(numberOfBlocks, function(err, blockHashes) { 105 | //... 106 | }); 107 | ``` 108 | 109 | ### Blocks and Transactions 110 | 111 | **Getting Block Information** 112 | 113 | It's possible to query blocks by both block hash and by height. Blocks are given as Node.js Buffers and can be parsed via Bch: 114 | 115 | ```js 116 | var blockHeight = 0; 117 | node.services.bitcoind.getRawBlock(blockHeight, function(err, blockBuffer) { 118 | if (err) { 119 | throw err; 120 | } 121 | var block = bch.Block.fromBuffer(blockBuffer); 122 | console.log(block); 123 | }; 124 | 125 | // get a bch object of the block (as above) 126 | node.services.bitcoind.getBlock(blockHash, function(err, block) { 127 | //... 128 | }; 129 | 130 | // get only the block header and index (including chain work, height, and previous hash) 131 | node.services.bitcoind.getBlockHeader(blockHeight, function(err, blockHeader) { 132 | //... 133 | }); 134 | 135 | // get the block with a list of txids 136 | node.services.bitcoind.getBlockOverview(blockHash, function(err, blockOverview) { 137 | //... 138 | }; 139 | ``` 140 | 141 | **Retrieving and Sending Transactions** 142 | 143 | Get a transaction asynchronously by reading it from disk: 144 | 145 | ```js 146 | var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; 147 | node.services.bitcoind.getRawTransaction(txid, function(err, transactionBuffer) { 148 | if (err) { 149 | throw err; 150 | } 151 | var transaction = bch.Transaction().fromBuffer(transactionBuffer); 152 | }); 153 | 154 | // get a bch object of the transaction (as above) 155 | node.services.bitcoind.getTransaction(txid, function(err, transaction) { 156 | //... 157 | }); 158 | 159 | // retrieve the transaction with input values, fees, spent and block info 160 | node.services.bitcoind.getDetailedTransaction(txid, function(err, transaction) { 161 | //... 162 | }); 163 | ``` 164 | 165 | Send a transaction to the network: 166 | 167 | ```js 168 | var numberOfBlocks = 3; 169 | node.services.bitcoind.estimateFee(numberOfBlocks, function(err, feesPerKilobyte) { 170 | //... 171 | }); 172 | 173 | node.services.bitcoind.sendTransaction(transaction.serialize(), function(err, hash) { 174 | //... 175 | }); 176 | ``` 177 | 178 | ### Addresses 179 | 180 | **Get Unspent Outputs** 181 | 182 | One of the most common uses will be to retrieve unspent outputs necessary to create a transaction, here is how to get the unspent outputs for an address: 183 | 184 | ```js 185 | var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; 186 | node.services.bitcoind.getAddressUnspentOutputs(address, options, function(err, unspentOutputs) { 187 | // see below 188 | }); 189 | ``` 190 | 191 | The `unspentOutputs` will have the format: 192 | 193 | ```js 194 | [ 195 | { 196 | address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW', 197 | txid: '9d956c5d324a1c2b12133f3242deff264a9b9f61be701311373998681b8c1769', 198 | outputIndex: 1, 199 | height: 150, 200 | satoshis: 1000000000, 201 | script: '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac', 202 | confirmations: 3 203 | } 204 | ] 205 | ``` 206 | 207 | **View Balances** 208 | 209 | ```js 210 | var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; 211 | node.services.bitcoind.getAddressBalance(address, options, function(err, balance) { 212 | // balance will be in satoshis with "received" and "balance" 213 | }); 214 | ``` 215 | 216 | **View Address History** 217 | 218 | This method will give history of an address limited by a range of block heights by using the "start" and "end" arguments. The "start" value is the more recent, and greater, block height. The "end" value is the older, and lesser, block height. This feature is most useful for synchronization as previous history can be omitted. Furthermore for large ranges of block heights, results can be paginated by using the "from" and "to" arguments. 219 | 220 | If "queryMempool" is set as true (it is true by default), it will show unconfirmed transactions from the bitcoin mempool. However, if you specify "start" and "end", "queryMempool" is ignored and is always false. 221 | 222 | If "queryMempoolOnly" is set as true (it is false by default), it will show *only* unconfirmed transactions from mempool. 223 | 224 | ```js 225 | var addresses = ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']; 226 | var options = { 227 | start: 345000, 228 | end: 344000, 229 | queryMempool: true // since we presented range, queryMempool will be ignored 230 | }; 231 | node.services.bitcoind.getAddressHistory(addresses, options, function(err, history) { 232 | // see below 233 | }); 234 | ``` 235 | 236 | The history format will be: 237 | 238 | ```js 239 | { 240 | totalCount: 1, // The total number of items within "start" and "end" 241 | items: [ 242 | { 243 | addresses: { 244 | 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW': { 245 | inputIndexes: [], 246 | outputIndexes: [0] 247 | } 248 | }, 249 | satoshis: 1000000000, 250 | tx: // the same format as getDetailedTransaction 251 | } 252 | ] 253 | } 254 | ``` 255 | 256 | **View Address Summary** 257 | 258 | ```js 259 | var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; 260 | var options = { 261 | noTxList: false 262 | }; 263 | 264 | node.services.bitcoind.getAddressSummary(address, options, function(err, summary) { 265 | // see below 266 | }); 267 | ``` 268 | 269 | The `summary` will have the format (values are in satoshis): 270 | 271 | ```js 272 | { 273 | totalReceived: 1000000000, 274 | totalSpent: 0, 275 | balance: 1000000000, 276 | unconfirmedBalance: 1000000000, 277 | appearances: 1, 278 | unconfirmedAppearances: 0, 279 | txids: [ 280 | '3f7d13efe12e82f873f4d41f7e63bb64708fc4c942eb8c6822fa5bd7606adb00' 281 | ] 282 | } 283 | ``` 284 | **Notes**: 285 | - `totalReceived` does not exclude change *(the amount of satoshis originating from the same address)* 286 | - `unconfirmedBalance` is the delta that the unconfirmed transactions have on the total balance *(can be both positive and negative)* 287 | - `unconfirmedAppearances` is the total number of unconfirmed transactions 288 | - `appearances` is the total confirmed transactions 289 | - `txids` Are sorted in block order with the most recent at the beginning. A maximum of 1000 *(default)* will be returned, the `from` and `to` options can be used to get further values. 290 | 291 | 292 | ## Events 293 | The Bitcoin Service exposes two events via the Bus, and there are a few events that can be directly registered: 294 | 295 | ```js 296 | node.services.bitcoind.on('tip', function(blockHash) { 297 | // a new block tip has been added, if there is a rapid update (with a second) this will not emit every tip update 298 | }); 299 | 300 | node.services.bitcoind.on('tx', function(transactionBuffer) { 301 | // a new transaction has entered the mempool 302 | }); 303 | 304 | node.services.bitcoind.on('block', function(blockHash) { 305 | // a new block has been added 306 | }); 307 | ``` 308 | 309 | For details on instantiating a bus for a node, see the [Bus Documentation](../bus.md). 310 | - Name: `bitcoind/rawtransaction` 311 | - Name: `bitcoind/hashblock` 312 | - Name: `bitcoind/addresstxid`, Arguments: [address, address...] 313 | 314 | **Examples:** 315 | 316 | ```js 317 | bus.subscribe('bitcoind/rawtransaction'); 318 | bus.subscribe('bitcoind/hashblock'); 319 | bus.subscribe('bitcoind/addresstxid', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); 320 | 321 | bus.on('bitcoind/rawtransaction', function(transactionHex) { 322 | //... 323 | }); 324 | 325 | bus.on('bitcoind/hashblock', function(blockhashHex) { 326 | //... 327 | }); 328 | 329 | bus.on('bitcoind/addresstxid', function(data) { 330 | // data.address; 331 | // data.txid; 332 | }); 333 | ``` 334 | -------------------------------------------------------------------------------- /docs/services/web.md: -------------------------------------------------------------------------------- 1 | # Web Service 2 | The web service creates an express app which can be used by services for setting up web routes for API's, static content, web applications, etc. This allows users to interact with various bch node services over one http or https port. 3 | 4 | In order for your service to add routes, it must implement the `setupRoutes()` and `getRoutePrefix()` methods. 5 | 6 | ## Example 7 | 8 | ```js 9 | MyService.prototype.setupRoutes = function(app, express) { 10 | // Set up routes 11 | app.get('/hello', function(req, res) { 12 | res.send('world'); 13 | }); 14 | 15 | // Serve static content 16 | app.use('/static', express.static(__dirname + '/static')); 17 | }; 18 | 19 | MyService.prototype.getRoutePrefix = function() { 20 | return 'my-service' 21 | }; 22 | ``` 23 | 24 | ## Configuring Web Service for HTTPS 25 | You can run the web service over https by editing your bch node config, setting https to true and adding httpsOptions: 26 | 27 | ```json 28 | { 29 | "port": 3001, 30 | "https": true, 31 | "httpsOptions": { 32 | "key": "path-to-private-key", 33 | "cert": "path-to-certificate" 34 | }, 35 | "services": [ 36 | "web" 37 | ] 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /extra/compose/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM buildpack-deps:jessie 2 | MAINTAINER Ian Patton (ian.patton@gmail.com) 3 | 4 | ENV BITCOIN_DATA=/data 5 | ENV CONFIG_DIR=/home/ows/config 6 | 7 | RUN mkdir -p $BITCOIN_DATA 8 | RUN mkdir -p $CONFIG_DIR 9 | 10 | COPY default.bitcoin.conf $BITCOIN_DATA/bitcoin.conf 11 | COPY default.bch-node.json $CONFIG_DIR/bch-node.json 12 | COPY default.bch-node.conf $CONFIG_DIR/bitcoin.conf 13 | 14 | CMD ["echo","installed config files"] 15 | -------------------------------------------------------------------------------- /extra/compose/default.bch-node.conf: -------------------------------------------------------------------------------- 1 | server=1 2 | txindex=1 3 | addressindex=1 4 | timestampindex=1 5 | spentindex=1 6 | zmqpubrawtx=tcp://bitcoin-abc:28332 7 | zmqpubhashblock=tcp://bitcoin-abc:28332 8 | rpcallowip=0.0.0.0/0 9 | rpcprotocol=http 10 | rpchost=bitcoin-abc 11 | rpcuser=bitcoin 12 | rpcpassword=local321 13 | uacomment=bch-node 14 | -------------------------------------------------------------------------------- /extra/compose/default.bch-node.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "livenet", 3 | "port": 3001, 4 | "services": [ 5 | "explorer-api", 6 | "bitcoind", 7 | "web" 8 | ], 9 | "servicesConfig": { 10 | "bitcoind": { 11 | "spawn": { 12 | "datadir": "/home/ows/config", 13 | "exec": "docker" 14 | }, 15 | "connect": { 16 | "rpcprotocol": "http", 17 | "rpchost": "bitcoin-abc", 18 | "rpcuser": "bitcoin", 19 | "rpcpassword": "local321", 20 | "rpcstrict": true 21 | } 22 | }, 23 | "explorer-api": { 24 | "module": "bch-explorer-api", 25 | "apiPrefix": "explorer-api" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /extra/compose/default.bitcoin.conf: -------------------------------------------------------------------------------- 1 | server=1 2 | txindex=1 3 | addressindex=1 4 | timestampindex=1 5 | spentindex=1 6 | zmqpubrawtx=tcp://0.0.0.0:28332 7 | zmqpubhashblock=tcp://0.0.0.0:28332 8 | rpcallowip=0.0.0.0/0 9 | rpcuser=bitcoin 10 | rpcpassword=local321 11 | uacomment=bch 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib'); 4 | module.exports.Node = require('./lib/node'); 5 | module.exports.Service = require('./lib/service'); 6 | module.exports.errors = require('./lib/errors'); 7 | 8 | module.exports.services = {}; 9 | module.exports.services.Bitcoin = require('./lib/services/bitcoind'); 10 | module.exports.services.Web = require('./lib/services/web'); 11 | 12 | module.exports.scaffold = {}; 13 | module.exports.scaffold.create = require('./lib/scaffold/create'); 14 | module.exports.scaffold.add = require('./lib/scaffold/add'); 15 | module.exports.scaffold.remove = require('./lib/scaffold/remove'); 16 | module.exports.scaffold.start = require('./lib/scaffold/start'); 17 | module.exports.scaffold.callMethod = require('./lib/scaffold/call-method'); 18 | module.exports.scaffold.findConfig = require('./lib/scaffold/find-config'); 19 | module.exports.scaffold.defaultConfig = require('./lib/scaffold/default-config'); 20 | 21 | module.exports.cli = {}; 22 | module.exports.cli.main = require('./lib/cli/main'); 23 | module.exports.cli.daemon = require('./lib/cli/daemon'); 24 | module.exports.cli.bchnode = require('./lib/cli/bchnode'); 25 | module.exports.cli.bchnoded = require('./lib/cli/bchnoded'); 26 | 27 | module.exports.lib = require('@owstack/bch-lib'); 28 | -------------------------------------------------------------------------------- /lib/bus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | 6 | /** 7 | * The bus represents a connection to node, decoupled from the transport layer, that can 8 | * listen and subscribe to any events that are exposed by available services. Services 9 | * can expose events that can be subscribed to by implementing a `getPublishEvents` method. 10 | * @param {Object} params 11 | * @param {Node} params.node - A reference to the node 12 | */ 13 | function Bus(params) { 14 | events.EventEmitter.call(this); 15 | this.node = params.node; 16 | this.remoteAddress = params.remoteAddress; 17 | } 18 | 19 | util.inherits(Bus, events.EventEmitter); 20 | 21 | /** 22 | * This function will find the service that exposes the event by name and 23 | * call the associated subscribe method with the arguments excluding the 24 | * first argument of this function. 25 | * @param {String} name - The name of the event 26 | */ 27 | Bus.prototype.subscribe = function(name) { 28 | var events = []; 29 | 30 | for(var i in this.node.services) { 31 | var service = this.node.services[i]; 32 | events = events.concat(service.getPublishEvents()); 33 | } 34 | 35 | for (var j = 0; j < events.length; j++) { 36 | var event = events[j]; 37 | var params = Array.prototype.slice.call(arguments).slice(1); 38 | params.unshift(this); 39 | if (name === event.name) { 40 | event.subscribe.apply(event.scope, params); 41 | } 42 | } 43 | }; 44 | 45 | /** 46 | * The inverse of the subscribe method. 47 | * @param {String} name - The name of the event 48 | */ 49 | Bus.prototype.unsubscribe = function(name) { 50 | var events = []; 51 | 52 | for(var i in this.node.services) { 53 | var service = this.node.services[i]; 54 | events = events.concat(service.getPublishEvents()); 55 | } 56 | 57 | for (var j = 0; j < events.length; j++) { 58 | var event = events[j]; 59 | var params = Array.prototype.slice.call(arguments).slice(1); 60 | params.unshift(this); 61 | if (name === event.name) { 62 | event.unsubscribe.apply(event.scope, params); 63 | } 64 | } 65 | }; 66 | 67 | /** 68 | * This function will unsubscribe all events. 69 | */ 70 | Bus.prototype.close = function() { 71 | var events = []; 72 | 73 | for(var i in this.node.services) { 74 | var service = this.node.services[i]; 75 | events = events.concat(service.getPublishEvents()); 76 | } 77 | 78 | // Unsubscribe from all events 79 | for (var j = 0; j < events.length; j++) { 80 | var event = events[j]; 81 | event.unsubscribe.call(event.scope, this); 82 | } 83 | }; 84 | 85 | module.exports = Bus; 86 | -------------------------------------------------------------------------------- /lib/cli/bchnode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Liftoff = require('liftoff'); 4 | 5 | function main(parentServicesPath, additionalServices) { 6 | 7 | var liftoff = new Liftoff({ 8 | name: 'bchnode', 9 | moduleName: 'bch-node', 10 | configName: 'bch-node', 11 | processTitle: 'bchnode' 12 | }).on('require', function (name) { 13 | console.log('Loading:', name); 14 | }).on('requireFail', function (name, err) { 15 | console.log('Unable to load:', name, err); 16 | }).on('respawn', function (flags, child) { 17 | console.log('Detected node flags:', flags); 18 | console.log('Respawned to PID:', child.pid); 19 | }); 20 | 21 | liftoff.launch({ 22 | cwd: process.cwd() 23 | }, function(env){ 24 | 25 | var node; 26 | if (env.configPath && env.modulePath) { 27 | node = require(env.modulePath); 28 | node.cli.main(); 29 | } else { 30 | node = require('../../'); 31 | node.cli.main(parentServicesPath, additionalServices); 32 | } 33 | 34 | }); 35 | 36 | } 37 | 38 | module.exports = main; 39 | -------------------------------------------------------------------------------- /lib/cli/bchnoded.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Liftoff = require('liftoff'); 4 | 5 | function main(parentServicesPath, additionalServices) { 6 | 7 | var liftoff = new Liftoff({ 8 | name: 'bchnoded', 9 | moduleName: 'bch-node', 10 | configName: 'bch-node', 11 | processTitle: 'bchnoded' 12 | }).on('require', function (name) { 13 | console.log('Loading:', name); 14 | }).on('requireFail', function (name, err) { 15 | console.log('Unable to load:', name, err); 16 | }).on('respawn', function (flags, child) { 17 | console.log('Detected node flags:', flags); 18 | console.log('Respawned to PID:', child.pid); 19 | }); 20 | 21 | liftoff.launch({ 22 | cwd: process.cwd() 23 | }, function(env){ 24 | 25 | var node; 26 | 27 | if (env.configPath && env.modulePath) { 28 | node = require(env.modulePath); 29 | node.cli.daemon(); 30 | } else { 31 | node = require('../../'); 32 | node.cli.daemon(parentServicesPath, additionalServices); 33 | } 34 | 35 | }); 36 | 37 | } 38 | 39 | module.exports = main; 40 | -------------------------------------------------------------------------------- /lib/cli/daemon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var program = require('commander'); 4 | var path = require('path'); 5 | var bchNode = require('..'); 6 | 7 | function main(servicesPath, additionalServices) { 8 | /* jshint maxstatements: 100 */ 9 | 10 | var version = bchNode.version; 11 | var start = bchNode.scaffold.start; 12 | var findConfig = bchNode.scaffold.findConfig; 13 | var defaultConfig = bchNode.scaffold.defaultConfig; 14 | 15 | program 16 | .version(version) 17 | .description('Start the current node') 18 | .option('-c, --config ', 'Specify the directory with Bch Node configuration'); 19 | 20 | program.parse(process.argv); 21 | 22 | if (program.config) { 23 | program.config = path.resolve(process.cwd(), program.config); 24 | } 25 | var configInfo = findConfig(program.config || process.cwd()); 26 | if (!configInfo) { 27 | configInfo = defaultConfig({ 28 | additionalServices: additionalServices 29 | }); 30 | } 31 | if (servicesPath) { 32 | configInfo.servicesPath = servicesPath; 33 | } 34 | start(configInfo); 35 | } 36 | 37 | module.exports = main; 38 | -------------------------------------------------------------------------------- /lib/cli/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var program = require('commander'); 4 | var path = require('path'); 5 | var bchnode = require('..'); 6 | var utils = require('../utils'); 7 | 8 | function main(servicesPath, additionalServices) { 9 | /* jshint maxstatements: 100 */ 10 | 11 | var version = bchnode.version; 12 | var create = bchnode.scaffold.create; 13 | var add = bchnode.scaffold.add; 14 | var start = bchnode.scaffold.start; 15 | var remove = bchnode.scaffold.remove; 16 | var callMethod = bchnode.scaffold.callMethod; 17 | var findConfig = bchnode.scaffold.findConfig; 18 | var defaultConfig = bchnode.scaffold.defaultConfig; 19 | 20 | program 21 | .version(version); 22 | 23 | program 24 | .command('create ') 25 | .description('Create a new node') 26 | .option('-d, --datadir ', 'Specify the bitcoin database directory') 27 | .option('-t, --testnet', 'Enable testnet as the network') 28 | .action(function(dirname, cmd){ 29 | if (cmd.datadir) { 30 | cmd.datadir = path.resolve(process.cwd(), cmd.datadir); 31 | } 32 | var opts = { 33 | cwd: process.cwd(), 34 | dirname: dirname, 35 | datadir: cmd.datadir || './data', 36 | isGlobal: false 37 | }; 38 | if (cmd.testnet) { 39 | opts.network = 'testnet'; 40 | } 41 | create(opts, function(err) { 42 | if (err) { 43 | throw err; 44 | } 45 | console.log('Successfully created node in directory: ', dirname); 46 | }); 47 | }); 48 | 49 | program 50 | .command('start') 51 | .description('Start the current node') 52 | .option('-c, --config ', 'Specify the directory with Bch Node configuration') 53 | .option('-m, --modules ', 'Specify the directory with Bch Node modules') 54 | .action(function(cmd){ 55 | if (cmd.config) { 56 | cmd.config = path.resolve(process.cwd(), cmd.config); 57 | } 58 | var configInfo = findConfig(cmd.config || process.cwd()); 59 | if (!configInfo) { 60 | configInfo = defaultConfig({ 61 | additionalServices: additionalServices 62 | }); 63 | } 64 | if (cmd.modules) { 65 | servicesPath = cmd.modules; 66 | } 67 | if (servicesPath) { 68 | configInfo.servicesPath = servicesPath; 69 | } 70 | start(configInfo); 71 | }); 72 | 73 | program 74 | .command('install ') 75 | .description('Install a service for the current node') 76 | .action(function(services){ 77 | var configInfo = findConfig(process.cwd()); 78 | if (!configInfo) { 79 | throw new Error('Could not find configuration, see `bch-node create --help`'); 80 | } 81 | var opts = { 82 | path: configInfo.path, 83 | services: services 84 | }; 85 | add(opts, function(err) { 86 | if (err) { 87 | throw err; 88 | } 89 | console.log('Successfully added services(s):', services.join(', ')); 90 | }); 91 | }).on('--help', function() { 92 | console.log(' Examples:'); 93 | console.log(); 94 | console.log(' $ bch-node add wallet-service'); 95 | console.log(' $ bch-node add explorer-api'); 96 | console.log(); 97 | }); 98 | 99 | program 100 | .command('uninstall ') 101 | .description('Uninstall a service for the current node') 102 | .action(function(services){ 103 | var configInfo = findConfig(process.cwd()); 104 | if (!configInfo) { 105 | throw new Error('Could not find configuration, see `bch-node create --help`'); 106 | } 107 | var opts = { 108 | path: configInfo.path, 109 | services: services 110 | }; 111 | remove(opts, function(err) { 112 | if (err) { 113 | throw err; 114 | } 115 | console.log('Successfully removed services(s):', services.join(', ')); 116 | }); 117 | }).on('--help', function() { 118 | console.log(' Examples:'); 119 | console.log(); 120 | console.log(' $ bch-node remove wallet-service'); 121 | console.log(' $ bch-node remove explorer-api'); 122 | console.log(); 123 | }); 124 | 125 | program 126 | .command('call [params...]') 127 | .description('Call an API method') 128 | .action(function(method, paramsArg) { 129 | var params = utils.parseParamsWithJSON(paramsArg); 130 | var configInfo = findConfig(process.cwd()); 131 | if (!configInfo) { 132 | configInfo = defaultConfig(); 133 | } 134 | var options = { 135 | protocol: 'http', 136 | host: 'localhost', 137 | port: configInfo.config.port 138 | }; 139 | callMethod(options, method, params, function(err, data) { 140 | if (err) { 141 | throw err; 142 | } 143 | console.log(JSON.stringify(data, null, 2)); 144 | }); 145 | }); 146 | 147 | program.parse(process.argv); 148 | 149 | if (process.argv.length === 2) { 150 | program.help(); 151 | } 152 | 153 | } 154 | 155 | module.exports = main; 156 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createError = require('errno').create; 4 | 5 | var BchNodeError = createError('BchNodeError'); 6 | 7 | var RPCError = createError('RPCError', BchNodeError); 8 | 9 | module.exports = { 10 | Error: BchNodeError, 11 | RPCError: RPCError 12 | }; 13 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Logger = require('./logger'); 2 | module.exports.errors = require('./errors'); 3 | module.exports.log = new Logger(); 4 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bchLib = require('@owstack/bch-lib'); 4 | var _ = bchLib.deps._; 5 | var colors = require('colors/safe'); 6 | 7 | /** 8 | * Wraps console.log with some special magic 9 | * @constructor 10 | */ 11 | function Logger(options) { 12 | if (!options) { 13 | options = {}; 14 | } 15 | this.formatting = _.isUndefined(options.formatting) ? Logger.DEFAULT_FORMATTING : options.formatting; 16 | } 17 | 18 | Logger.DEFAULT_FORMATTING = true; 19 | 20 | /** 21 | * Prints an info message 22 | * #info 23 | */ 24 | Logger.prototype.info = function() { 25 | this._log.apply(this, ['blue', 'info'].concat(Array.prototype.slice.call(arguments))); 26 | }; 27 | 28 | /** 29 | * Prints an error message 30 | * #error 31 | */ 32 | Logger.prototype.error = function() { 33 | this._log.apply(this, ['red', 'error'].concat(Array.prototype.slice.call(arguments))); 34 | }; 35 | 36 | /** 37 | * Prints an debug message 38 | * #debug 39 | */ 40 | Logger.prototype.debug = function() { 41 | this._log.apply(this, ['magenta', 'debug'].concat(Array.prototype.slice.call(arguments))); 42 | }; 43 | 44 | /** 45 | * Prints an warn message 46 | * #warn 47 | */ 48 | Logger.prototype.warn = function() { 49 | this._log.apply(this, ['yellow', 'warn'].concat(Array.prototype.slice.call(arguments))); 50 | }; 51 | 52 | /** 53 | * Proxies console.log with color and arg parsing magic 54 | * #_log 55 | */ 56 | Logger.prototype._log = function(color) { 57 | var args = Array.prototype.slice.call(arguments); 58 | args = args.slice(1); 59 | var level = args.shift(); 60 | 61 | if (this.formatting) { 62 | var date = new Date(); 63 | var typeString = colors[color].italic(level + ':'); 64 | args[0] = '[' + date.toISOString() + ']' + ' ' + typeString + ' ' + args[0]; 65 | } 66 | var fn = console[level] || console.log; 67 | fn.apply(console, args); 68 | }; 69 | 70 | module.exports = Logger; 71 | -------------------------------------------------------------------------------- /lib/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var EventEmitter = require('events').EventEmitter; 5 | var async = require('async'); 6 | var bchLib = require('@owstack/bch-lib'); 7 | var Networks = bchLib.Networks; 8 | var $ = bchLib.util.preconditions; 9 | var _ = bchLib.deps._; 10 | var index = require('./'); 11 | var log = index.log; 12 | var Bus = require('./bus'); 13 | var errors = require('./errors'); 14 | 15 | /** 16 | * A node is a hub of services, and will manage starting and stopping the services in 17 | * the correct order based the the dependency chain. The node also holds common configuration 18 | * properties that can be shared across services, such as network settings. 19 | * 20 | * The array of services should have the format: 21 | * ```js 22 | * { 23 | * name: 'bitcoind', 24 | * config: {}, // options to pass into constructor 25 | * module: ServiceConstructor 26 | * } 27 | * ``` 28 | * 29 | * @param {Object} config - The configuration of the node 30 | * @param {Array} config.formatLogs - Option to disable formatting of logs 31 | * @param {Array} config.services - The array of services 32 | * @param {Number} config.port - The HTTP port for services 33 | * @param {Boolean} config.https - Enable https 34 | * @param {Object} config.httpsOptions - Options for https 35 | * @param {String} config.httpsOptions.key - Path to key file 36 | * @param {String} config.httpsOptions.cert - Path to cert file 37 | * @param {} 38 | */ 39 | function Node(config) { 40 | /* jshint maxstatements: 20 */ 41 | if(!(this instanceof Node)) { 42 | return new Node(config); 43 | } 44 | this.configPath = config.path; 45 | this.errors = errors; 46 | this.log = log; 47 | 48 | if (!_.isUndefined(config.formatLogs)) { 49 | this.log.formatting = config.formatLogs ? true : false; 50 | } 51 | 52 | this.network = null; 53 | this.services = {}; 54 | this._unloadedServices = []; 55 | 56 | // TODO type check the arguments of config.services 57 | if (config.services) { 58 | $.checkArgument(Array.isArray(config.services)); 59 | this._unloadedServices = config.services; 60 | } 61 | this.port = config.port; 62 | this.https = config.https; 63 | this.httpsOptions = config.httpsOptions; 64 | this._setNetwork(config); 65 | } 66 | 67 | util.inherits(Node, EventEmitter); 68 | 69 | /** 70 | * Will set the this.network based on a network string. 71 | * @param {Object} config 72 | * @param {String} config.network - Possible options "testnet", "regtest" or "livenet" 73 | */ 74 | Node.prototype._setNetwork = function(config) { 75 | if (config.network === 'testnet') { 76 | this.network = Networks.get('testnet'); 77 | } else if (config.network === 'regtest') { 78 | Networks.enableRegtest(); 79 | this.network = Networks.get('regtest'); 80 | } else { 81 | this.network = Networks.defaultNetwork; 82 | } 83 | $.checkState(this.network, 'Unrecognized network'); 84 | }; 85 | 86 | /** 87 | * Will instantiate a new Bus for this node. 88 | * @returns {Bus} 89 | */ 90 | Node.prototype.openBus = function(options) { 91 | if (!options) { 92 | options = {}; 93 | } 94 | return new Bus({node: this, remoteAddress: options.remoteAddress}); 95 | }; 96 | 97 | /** 98 | * Will get an array of API method descriptions from all of the available services. 99 | * @returns {Array} 100 | */ 101 | Node.prototype.getAllAPIMethods = function() { 102 | var methods = []; 103 | for(var i in this.services) { 104 | var mod = this.services[i]; 105 | if (mod.getAPIMethods) { 106 | methods = methods.concat(mod.getAPIMethods()); 107 | } 108 | } 109 | return methods; 110 | }; 111 | 112 | /** 113 | * Will get an array of events from all of the available services. 114 | * @returns {Array} 115 | */ 116 | Node.prototype.getAllPublishEvents = function() { 117 | var events = []; 118 | for (var i in this.services) { 119 | var mod = this.services[i]; 120 | if (mod.getPublishEvents) { 121 | events = events.concat(mod.getPublishEvents()); 122 | } 123 | } 124 | return events; 125 | }; 126 | 127 | /** 128 | * Will organize services into the order that they should be started 129 | * based on the service's dependencies. 130 | * @returns {Array} 131 | */ 132 | Node.prototype.getServiceOrder = function() { 133 | 134 | var services = this._unloadedServices; 135 | 136 | // organize data for sorting 137 | var moduleNames = []; 138 | var servicesByModuleName = {}; 139 | for (var i = 0; i < services.length; i++) { 140 | var service = services[i]; 141 | moduleNames.push(service.moduleName); 142 | servicesByModuleName[service.moduleName] = service; 143 | } 144 | 145 | var stackNames = {}; 146 | var stack = []; 147 | 148 | function addToStack(moduleNames) { 149 | for(var i = 0; i < moduleNames.length; i++) { 150 | 151 | var moduleName = moduleNames[i]; 152 | var service = servicesByModuleName[moduleName]; 153 | if (!service) { 154 | log.warn('Dependency "' + moduleName + '" is not available locally. Make sure your configuration specifies a remote instance'); 155 | } else { 156 | // first add the dependencies 157 | addToStack(service.module.dependencies); 158 | 159 | // add to the stack if it hasn't been added 160 | if(!stackNames[moduleName]) { 161 | stack.push(service); 162 | stackNames[moduleName] = true; 163 | } 164 | } 165 | } 166 | } 167 | 168 | addToStack(moduleNames); 169 | 170 | return stack; 171 | }; 172 | 173 | /** 174 | * Will instantiate an instance of the service module, add it to the node 175 | * services, start the service and add available API methods to the node and 176 | * checking for any conflicts. 177 | * @param {Object} serviceInfo 178 | * @param {String} serviceInfo.name - The name of the service 179 | * @param {String} serviceInfo.moduleName - The loadable module name of the service 180 | * @param {Object} serviceInfo.module - The service module constructor 181 | * @param {Object} serviceInfo.config - Options to pass into the constructor 182 | * @param {Function} callback - Called when the service is started 183 | * @private 184 | */ 185 | Node.prototype._startService = function(serviceInfo, callback) { 186 | /* jshint maxstatements: 20 */ 187 | var self = this; 188 | 189 | log.info('Starting ' + serviceInfo.moduleName + 190 | (serviceInfo.moduleName !== serviceInfo.name ? ' as ' + serviceInfo.name : '')); 191 | 192 | var config; 193 | if (serviceInfo.config) { 194 | $.checkState(_.isObject(serviceInfo.config)); 195 | $.checkState(!serviceInfo.config.node); 196 | $.checkState(!serviceInfo.config.name); 197 | $.checkState(!serviceInfo.config.moduleName); 198 | config = serviceInfo.config; 199 | } else { 200 | config = {}; 201 | } 202 | 203 | config.node = this; 204 | config.name = serviceInfo.name; 205 | config.moduleName = serviceInfo.moduleName; 206 | var service = new serviceInfo.module(config); 207 | 208 | // include in loaded services 209 | self.services[serviceInfo.name] = service; 210 | 211 | service.start(function(err) { 212 | if (err) { 213 | return callback(err); 214 | } 215 | 216 | // add API methods 217 | if (service.getAPIMethods) { 218 | var methodData = service.getAPIMethods(); 219 | var methodNameConflicts = []; 220 | methodData.forEach(function(data) { 221 | var name = data[0]; 222 | var instance = data[1]; 223 | var method = data[2]; 224 | 225 | if (self[name]) { 226 | methodNameConflicts.push(name); 227 | } else { 228 | self[name] = function() { 229 | return method.apply(instance, arguments); 230 | }; 231 | } 232 | }); 233 | 234 | if (methodNameConflicts.length > 0) { 235 | return callback(new Error('Existing API method(s) exists: ' + methodNameConflicts.join(', '))); 236 | } 237 | } 238 | 239 | callback(); 240 | 241 | }); 242 | 243 | }; 244 | 245 | Node.prototype._logTitle = function() { 246 | if (this.configPath) { 247 | log.info('Using config:', this.configPath); 248 | log.info('Using network:', this.getNetworkName()); 249 | } 250 | }; 251 | 252 | 253 | /** 254 | * Will start all running services in the order based on the dependency chain. 255 | * @param {Function} callback - Called when all services are started 256 | */ 257 | Node.prototype.start = function(callback) { 258 | var self = this; 259 | var servicesOrder = this.getServiceOrder(); 260 | 261 | self._logTitle(); 262 | 263 | async.eachSeries( 264 | servicesOrder, 265 | function(service, next) { 266 | self._startService(service, next); 267 | }, 268 | function(err) { 269 | if (err) { 270 | return callback(err); 271 | } 272 | self.emit('ready'); 273 | callback(); 274 | } 275 | ); 276 | }; 277 | 278 | Node.prototype.getNetworkName = function() { 279 | var network = this.network.name; 280 | if (this.network.regtestEnabled) { 281 | network = 'regtest'; 282 | } 283 | return network; 284 | }; 285 | 286 | /** 287 | * Will stop all running services in the reverse order that they 288 | * were initially started. 289 | * @param {Function} callback - Called when all services are stopped 290 | */ 291 | Node.prototype.stop = function(callback) { 292 | log.info('Beginning shutdown'); 293 | var self = this; 294 | var services = this.getServiceOrder().reverse(); 295 | 296 | this.stopping = true; 297 | this.emit('stopping'); 298 | 299 | async.eachSeries( 300 | services, 301 | function(service, next) { 302 | if (self.services[service.name]) { 303 | log.info('Stopping ' + service.moduleName + 304 | (service.moduleName !== service.name ? ' running as ' + service.name : '')); 305 | self.services[service.name].stop(next); 306 | } else { 307 | log.info('Stopping ' + service.moduleName + 308 | (service.moduleName !== service.name ? ' running as ' + service.name : '') + ' (not started)'); 309 | setImmediate(next); 310 | } 311 | }, 312 | callback 313 | ); 314 | }; 315 | 316 | module.exports = Node; 317 | -------------------------------------------------------------------------------- /lib/scaffold/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var spawn = require('child_process').spawn; 7 | var bchLib = require('@owstack/bch-lib'); 8 | var utils = require('../utils'); 9 | var $ = bchLib.util.preconditions; 10 | var _ = bchLib.deps._; 11 | 12 | /** 13 | * @param {String} configFilePath - The absolute path to the configuration file 14 | * @param {String} service - The name of the service 15 | * @param {Function} done 16 | */ 17 | function addConfig(configFilePath, service, done) { 18 | $.checkState(utils.isAbsolutePath(configFilePath), 'An absolute path is expected'); 19 | fs.readFile(configFilePath, function(err, data) { 20 | if (err) { 21 | return done(err); 22 | } 23 | var config = JSON.parse(data); 24 | $.checkState( 25 | Array.isArray(config.services), 26 | 'Configuration file is expected to have a services array.' 27 | ); 28 | config.services.push(service); 29 | config.services = _.uniq(config.services); 30 | config.services.sort(function(a, b) { 31 | return a > b; 32 | }); 33 | fs.writeFile(configFilePath, JSON.stringify(config, null, 2), done); 34 | }); 35 | } 36 | 37 | /** 38 | * @param {String} configDir - The absolute configuration directory path 39 | * @param {String} service - The name of the service 40 | * @param {Function} done 41 | */ 42 | function addService(configDir, service, done) { 43 | $.checkState(utils.isAbsolutePath(configDir), 'An absolute path is expected'); 44 | var npm = spawn('npm', ['install', service, '--save'], {cwd: configDir}); 45 | 46 | npm.stdout.on('data', function(data) { 47 | process.stdout.write(data); 48 | }); 49 | 50 | npm.stderr.on('data', function(data) { 51 | process.stderr.write(data); 52 | }); 53 | 54 | npm.on('close', function(code) { 55 | if (code !== 0) { 56 | return done(new Error('There was an error installing service: ' + service)); 57 | } else { 58 | return done(); 59 | } 60 | }); 61 | } 62 | 63 | /** 64 | * @param {String} options.cwd - The current working directory 65 | * @param {String} options.dirname - The bch-node configuration directory 66 | * @param {Array} options.services - An array of strings of service names 67 | * @param {Function} done - A callback function called when finished 68 | */ 69 | function add(options, done) { 70 | $.checkArgument(_.isObject(options)); 71 | $.checkArgument(_.isFunction(done)); 72 | $.checkArgument( 73 | _.isString(options.path) && utils.isAbsolutePath(options.path), 74 | 'An absolute path is expected' 75 | ); 76 | $.checkArgument(Array.isArray(options.services)); 77 | 78 | var configPath = options.path; 79 | var services = options.services; 80 | 81 | var bchNodeConfigPath = path.resolve(configPath, 'bch-node.json'); 82 | var packagePath = path.resolve(configPath, 'package.json'); 83 | 84 | if (!fs.existsSync(bchNodeConfigPath) || !fs.existsSync(packagePath)) { 85 | return done( 86 | new Error('Directory does not have a bch-node.json and/or package.json file.') 87 | ); 88 | } 89 | 90 | var oldPackage = JSON.parse(fs.readFileSync(packagePath)); 91 | 92 | async.eachSeries( 93 | services, 94 | function(service, next) { 95 | // npm install --save 96 | addService(configPath, service, function(err) { 97 | if (err) { 98 | return next(err); 99 | } 100 | 101 | // get the name of the service from package.json 102 | var updatedPackage = JSON.parse(fs.readFileSync(packagePath)); 103 | var newDependencies = _.difference( 104 | Object.keys(updatedPackage.dependencies), 105 | Object.keys(oldPackage.dependencies) 106 | ); 107 | $.checkState(newDependencies.length === 1); 108 | oldPackage = updatedPackage; 109 | var serviceName = newDependencies[0]; 110 | 111 | // add service to bch-node.json 112 | addConfig(bchNodeConfigPath, serviceName, next); 113 | }); 114 | }, done 115 | ); 116 | } 117 | 118 | module.exports = add; 119 | -------------------------------------------------------------------------------- /lib/scaffold/call-method.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var socketClient = require('socket.io-client'); 4 | 5 | /** 6 | * Calls a remote node with a method and params 7 | * @param {Object} options 8 | * @param {String} method - The name of the method to call 9 | * @param {Array} params - An array of the params for the method 10 | * @param {Function} done - The callback function 11 | */ 12 | function callMethod(options, method, params, done) { 13 | 14 | var host = options.host; 15 | var protocol = options.protocol; 16 | var port = options.port; 17 | var url = protocol + '://' + host + ':' + port; 18 | var socketOptions = { 19 | reconnection: false, 20 | connect_timeout: 5000 21 | }; 22 | var socket = socketClient(url, socketOptions); 23 | 24 | socket.on('connect', function(){ 25 | socket.send({ 26 | method: method, 27 | params: params, 28 | }, function(response) { 29 | if (response.error) { 30 | return done(new Error(response.error.message)); 31 | } 32 | socket.close(); 33 | done(null, response.result); 34 | }); 35 | }); 36 | 37 | socket.on('connect_error', done); 38 | 39 | return socket; 40 | 41 | } 42 | 43 | module.exports = callMethod; 44 | -------------------------------------------------------------------------------- /lib/scaffold/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var spawn = require('child_process').spawn; 4 | var bchLib = require('@owstack/bch-lib'); 5 | var async = require('async'); 6 | var $ = bchLib.util.preconditions; 7 | var _ = bchLib.deps._; 8 | var path = require('path'); 9 | var packageFile = require('../../package.json'); 10 | var mkdirp = require('mkdirp'); 11 | var fs = require('fs'); 12 | var defaultBaseConfig = require('./default-base-config'); 13 | 14 | var version = '^' + packageFile.version; 15 | 16 | var BASE_PACKAGE = { 17 | description: 'A full Bitcoin node build with Bch', 18 | repository: 'https://github.com/user/project', 19 | license: 'MIT', 20 | readme: 'README.md', 21 | dependencies: { 22 | '@owstack/bch-lib': '^' + bchLib.version, 23 | '@owstack/bch-node': version 24 | } 25 | }; 26 | 27 | /** 28 | * Will create a directory and bitcoin.conf file for Bitcoin. 29 | * @param {String} dataDir - The absolute path 30 | * @param {Function} done - The callback function called when finished 31 | */ 32 | function createBitcoinDirectory(datadir, done) { 33 | mkdirp(datadir, function(err) { 34 | if (err) { 35 | throw err; 36 | } 37 | 38 | done(); 39 | 40 | // Don't create the configuration yet 41 | }); 42 | } 43 | 44 | /** 45 | * Will create a base Bch Node configuration directory and files. 46 | * @param {Object} options 47 | * @param {String} options.network - "testnet" or "livenet" 48 | * @param {String} options.datadir - The bitcoin database directory 49 | * @param {String} configDir - The absolute path 50 | * @param {Boolean} isGlobal - If the configuration depends on globally installed node services. 51 | * @param {Function} done - The callback function called when finished 52 | */ 53 | function createConfigDirectory(options, configDir, isGlobal, done) { 54 | mkdirp(configDir, function(err) { 55 | if (err) { 56 | throw err; 57 | } 58 | var configInfo = defaultBaseConfig(options); 59 | var config = configInfo.config; 60 | 61 | var configJSON = JSON.stringify(config, null, 2); 62 | var packageJSON = JSON.stringify(BASE_PACKAGE, null, 2); 63 | try { 64 | fs.writeFileSync(configDir + '/bch-node.json', configJSON); 65 | if (!isGlobal) { 66 | fs.writeFileSync(configDir + '/package.json', packageJSON); 67 | } 68 | } catch(e) { 69 | done(e); 70 | } 71 | done(); 72 | 73 | }); 74 | } 75 | 76 | /** 77 | * Will setup a directory with a Bch Node directory, configuration file, 78 | * bitcoin configuration, and will install all necessary dependencies. 79 | * 80 | * @param {Object} options 81 | * @param {String} options.cwd - The current working directory 82 | * @param {String} options.dirname - The name of the bch node configuration directory 83 | * @param {String} options.datadir - The path to the bitcoin datadir 84 | * @param {Function} done - A callback function called when finished 85 | */ 86 | function create(options, done) { 87 | /* jshint maxstatements:20 */ 88 | 89 | $.checkArgument(_.isObject(options)); 90 | $.checkArgument(_.isFunction(done)); 91 | $.checkArgument(_.isString(options.cwd)); 92 | $.checkArgument(_.isString(options.dirname)); 93 | $.checkArgument(_.isBoolean(options.isGlobal)); 94 | $.checkArgument(_.isString(options.datadir)); 95 | 96 | var cwd = options.cwd; 97 | var dirname = options.dirname; 98 | var datadir = options.datadir; 99 | var isGlobal = options.isGlobal; 100 | 101 | var absConfigDir = path.resolve(cwd, dirname); 102 | var absDataDir = path.resolve(absConfigDir, datadir); 103 | 104 | async.series([ 105 | function(next) { 106 | // Setup the the bch-node directory and configuration 107 | if (!fs.existsSync(absConfigDir)) { 108 | var createOptions = { 109 | network: options.network, 110 | datadir: datadir 111 | }; 112 | createConfigDirectory(createOptions, absConfigDir, isGlobal, next); 113 | } else { 114 | next(new Error('Directory "' + absConfigDir+ '" already exists.')); 115 | } 116 | }, 117 | function(next) { 118 | // Setup the bitcoin directory and configuration 119 | if (!fs.existsSync(absDataDir)) { 120 | createBitcoinDirectory(absDataDir, next); 121 | } else { 122 | next(); 123 | } 124 | }, 125 | function(next) { 126 | // Install all of the necessary dependencies 127 | if (!isGlobal) { 128 | var npm = spawn('npm', ['install'], {cwd: absConfigDir}); 129 | 130 | npm.stdout.on('data', function (data) { 131 | process.stdout.write(data); 132 | }); 133 | 134 | npm.stderr.on('data', function (data) { 135 | process.stderr.write(data); 136 | }); 137 | 138 | npm.on('close', function (code) { 139 | if (code !== 0) { 140 | return next(new Error('There was an error installing dependencies.')); 141 | } else { 142 | return next(); 143 | } 144 | }); 145 | 146 | } else { 147 | next(); 148 | } 149 | } 150 | ], done); 151 | 152 | } 153 | 154 | module.exports = create; 155 | -------------------------------------------------------------------------------- /lib/scaffold/default-base-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | /** 6 | * Will return the path and default bch-node configuration on environment variables 7 | * or default locations. 8 | * @param {Object} options 9 | * @param {String} options.network - "testnet" or "livenet" 10 | * @param {String} options.datadir - Absolute path to bitcoin database directory 11 | */ 12 | function getDefaultBaseConfig(options) { 13 | if (!options) { 14 | options = {}; 15 | } 16 | return { 17 | path: process.cwd(), 18 | config: { 19 | network: options.network || 'livenet', 20 | port: 3001, 21 | services: ['bitcoind', 'web'], 22 | servicesConfig: { 23 | bitcoind: { 24 | spawn: { 25 | datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'), 26 | exec: path.resolve(__dirname, '../../bin/bitcoind') 27 | } 28 | } 29 | } 30 | } 31 | }; 32 | } 33 | 34 | module.exports = getDefaultBaseConfig; 35 | -------------------------------------------------------------------------------- /lib/scaffold/default-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var mkdirp = require('mkdirp'); 5 | var fs = require('fs'); 6 | 7 | /** 8 | * Will return the path and default bch-node configuration. It will search for the 9 | * configuration file in the "~/.bch" directory, and if it doesn't exist, it will create one 10 | * based on default settings. 11 | * @param {Object} [options] 12 | * @param {Array} [options.additionalServices] - An optional array of services. 13 | */ 14 | function getDefaultConfig(options) { 15 | /* jshint maxstatements: 40 */ 16 | if (!options) { 17 | options = {}; 18 | } 19 | 20 | var defaultPath = path.resolve(process.env.HOME, './.bch'); 21 | var defaultConfigFile = path.resolve(defaultPath, './bch-node.json'); 22 | 23 | if (!fs.existsSync(defaultPath)) { 24 | mkdirp.sync(defaultPath); 25 | } 26 | 27 | var defaultServices = ['bitcoind', 'web']; 28 | if (options.additionalServices) { 29 | defaultServices = defaultServices.concat(options.additionalServices); 30 | } 31 | 32 | if (!fs.existsSync(defaultConfigFile)) { 33 | var defaultConfig = { 34 | network: 'livenet', 35 | port: 3001, 36 | services: defaultServices, 37 | servicesConfig: { 38 | bitcoind: { 39 | spawn: { 40 | datadir: path.resolve(defaultPath, './data'), 41 | exec: path.resolve(__dirname, '../../bin/bitcoind') 42 | } 43 | } 44 | } 45 | }; 46 | fs.writeFileSync(defaultConfigFile, JSON.stringify(defaultConfig, null, 2)); 47 | } 48 | 49 | var defaultDataDir = path.resolve(defaultPath, './data'); 50 | 51 | if (!fs.existsSync(defaultDataDir)) { 52 | mkdirp.sync(defaultDataDir); 53 | } 54 | 55 | var config = JSON.parse(fs.readFileSync(defaultConfigFile, 'utf-8')); 56 | 57 | return { 58 | path: defaultPath, 59 | config: config 60 | }; 61 | 62 | } 63 | 64 | module.exports = getDefaultConfig; 65 | -------------------------------------------------------------------------------- /lib/scaffold/find-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bchLib = require('@owstack/bch-lib'); 4 | var $ = bchLib.util.preconditions; 5 | var _ = bchLib.deps._; 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | var utils = require('../utils'); 9 | 10 | /** 11 | * Will return the path and bch-node configuration 12 | * @param {String} cwd - The absolute path to the current working directory 13 | */ 14 | function findConfig(cwd) { 15 | $.checkArgument(_.isString(cwd), 'Argument should be a string'); 16 | $.checkArgument(utils.isAbsolutePath(cwd), 'Argument should be an absolute path'); 17 | var directory = String(cwd); 18 | while (!fs.existsSync(path.resolve(directory, 'bch-node.json'))) { 19 | directory = path.resolve(directory, '../'); 20 | if (directory === '/') { 21 | return false; 22 | } 23 | } 24 | return { 25 | path: directory, 26 | config: require(path.resolve(directory, 'bch-node.json')) 27 | }; 28 | } 29 | 30 | module.exports = findConfig; 31 | -------------------------------------------------------------------------------- /lib/scaffold/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var spawn = require('child_process').spawn; 7 | var bchLib = require('@owstack/bch-lib'); 8 | var $ = bchLib.util.preconditions; 9 | var _ = bchLib.deps._; 10 | var utils = require('../utils'); 11 | 12 | /** 13 | * Will remove a service from bch-node.json 14 | * @param {String} configFilePath - The absolute path to the configuration file 15 | * @param {String} service - The name of the module 16 | * @param {Function} done 17 | */ 18 | function removeConfig(configFilePath, service, done) { 19 | $.checkArgument(utils.isAbsolutePath(configFilePath), 'An absolute path is expected'); 20 | fs.readFile(configFilePath, function(err, data) { 21 | if (err) { 22 | return done(err); 23 | } 24 | var config = JSON.parse(data); 25 | $.checkState( 26 | Array.isArray(config.services), 27 | 'Configuration file is expected to have a services array.' 28 | ); 29 | // remove the service from the configuration 30 | for (var i = 0; i < config.services.length; i++) { 31 | if (config.services[i] === service) { 32 | config.services.splice(i, 1); 33 | } 34 | } 35 | config.services = _.uniq(config.services); 36 | config.services.sort(function(a, b) { 37 | return a > b; 38 | }); 39 | fs.writeFile(configFilePath, JSON.stringify(config, null, 2), done); 40 | }); 41 | } 42 | 43 | /** 44 | * Will uninstall a Node.js service and remove from package.json. 45 | * @param {String} configDir - The absolute configuration directory path 46 | * @param {String} service - The name of the service 47 | * @param {Function} done 48 | */ 49 | function uninstallService(configDir, service, done) { 50 | $.checkArgument(utils.isAbsolutePath(configDir), 'An absolute path is expected'); 51 | $.checkArgument(_.isString(service), 'A string is expected for the service argument'); 52 | 53 | var child = spawn('npm', ['uninstall', service, '--save'], {cwd: configDir}); 54 | 55 | child.stdout.on('data', function(data) { 56 | process.stdout.write(data); 57 | }); 58 | 59 | child.stderr.on('data', function(data) { 60 | process.stderr.write(data); 61 | }); 62 | 63 | child.on('close', function(code) { 64 | if (code !== 0) { 65 | return done(new Error('There was an error uninstalling service(s): ' + service)); 66 | } else { 67 | return done(); 68 | } 69 | }); 70 | } 71 | 72 | /** 73 | * Will remove a Node.js service if it is installed. 74 | * @param {String} configDir - The absolute configuration directory path 75 | * @param {String} service - The name of the service 76 | * @param {Function} done 77 | */ 78 | function removeService(configDir, service, done) { 79 | $.checkArgument(utils.isAbsolutePath(configDir), 'An absolute path is expected'); 80 | $.checkArgument(_.isString(service), 'A string is expected for the service argument'); 81 | uninstallService(configDir, service, done); 82 | } 83 | 84 | /** 85 | * Will remove the Node.js service and from the bch-node configuration. 86 | * @param {String} options.cwd - The current working directory 87 | * @param {String} options.dirname - The bch-node configuration directory 88 | * @param {Array} options.services - An array of strings of service names 89 | * @param {Function} done - A callback function called when finished 90 | */ 91 | function remove(options, done) { 92 | $.checkArgument(_.isObject(options)); 93 | $.checkArgument(_.isFunction(done)); 94 | $.checkArgument( 95 | _.isString(options.path) && utils.isAbsolutePath(options.path), 96 | 'An absolute path is expected' 97 | ); 98 | $.checkArgument(Array.isArray(options.services)); 99 | 100 | var configPath = options.path; 101 | var services = options.services; 102 | 103 | var bchNodeConfigPath = path.resolve(configPath, 'bch-node.json'); 104 | var packagePath = path.resolve(configPath, 'package.json'); 105 | 106 | if (!fs.existsSync(bchNodeConfigPath) || !fs.existsSync(packagePath)) { 107 | return done( 108 | new Error('Directory does not have a bch-node.json and/or package.json file.') 109 | ); 110 | } 111 | 112 | async.eachSeries( 113 | services, 114 | function(service, next) { 115 | // if the service is installed remove it 116 | removeService(configPath, service, function(err) { 117 | if (err) { 118 | return next(err); 119 | } 120 | // remove service to bch-node.json 121 | removeConfig(bchNodeConfigPath, service, next); 122 | }); 123 | }, done 124 | ); 125 | } 126 | 127 | module.exports = remove; 128 | -------------------------------------------------------------------------------- /lib/scaffold/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var BchNode = require('../node'); 5 | var index = require('../'); 6 | var bchLib = require('@owstack/bch-lib'); 7 | var _ = bchLib.deps._; 8 | var log = index.log; 9 | var shuttingDown = false; 10 | 11 | log.debug = function() {}; 12 | 13 | /** 14 | * Checks for configuration options from version 2. This includes an "address" and 15 | * "db" service, or having "datadir" at the root of the config. 16 | */ 17 | function checkConfigVersion2(fullConfig) { 18 | var datadirUndefined = _.isUndefined(fullConfig.datadir); 19 | var addressDefined = (fullConfig.services.indexOf('address') >= 0); 20 | var dbDefined = (fullConfig.services.indexOf('db') >= 0); 21 | 22 | if (!datadirUndefined || addressDefined || dbDefined) { 23 | 24 | console.warn('\nConfiguration file is not compatible with this version. \n' + 25 | 'A reindex for bitcoind is necessary for this upgrade with the "reindex=1" bitcoin.conf option. \n' + 26 | 'There are changes necessary in both bitcoin.conf and bch-node.json. \n\n' + 27 | 'To upgrade please see the details below and documentation at: \n' + 28 | 'https://github.com/owstack/bch-node/blob/bitcoind/docs/upgrade.md \n'); 29 | 30 | if (!datadirUndefined) { 31 | console.warn('Please remove "datadir" and add it to the config at ' + fullConfig.path + ' with:'); 32 | var missingConfig = { 33 | servicesConfig: { 34 | bitcoind: { 35 | spawn: { 36 | datadir: fullConfig.datadir, 37 | exec: path.resolve(__dirname, '../../bin/bitcoind') 38 | } 39 | } 40 | } 41 | }; 42 | console.warn(JSON.stringify(missingConfig, null, 2) + '\n'); 43 | } 44 | 45 | if (addressDefined || dbDefined) { 46 | console.warn('Please remove "address" and/or "db" from "services" in: ' + fullConfig.path + '\n'); 47 | } 48 | 49 | return true; 50 | } 51 | 52 | return false; 53 | } 54 | 55 | /** 56 | * This function will instantiate and start a Node, requiring the necessary service 57 | * modules, and registering event handlers. 58 | * @param {Object} options 59 | * @param {Object} options.servicesPath - The path to the location of service modules 60 | * @param {String} options.path - The absolute path of the configuration file 61 | * @param {Object} options.config - The parsed bch-node.json configuration file 62 | * @param {Array} options.config.services - An array of services names. 63 | * @param {Object} options.config.servicesConfig - Parameters to pass to each service 64 | * @param {String} options.config.network - 'livenet', 'testnet' or 'regtest 65 | * @param {Number} options.config.port - The port to use for the web service 66 | */ 67 | function start(options) { 68 | /* jshint maxstatements: 20 */ 69 | 70 | var fullConfig = _.clone(options.config); 71 | 72 | var servicesPath; 73 | if (options.servicesPath) { 74 | servicesPath = options.servicesPath; // services are in a different directory than the config 75 | } else { 76 | servicesPath = options.path; // defaults to the same directory 77 | } 78 | 79 | fullConfig.path = path.resolve(options.path, './bch-node.json'); 80 | 81 | if (checkConfigVersion2(fullConfig)) { 82 | process.exit(1); 83 | } 84 | 85 | fullConfig.services = start.setupServices(require, servicesPath, options.config); 86 | 87 | var node = new BchNode(fullConfig); 88 | 89 | // setup handlers for uncaught exceptions and ctrl+c 90 | start.registerExitHandlers(process, node); 91 | 92 | node.on('ready', function() { 93 | log.info('Bch Node ready'); 94 | }); 95 | 96 | node.on('error', function(err) { 97 | log.error(err); 98 | }); 99 | 100 | node.start(function(err) { 101 | if(err) { 102 | log.error('Failed to start services'); 103 | if (err.stack) { 104 | log.error(err.stack); 105 | } 106 | start.cleanShutdown(process, node); 107 | } 108 | }); 109 | 110 | return node; 111 | 112 | } 113 | 114 | /** 115 | * Checks a service for the expected methods 116 | * @param {Object} service 117 | */ 118 | function checkService(service) { 119 | // check that the service supports expected methods 120 | if (!service.module.prototype || 121 | !service.module.dependencies || 122 | !service.module.prototype.start || 123 | !service.module.prototype.stop) { 124 | throw new Error( 125 | 'Could not load service ' + service.name + 126 | (service.moduleName !== service.name ? ' using module name ' + service.moduleName : '') + 127 | ' as it does not support necessary methods and properties.' 128 | ); 129 | } 130 | } 131 | 132 | /** 133 | * Will require a module from local services directory first 134 | * and then from available node_modules 135 | * @param {Function} req 136 | * @param {Object} service 137 | */ 138 | function loadModule(req, service) { 139 | try { 140 | // first try in the built-in bch-node services directory 141 | service.module = req(path.resolve(__dirname, '../services/' + service.moduleName)); 142 | } catch(e) { 143 | 144 | // check if the package.json specifies a specific file to use 145 | var servicePackage = req(service.moduleName + '/package.json'); 146 | var serviceModule = service.moduleName; 147 | 148 | // The package.json should not define both bchNode and owsNode 149 | if (servicePackage.bchNode) { 150 | serviceModule = service.moduleName + '/' + servicePackage.bchNode; 151 | } else if (servicePackage.owsNode) { 152 | serviceModule = service.moduleName + '/' + servicePackage.owsNode; 153 | } 154 | service.module = req(serviceModule); 155 | } 156 | } 157 | 158 | /** 159 | * This function will loop over the configuration for services and require the 160 | * specified modules, and assemble an array in this format: 161 | * [ 162 | * { 163 | * name: 'bitcoind', 164 | * config: {}, 165 | * module: BitcoinService 166 | * } 167 | * ] 168 | * @param {Function} req - The require function to use 169 | * @param {Array} servicesPath - The local path (for requiring services) 170 | * @param {Object} config 171 | * @param {Array} config.services - An array of strings of service names. 172 | * @returns {Array} 173 | */ 174 | function setupServices(req, servicesPath, config) { 175 | module.paths.push(path.resolve(servicesPath, './node_modules')); 176 | 177 | var services = []; 178 | if (config.services) { 179 | for (var i = 0; i < config.services.length; i++) { 180 | var service = {}; 181 | service.name = config.services[i]; 182 | 183 | var hasConfig = config.servicesConfig && config.servicesConfig[service.name]; 184 | service.config = hasConfig ? config.servicesConfig[service.name] : {}; 185 | service.moduleName = service.config.module || service.name; 186 | 187 | loadModule(req, service); 188 | checkService(service); 189 | 190 | services.push(service); 191 | } 192 | } 193 | return services; 194 | } 195 | 196 | /** 197 | * Will shutdown a node and then the process 198 | * @param {Object} _process - The Node.js process object 199 | * @param {Node} node - The Bch Node instance 200 | */ 201 | function cleanShutdown(_process, node) { 202 | node.stop(function(err) { 203 | if(err) { 204 | log.error('Failed to stop services: ' + err); 205 | return _process.exit(1); 206 | } 207 | log.info('Halted'); 208 | _process.exit(0); 209 | }); 210 | } 211 | 212 | /** 213 | * Will handle all the shutdown tasks that need to take place to ensure a safe exit 214 | * @param {Object} options 215 | * @param {String} options.sigint - The signal given was a SIGINT 216 | * @param {Array} options.exit - The signal given was an uncaughtException 217 | * @param {Object} _process - The Node.js process 218 | * @param {Node} node 219 | * @param {Error} error 220 | */ 221 | function exitHandler(options, _process, node, err) { 222 | if (err) { 223 | log.error('uncaught exception:', err); 224 | if(err.stack) { 225 | log.error(err.stack); 226 | } 227 | node.stop(function(err) { 228 | if(err) { 229 | log.error('Failed to stop services: ' + err); 230 | } 231 | _process.exit(-1); 232 | }); 233 | } 234 | if (options.sigint) { 235 | if (!shuttingDown) { 236 | shuttingDown = true; 237 | start.cleanShutdown(_process, node); 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * Will register event handlers to stop the node for `process` events 244 | * `uncaughtException` and `SIGINT`. 245 | * @param {Object} _process - The Node.js process 246 | * @param {Node} node 247 | */ 248 | function registerExitHandlers(_process, node) { 249 | //catches uncaught exceptions 250 | _process.on('uncaughtException', exitHandler.bind(null, {exit:true}, _process, node)); 251 | 252 | //catches ctrl+c event 253 | _process.on('SIGINT', exitHandler.bind(null, {sigint:true}, _process, node)); 254 | } 255 | 256 | module.exports = start; 257 | module.exports.registerExitHandlers = registerExitHandlers; 258 | module.exports.exitHandler = exitHandler; 259 | module.exports.setupServices = setupServices; 260 | module.exports.cleanShutdown = cleanShutdown; 261 | module.exports.checkConfigVersion2 = checkConfigVersion2; 262 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var EventEmitter = require('events').EventEmitter; 5 | 6 | var Service = function(options) { 7 | EventEmitter.call(this); 8 | 9 | this.node = options.node; 10 | this.name = options.name; 11 | }; 12 | 13 | util.inherits(Service, EventEmitter); 14 | 15 | /** 16 | * Describes the dependencies that should be loaded before this service. 17 | */ 18 | Service.dependencies = []; 19 | 20 | /** 21 | * blockHandler 22 | * @param {Block} block - the block being added or removed from the chain 23 | * @param {Boolean} add - whether the block is being added or removed 24 | * @param {Function} callback - call with the leveldb database operations to perform 25 | */ 26 | Service.prototype.blockHandler = function(block, add, callback) { 27 | // implement in the child class 28 | setImmediate(function() { 29 | callback(null, []); 30 | }); 31 | }; 32 | 33 | /** 34 | * the bus events available for subscription 35 | * @return {Array} an array of event info 36 | */ 37 | Service.prototype.getPublishEvents = function() { 38 | // Example: 39 | // return [ 40 | // ['eventname', this, this.subscribeEvent, this.unsubscribeEvent], 41 | // ]; 42 | return []; 43 | }; 44 | 45 | /** 46 | * the API methods to expose 47 | * @return {Array} return array of methods 48 | */ 49 | Service.prototype.getAPIMethods = function() { 50 | // Example: 51 | // return [ 52 | // ['getData', this, this.getData, 1] 53 | // ]; 54 | 55 | return []; 56 | }; 57 | 58 | // Example: 59 | // Service.prototype.getData = function(arg1, callback) { 60 | // 61 | // }; 62 | 63 | /** 64 | * Function which is called when module is first initialized 65 | */ 66 | Service.prototype.start = function(done) { 67 | setImmediate(done); 68 | }; 69 | 70 | /** 71 | * Function to be called when bch-node is stopped 72 | */ 73 | Service.prototype.stop = function(done) { 74 | setImmediate(done); 75 | }; 76 | 77 | /** 78 | * Setup express routes 79 | * @param {Express} app 80 | */ 81 | Service.prototype.setupRoutes = function() { 82 | // Setup express routes here 83 | }; 84 | 85 | Service.prototype.getRoutePrefix = function() { 86 | return this.name; 87 | }; 88 | 89 | module.exports = Service; 90 | -------------------------------------------------------------------------------- /lib/services/web.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var http = require('http'); 5 | var https = require('https'); 6 | var express = require('express'); 7 | var bodyParser = require('body-parser'); 8 | var socketio = require('socket.io'); 9 | var inherits = require('util').inherits; 10 | 11 | var BaseService = require('../service'); 12 | var bchLib = require('@owstack/bch-lib'); 13 | var _ = bchLib.deps._; 14 | var index = require('../'); 15 | var log = index.log; 16 | 17 | 18 | /** 19 | * This service represents a hub for combining several services over a single HTTP port. Services 20 | * can extend routes by implementing the methods `getRoutePrefix` and `setupRoutes`. Additionally 21 | * events that are exposed via the `getPublishEvents` and API methods exposed via `getAPIMethods` 22 | * will be available over a socket.io connection. 23 | * 24 | * @param {Object} options 25 | * @param {Node} options.node - A reference to the node 26 | * @param {Boolean} options.https - Enable https, will default to node.https settings. 27 | * @param {Object} options.httpsOptions - Options passed into https.createServer, defaults to node settings. 28 | * @param {String} options.httpsOptions.key - Path to key file 29 | * @param {String} options.httpsOptions.cert - Path to cert file 30 | * @param {Boolean} options.enableSocketRPC - Option to enable/disable websocket RPC handling 31 | * @param {Number} options.port - The port for the service, defaults to node settings. 32 | */ 33 | var WebService = function(options) { 34 | var self = this; 35 | this.node = options.node; 36 | this.https = options.https || this.node.https; 37 | this.httpsOptions = options.httpsOptions || this.node.httpsOptions; 38 | this.port = options.port || this.node.port || 3456; 39 | 40 | // set the maximum size of json payload, defaults to express default 41 | // see: https://github.com/expressjs/body-parser#limit 42 | this.jsonRequestLimit = options.jsonRequestLimit || '100kb'; 43 | 44 | this.enableSocketRPC = _.isUndefined(options.enableSocketRPC) ? 45 | WebService.DEFAULT_SOCKET_RPC : options.enableSocketRPC; 46 | 47 | this.node.on('ready', function() { 48 | self.eventNames = self.getEventNames(); 49 | self.setupAllRoutes(); 50 | self.server.listen(self.port); 51 | self.createMethodsMap(); 52 | }); 53 | }; 54 | 55 | inherits(WebService, BaseService); 56 | 57 | WebService.dependencies = []; 58 | WebService.DEFAULT_SOCKET_RPC = true; 59 | 60 | /** 61 | * Called by Node to start the service 62 | * @param {Function} callback 63 | */ 64 | WebService.prototype.start = function(callback) { 65 | this.app = express(); 66 | this.app.use(bodyParser.json({limit: this.jsonRequestLimit})); 67 | 68 | if(this.https) { 69 | this.transformHttpsOptions(); 70 | this.server = https.createServer(this.httpsOptions, this.app); 71 | } else { 72 | this.server = http.createServer(this.app); 73 | } 74 | 75 | this.io = socketio.listen(this.server); 76 | this.io.on('connection', this.socketHandler.bind(this)); 77 | 78 | setImmediate(callback); 79 | }; 80 | 81 | /** 82 | * Called by Node. stop the service 83 | * @param {Function} callback 84 | */ 85 | WebService.prototype.stop = function(callback) { 86 | var self = this; 87 | 88 | setImmediate(function() { 89 | if(self.server) { 90 | self.server.close(); 91 | } 92 | callback(); 93 | }); 94 | }; 95 | 96 | /** 97 | * This function will iterate over all of the available services gathering 98 | * all of the exposed HTTP routes. 99 | */ 100 | WebService.prototype.setupAllRoutes = function() { 101 | for(var key in this.node.services) { 102 | var subApp = new express(); 103 | var service = this.node.services[key]; 104 | 105 | if(service.getRoutePrefix && service.setupRoutes) { 106 | this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp); 107 | this.node.services[key].setupRoutes(subApp, express); 108 | } else { 109 | log.debug('No routes defined for: ' + key); 110 | } 111 | } 112 | }; 113 | 114 | /** 115 | * This function will construct an API methods map of all of the 116 | * available methods that can be called from enable services. 117 | */ 118 | WebService.prototype.createMethodsMap = function() { 119 | var self = this; 120 | var methods = this.node.getAllAPIMethods(); 121 | this.methodsMap = {}; 122 | 123 | methods.forEach(function(data) { 124 | var name = data[0]; 125 | var instance = data[1]; 126 | var method = data[2]; 127 | var args = data[3]; 128 | self.methodsMap[name] = { 129 | fn: function() { 130 | return method.apply(instance, arguments); 131 | }, 132 | args: args 133 | }; 134 | }); 135 | }; 136 | 137 | /** 138 | * This function will gather all of the available events exposed from 139 | * the enabled services. 140 | */ 141 | WebService.prototype.getEventNames = function() { 142 | var events = this.node.getAllPublishEvents(); 143 | var eventNames = []; 144 | 145 | function addEventName(name) { 146 | if(eventNames.indexOf(name) > -1) { 147 | throw new Error('Duplicate event ' + name); 148 | } 149 | eventNames.push(name); 150 | } 151 | 152 | events.forEach(function(event) { 153 | addEventName(event.name); 154 | 155 | if(event.extraEvents) { 156 | event.extraEvents.forEach(function(name) { 157 | addEventName(name); 158 | }); 159 | } 160 | }); 161 | 162 | return eventNames; 163 | }; 164 | 165 | WebService.prototype._getRemoteAddress = function(socket) { 166 | return socket.client.request.headers['cf-connecting-ip'] || socket.conn.remoteAddress; 167 | }; 168 | 169 | /** 170 | * This function is responsible for managing a socket.io connection, including 171 | * instantiating a new Bus, subscribing/unsubscribing and handling RPC commands. 172 | * @param {Socket} socket - A socket.io socket instance 173 | */ 174 | WebService.prototype.socketHandler = function(socket) { 175 | var self = this; 176 | var remoteAddress = self._getRemoteAddress(socket); 177 | var bus = this.node.openBus({remoteAddress: remoteAddress}); 178 | 179 | if (this.enableSocketRPC) { 180 | socket.on('message', this.socketMessageHandler.bind(this)); 181 | } 182 | 183 | socket.on('subscribe', function(name, params) { 184 | log.info(remoteAddress, 'web socket subscribe:', name); 185 | bus.subscribe(name, params); 186 | }); 187 | 188 | socket.on('unsubscribe', function(name, params) { 189 | log.info(remoteAddress, 'web socket unsubscribe:', name); 190 | bus.unsubscribe(name, params); 191 | }); 192 | 193 | this.eventNames.forEach(function(eventName) { 194 | bus.on(eventName, function() { 195 | if(socket.connected) { 196 | var results = []; 197 | 198 | for(var i = 0; i < arguments.length; i++) { 199 | results.push(arguments[i]); 200 | } 201 | 202 | var params = [eventName].concat(results); 203 | socket.emit.apply(socket, params); 204 | } 205 | }); 206 | }); 207 | 208 | socket.on('disconnect', function() { 209 | log.info(remoteAddress, 'web socket disconnect'); 210 | bus.close(); 211 | }); 212 | }; 213 | 214 | /** 215 | * This method will handle incoming RPC messages to a socket.io connection, 216 | * call the appropriate method, and respond with the result. 217 | * @param {Object} message - The socket.io "message" object 218 | * @param {Function} socketCallback 219 | */ 220 | WebService.prototype.socketMessageHandler = function(message, socketCallback) { 221 | if (this.methodsMap[message.method]) { 222 | var params = message.params; 223 | 224 | if(!params || !params.length) { 225 | params = []; 226 | } 227 | 228 | if(params.length !== this.methodsMap[message.method].args) { 229 | return socketCallback({ 230 | error: { 231 | message: 'Expected ' + this.methodsMap[message.method].args + ' parameter(s)' 232 | } 233 | }); 234 | } 235 | 236 | var callback = function(err, result) { 237 | var response = {}; 238 | if(err) { 239 | response.error = { 240 | message: err.toString() 241 | }; 242 | } 243 | 244 | if(result) { 245 | response.result = result; 246 | } 247 | 248 | socketCallback(response); 249 | }; 250 | 251 | params = params.concat(callback); 252 | this.methodsMap[message.method].fn.apply(this, params); 253 | } else { 254 | socketCallback({ 255 | error: { 256 | message: 'Method Not Found' 257 | } 258 | }); 259 | } 260 | }; 261 | 262 | /** 263 | * This method will read `key` and `cert` from disk based on `httpsOptions` and 264 | * replace the options with the files. 265 | */ 266 | WebService.prototype.transformHttpsOptions = function() { 267 | if(!this.httpsOptions || !this.httpsOptions.key || !this.httpsOptions.cert) { 268 | throw new Error('Missing https options'); 269 | } 270 | 271 | this.httpsOptions = { 272 | key: fs.readFileSync(this.httpsOptions.key), 273 | cert: fs.readFileSync(this.httpsOptions.cert) 274 | }; 275 | }; 276 | 277 | module.exports = WebService; 278 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1 4 | 5 | var utils = {}; 6 | utils.isHash = function isHash(value) { 7 | return typeof value === 'string' && value.length === 64 && /^[0-9a-fA-F]+$/.test(value); 8 | }; 9 | 10 | utils.isSafeNatural = function isSafeNatural(value) { 11 | return typeof value === 'number' && 12 | isFinite(value) && 13 | Math.floor(value) === value && 14 | value >= 0 && 15 | value <= MAX_SAFE_INTEGER; 16 | }; 17 | 18 | utils.startAtZero = function startAtZero(obj, key) { 19 | if (!obj.hasOwnProperty(key)) { 20 | obj[key] = 0; 21 | } 22 | }; 23 | 24 | utils.isAbsolutePath = require('path').isAbsolute; 25 | if (!utils.isAbsolutePath) { 26 | utils.isAbsolutePath = require('path-is-absolute'); 27 | } 28 | 29 | utils.parseParamsWithJSON = function parseParamsWithJSON(paramsArg) { 30 | var params = paramsArg.map(function(paramArg) { 31 | var param; 32 | try { 33 | param = JSON.parse(paramArg); 34 | } catch(err) { 35 | param = paramArg; 36 | } 37 | return param; 38 | }); 39 | return params; 40 | }; 41 | 42 | module.exports = utils; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@owstack/bch-node", 3 | "description": "Bitcoin Cash full node with extended capabilities.", 4 | "version": "0.1.0", 5 | "main": "./index.js", 6 | "repository": "git://github.com/owstack/bch-node.git", 7 | "homepage": "https://github.com/owstack/bch-node", 8 | "bugs": { 9 | "url": "https://github.com/owstack/bch-node/issues" 10 | }, 11 | "bin": { 12 | "bchnode": "./bin/bchnode" 13 | }, 14 | "scripts": { 15 | "test": "mocha -R spec --recursive", 16 | "regtest": "./scripts/regtest", 17 | "jshint": "jshint --reporter=node_modules/jshint-stylish ./lib", 18 | "coverage": "istanbul cover _mocha -- --recursive", 19 | "coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- --recursive -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 20 | }, 21 | "tags": [ 22 | "bitcoin", 23 | "bitcoind" 24 | ], 25 | "dependencies": { 26 | "@owstack/bch-lib": "^0.1.0", 27 | "@owstack/bitcoind-rpc": "^0.0.1", 28 | "async": "^2.5.0", 29 | "body-parser": "^1.13.3", 30 | "colors": "^1.1.2", 31 | "commander": "^2.8.1", 32 | "errno": "^0.1.4", 33 | "express": "^4.13.3", 34 | "liftoff": "^2.2.0", 35 | "lru-cache": "^4.0.1", 36 | "mkdirp": "0.5.1", 37 | "path-is-absolute": "^1.0.0", 38 | "semver": "^5.0.1", 39 | "socket.io": "^2.0.3", 40 | "socket.io-client": "^2.0.3", 41 | "zeromq": "^4.2.0" 42 | }, 43 | "optionalDependencies": { 44 | "bufferutil": "~3.0.2", 45 | "utf-8-validate": "~3.0.3" 46 | }, 47 | "devDependencies": { 48 | "@owstack/bch-p2p": "^0.0.6", 49 | "benchmark": "2.1.4", 50 | "chai": "^4.1.2", 51 | "coveralls": "^3.0.0", 52 | "istanbul": "^0.4.3", 53 | "jshint": "^2.9.2", 54 | "jshint-stylish": "^2.1.0", 55 | "mocha": "^4.0.0", 56 | "proxyquire": "^1.3.1", 57 | "rimraf": "^2.4.2", 58 | "sinon": "^4.0.0" 59 | }, 60 | "license": "MIT" 61 | } 62 | -------------------------------------------------------------------------------- /regtest/cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var async = require('async'); 5 | var spawn = require('child_process').spawn; 6 | 7 | var BitcoinRPC = require('@owstack/bitcoind-rpc'); 8 | var rimraf = require('rimraf'); 9 | var bchLib = require('@owstack/bch-lib'); 10 | var chai = require('chai'); 11 | var should = chai.should(); 12 | 13 | var index = require('..'); 14 | var log = index.log; 15 | log.debug = function() {}; 16 | var BchNode = index.Node; 17 | var BitcoinService = index.services.Bitcoin; 18 | 19 | describe('Bitcoin Cluster', function() { 20 | var node; 21 | var daemons = []; 22 | var execPath = path.resolve(__dirname, '../bin/bitcoind'); 23 | var nodesConf = [ 24 | { 25 | datadir: path.resolve(__dirname, './data/node1'), 26 | conf: path.resolve(__dirname, './data/node1/bitcoin.conf'), 27 | rpcuser: 'bitcoin', 28 | rpcpassword: 'local321', 29 | rpcport: 30521, 30 | zmqpubrawtx: 'tcp://127.0.0.1:30611', 31 | zmqpubhashblock: 'tcp://127.0.0.1:30611' 32 | }, 33 | { 34 | datadir: path.resolve(__dirname, './data/node2'), 35 | conf: path.resolve(__dirname, './data/node2/bitcoin.conf'), 36 | rpcuser: 'bitcoin', 37 | rpcpassword: 'local321', 38 | rpcport: 30522, 39 | zmqpubrawtx: 'tcp://127.0.0.1:30622', 40 | zmqpubhashblock: 'tcp://127.0.0.1:30622' 41 | }, 42 | { 43 | datadir: path.resolve(__dirname, './data/node3'), 44 | conf: path.resolve(__dirname, './data/node3/bitcoin.conf'), 45 | rpcuser: 'bitcoin', 46 | rpcpassword: 'local321', 47 | rpcport: 30523, 48 | zmqpubrawtx: 'tcp://127.0.0.1:30633', 49 | zmqpubhashblock: 'tcp://127.0.0.1:30633' 50 | } 51 | ]; 52 | 53 | before(function(done) { 54 | log.info('Starting 3 bitcoind daemons'); 55 | this.timeout(60000); 56 | async.each(nodesConf, function(nodeConf, next) { 57 | var opts = [ 58 | '--regtest', 59 | '--datadir=' + nodeConf.datadir, 60 | '--conf=' + nodeConf.conf 61 | ]; 62 | 63 | rimraf(path.resolve(nodeConf.datadir, './regtest'), function(err) { 64 | if (err) { 65 | return done(err); 66 | } 67 | 68 | var process = spawn(execPath, opts, {stdio: 'inherit'}); 69 | 70 | var client = new BitcoinRPC({ 71 | protocol: 'http', 72 | host: '127.0.0.1', 73 | port: nodeConf.rpcport, 74 | user: nodeConf.rpcuser, 75 | pass: nodeConf.rpcpassword 76 | }); 77 | 78 | daemons.push(process); 79 | 80 | async.retry({times: 10, interval: 5000}, function(ready) { 81 | client.getInfo(ready); 82 | }, next); 83 | 84 | }); 85 | 86 | }, done); 87 | }); 88 | 89 | after(function(done) { 90 | this.timeout(10000); 91 | setTimeout(function() { 92 | async.each(daemons, function(process, next) { 93 | process.once('exit', next); 94 | process.kill('SIGINT'); 95 | }, done); 96 | }, 1000); 97 | }); 98 | 99 | it('step 1: will connect to three bitcoind daemons', function(done) { 100 | this.timeout(20000); 101 | var configuration = { 102 | network: 'regtest', 103 | services: [ 104 | { 105 | name: 'bitcoind', 106 | module: BitcoinService, 107 | config: { 108 | connect: [ 109 | { 110 | rpchost: '127.0.0.1', 111 | rpcport: 30521, 112 | rpcuser: 'bitcoin', 113 | rpcpassword: 'local321', 114 | zmqpubrawtx: 'tcp://127.0.0.1:30611' 115 | }, 116 | { 117 | rpchost: '127.0.0.1', 118 | rpcport: 30522, 119 | rpcuser: 'bitcoin', 120 | rpcpassword: 'local321', 121 | zmqpubrawtx: 'tcp://127.0.0.1:30622' 122 | }, 123 | { 124 | rpchost: '127.0.0.1', 125 | rpcport: 30523, 126 | rpcuser: 'bitcoin', 127 | rpcpassword: 'local321', 128 | zmqpubrawtx: 'tcp://127.0.0.1:30633' 129 | } 130 | ] 131 | } 132 | } 133 | ] 134 | }; 135 | 136 | var regtest = bchLib.Networks.get('regtest'); 137 | should.exist(regtest); 138 | 139 | node = new BchNode(configuration); 140 | 141 | node.on('error', function(err) { 142 | log.error(err); 143 | }); 144 | 145 | node.on('ready', function() { 146 | done(); 147 | }); 148 | 149 | node.start(function(err) { 150 | if (err) { 151 | return done(err); 152 | } 153 | }); 154 | 155 | }); 156 | 157 | it('step 2: receive block events', function(done) { 158 | this.timeout(10000); 159 | node.services.bitcoind.once('tip', function(height) { 160 | height.should.equal(1); 161 | done(); 162 | }); 163 | node.generateBlock(1, function(err, hashes) { 164 | if (err) { 165 | return done(err); 166 | } 167 | should.exist(hashes); 168 | }); 169 | }); 170 | 171 | it('step 3: get blocks', function(done) { 172 | async.times(3, function(n, next) { 173 | node.getBlock(1, function(err, block) { 174 | if (err) { 175 | return next(err); 176 | } 177 | should.exist(block); 178 | next(); 179 | }); 180 | }, done); 181 | }); 182 | 183 | }); 184 | -------------------------------------------------------------------------------- /regtest/data/.gitignore: -------------------------------------------------------------------------------- 1 | .lock 2 | blocks 3 | regtest 4 | -------------------------------------------------------------------------------- /regtest/data/bitcoin.conf: -------------------------------------------------------------------------------- 1 | server=1 2 | whitelist=127.0.0.1 3 | txindex=1 4 | addressindex=1 5 | timestampindex=1 6 | spentindex=1 7 | zmqpubrawtx=tcp://127.0.0.1:30332 8 | zmqpubhashblock=tcp://127.0.0.1:30332 9 | rpcallowip=127.0.0.1 10 | rpcport=30331 11 | rpcuser=bitcoin 12 | rpcpassword=local321 13 | -------------------------------------------------------------------------------- /regtest/data/bitcoind.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICDTCCAXYCCQCsGf/7CM97gDANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECBMCR0ExDDAKBgNVBAcTA2ZvbzEhMB8GA1UEChMYSW50ZXJuZXQg 4 | V2lkZ2l0cyBQdHkgTHRkMB4XDTE1MDgyNjE3NTAwOFoXDTE1MDkyNTE3NTAwOFow 5 | SzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkdBMQwwCgYDVQQHEwNmb28xITAfBgNV 6 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOB 7 | jQAwgYkCgYEA7SXnPpKk+0qXTTa42jzvu/C/gdGby0arY50bE2A+epI7FlI5YqKd 8 | hQWoNRpuehF3jH6Ij3mbeeImtTA7TaUYlgHKpn63xfJ0cRj55+6vqq09nDxf0Lm9 9 | IpTbgllu1l+SHtSuzFBVtGuNRSqObf8gD5XCD5lWK1vXHQ6PFSnAakMCAwEAATAN 10 | BgkqhkiG9w0BAQUFAAOBgQBNARLDgsw7NCBVkn57AEgwZptxeyvFWlGZCd0BmYIX 11 | ZFk7T1OQDwn7GlHry2IBswI0QRi076RQ4oJq+fg2O3XdFvEYV0cyypW7AxrnYTHP 12 | m1h2xr6Y5vhxFKP8DxpAxST27DHbR18YvTD+IGtp2UjLj646587N0MWxt8vmaU3c 13 | og== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /regtest/data/bitcoind_no_pass.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXwIBAAKBgQDtJec+kqT7SpdNNrjaPO+78L+B0ZvLRqtjnRsTYD56kjsWUjli 3 | op2FBag1Gm56EXeMfoiPeZt54ia1MDtNpRiWAcqmfrfF8nRxGPnn7q+qrT2cPF/Q 4 | ub0ilNuCWW7WX5Ie1K7MUFW0a41FKo5t/yAPlcIPmVYrW9cdDo8VKcBqQwIDAQAB 5 | AoGBAOzYFxyCPu2OMI/4ICQuCcwtBEa2Ph+Fo/Rn2ru+OogV9Zc0ZYWiHSnWXYkz 6 | rbSSL1CMqvyIGoRfHgOFeSTxxxtVyRo5LayI6Ce6V04yUFua16uo6hMX5bfGKZ9d 7 | uq/HCDmdQvgifxUFpTpoSencuxwVCSYstMqjGfpobc5nxN2RAkEA+23BWNyA2qcq 8 | yqSplgTQO0laXox9ksr1k2mJB2HtG3GNs1kapP+Z8AVtn/Mf9Va0hgbSnqpF9fll 9 | 1xpBqfidSQJBAPF1rizAm7xP7AeRRET36YKjZCVayA/g4rp7kecrNJTTupJIuxHr 10 | JUlOTtXEWjIVcutSM7bP7bytPv4SAaApBysCQQDEZhqnCC+bHQO/IVrbNc1W0ljG 11 | DGY22VV1HfYND0CAtHXkx9CZXJPpusPEMs0e/uiq3P9/MzDNEFCt8vOiCvMJAkEA 12 | 65oDIKGzk/R7/wpMjetEyva5AgXpjizFrmZigCjVPp61voT/G8XQ9Q1WuRjFVXc+ 13 | UcU8tpV+iIqXG3vgYDGITwJBANb0NFFF8QsygbENtad1tw1C/hNabHk8n9hu+Z8+ 14 | OSzEMlP7SsvddPaqusGydhTUxazoG3s4kEh5WmCWKgZGKO0= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /regtest/data/node1/bitcoin.conf: -------------------------------------------------------------------------------- 1 | server=1 2 | whitelist=127.0.0.1 3 | txindex=1 4 | addressindex=1 5 | timestampindex=1 6 | spentindex=1 7 | addnode=127.0.0.1:30432 8 | addnode=127.0.0.1:30433 9 | port=30431 10 | rpcport=30521 11 | zmqpubrawtx=tcp://127.0.0.1:30611 12 | zmqpubhashblock=tcp://127.0.0.1:30611 13 | rpcallowip=127.0.0.1 14 | rpcuser=bitcoin 15 | rpcpassword=local321 16 | keypool=3 17 | -------------------------------------------------------------------------------- /regtest/data/node2/bitcoin.conf: -------------------------------------------------------------------------------- 1 | server=1 2 | whitelist=127.0.0.1 3 | txindex=1 4 | addressindex=1 5 | timestampindex=1 6 | spentindex=1 7 | addnode=127.0.0.1:30431 8 | addnode=127.0.0.1:30433 9 | port=30432 10 | rpcport=30522 11 | zmqpubrawtx=tcp://127.0.0.1:30622 12 | zmqpubhashblock=tcp://127.0.0.1:30622 13 | rpcallowip=127.0.0.1 14 | rpcuser=bitcoin 15 | rpcpassword=local321 16 | keypool=3 17 | -------------------------------------------------------------------------------- /regtest/data/node3/bitcoin.conf: -------------------------------------------------------------------------------- 1 | server=1 2 | whitelist=127.0.0.1 3 | txindex=1 4 | addressindex=1 5 | timestampindex=1 6 | spentindex=1 7 | addnode=127.0.0.1:30431 8 | addnode=127.0.0.1:30432 9 | port=30433 10 | rpcport=30523 11 | zmqpubrawtx=tcp://127.0.0.1:30633 12 | zmqpubhashblock=tcp://127.0.0.1:30633 13 | rpcallowip=127.0.0.1 14 | rpcuser=bitcoin 15 | rpcpassword=local321 16 | keypool=3 17 | -------------------------------------------------------------------------------- /regtest/p2p.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // To run the tests: $ mocha -R spec regtest/p2p.js 4 | 5 | var path = require('path'); 6 | var index = require('..'); 7 | var log = index.log; 8 | 9 | var bchP2p = require('@owstack/bch-p2p'); 10 | var Peer = bchP2p.Peer; 11 | var Messages = bchP2p.Messages; 12 | var chai = require('chai'); 13 | var bchLib = require('@owstack/bch-lib'); 14 | var Transaction = bchLib.Transaction; 15 | var BN = bchLib.crypto.BN; 16 | var async = require('async'); 17 | var rimraf = require('rimraf'); 18 | var bitcoind; 19 | 20 | /* jshint unused: false */ 21 | var should = chai.should(); 22 | var assert = chai.assert; 23 | var sinon = require('sinon'); 24 | var BitcoinRPC = require('@owstack/bitcoind-rpc'); 25 | var transactionData = []; 26 | var blockHashes = []; 27 | var txs = []; 28 | var client; 29 | var messages; 30 | var peer; 31 | var coinbasePrivateKey; 32 | var privateKey = bchLib.PrivateKey(); 33 | var destKey = bchLib.PrivateKey(); 34 | var BufferUtil = bchLib.util.buffer; 35 | var blocks; 36 | 37 | describe('P2P Functionality', function() { 38 | 39 | before(function(done) { 40 | this.timeout(100000); 41 | 42 | // enable regtest 43 | bchLib.Networks.enableRegtest(); 44 | var regtestNetwork = bchLib.Networks.get('regtest'); 45 | var datadir = __dirname + '/data'; 46 | 47 | rimraf(datadir + '/regtest', function(err) { 48 | if (err) { 49 | throw err; 50 | } 51 | 52 | bitcoind = require('../').services.Bitcoin({ 53 | spawn: { 54 | datadir: datadir, 55 | exec: path.resolve(__dirname, '../bin/bitcoind') 56 | }, 57 | node: { 58 | network: bchLib.Networks.testnet 59 | } 60 | }); 61 | 62 | bitcoind.on('error', function(err) { 63 | log.error('error="%s"', err.message); 64 | }); 65 | 66 | log.info('Waiting for Bitcoin Core to initialize...'); 67 | 68 | bitcoind.start(function(err) { 69 | if (err) { 70 | throw err; 71 | } 72 | log.info('Bitcoind started'); 73 | 74 | client = new BitcoinRPC({ 75 | protocol: 'http', 76 | host: '127.0.0.1', 77 | port: 30331, 78 | user: 'bitcoin', 79 | pass: 'local321', 80 | rejectUnauthorized: false 81 | }); 82 | 83 | peer = new Peer({ 84 | host: '127.0.0.1', 85 | port: '18444', 86 | network: regtestNetwork 87 | }); 88 | 89 | messages = new Messages({ 90 | network: regtestNetwork 91 | }); 92 | 93 | blocks = 500; 94 | 95 | log.info('Generating ' + blocks + ' blocks...'); 96 | 97 | // Generate enough blocks so that the initial coinbase transactions 98 | // can be spent. 99 | 100 | setImmediate(function() { 101 | client.generate(blocks, function(err, response) { 102 | if (err) { 103 | throw err; 104 | } 105 | blockHashes = response.result; 106 | 107 | log.info('Preparing test data...'); 108 | 109 | // Get all of the unspent outputs 110 | client.listUnspent(0, blocks, function(err, response) { 111 | var utxos = response.result; 112 | 113 | async.mapSeries(utxos, function(utxo, next) { 114 | async.series([ 115 | function(finished) { 116 | // Load all of the transactions for later testing 117 | client.getTransaction(utxo.txid, function(err, txresponse) { 118 | if (err) { 119 | throw err; 120 | } 121 | // add to the list of transactions for testing later 122 | transactionData.push(txresponse.result.hex); 123 | finished(); 124 | }); 125 | }, 126 | function(finished) { 127 | // Get the private key for each utxo 128 | client.dumpPrivKey(utxo.address, function(err, privresponse) { 129 | if (err) { 130 | throw err; 131 | } 132 | utxo.privateKeyWIF = privresponse.result; 133 | var tx = bchLib.Transaction(); 134 | tx.from(utxo); 135 | tx.change(privateKey.toAddress()); 136 | tx.to(destKey.toAddress(), utxo.amount * 1e8 - 1000); 137 | tx.sign(bchLib.PrivateKey.fromWIF(utxo.privateKeyWIF)); 138 | txs.push(tx); 139 | finished(); 140 | }); 141 | } 142 | ], next); 143 | }, function(err) { 144 | if (err) { 145 | throw err; 146 | } 147 | peer.on('ready', function() { 148 | log.info('Peer ready'); 149 | done(); 150 | }); 151 | log.info('Connecting to peer'); 152 | peer.connect(); 153 | }); 154 | }); 155 | }); 156 | }); 157 | }); 158 | }); 159 | 160 | }); 161 | 162 | after(function(done) { 163 | this.timeout(20000); 164 | peer.on('disconnect', function() { 165 | log.info('Peer disconnected'); 166 | bitcoind.node.stopping = true; 167 | bitcoind.stop(function(err, result) { 168 | done(); 169 | }); 170 | }); 171 | peer.disconnect(); 172 | }); 173 | 174 | it('will be able to handle many inventory messages and be able to send getdata messages and received the txs', function(done) { 175 | this.timeout(100000); 176 | 177 | var usedTxs = {}; 178 | 179 | bitcoind.on('tx', function(buffer) { 180 | var txFromResult = new Transaction().fromBuffer(buffer); 181 | var tx = usedTxs[txFromResult.id]; 182 | should.exist(tx); 183 | buffer.toString('hex').should.equal(tx.serialize()); 184 | delete usedTxs[tx.id]; 185 | if (Object.keys(usedTxs).length === 0) { 186 | done(); 187 | } 188 | }); 189 | 190 | peer.on('getdata', function(message) { 191 | var hash = message.inventory[0].hash; 192 | var reversedHash = BufferUtil.reverse(hash).toString('hex'); 193 | var tx = usedTxs[reversedHash]; 194 | if (reversedHash === tx.id) { 195 | var txMessage = messages.Transaction(tx); 196 | peer.sendMessage(txMessage); 197 | } 198 | }); 199 | async.whilst( 200 | function() { 201 | return txs.length > 0; 202 | }, 203 | function(callback) { 204 | var tx = txs.pop(); 205 | usedTxs[tx.id] = tx; 206 | var message = messages.Inventory.forTransaction(tx.hash); 207 | peer.sendMessage(message); 208 | callback(); 209 | }, 210 | function(err) { 211 | if (err) { 212 | throw err; 213 | } 214 | }); 215 | }); 216 | 217 | }); 218 | -------------------------------------------------------------------------------- /scripts/regtest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | _mocha -R spec regtest/p2p.js 6 | _mocha -R spec regtest/bitcoind.js 7 | _mocha -R spec regtest/cluster.js 8 | _mocha -R spec regtest/node.js 9 | -------------------------------------------------------------------------------- /test/bus.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var Service = require('../lib/service'); 5 | var BchNode = require('../lib/node'); 6 | var util = require('util'); 7 | var should = require('chai').should(); 8 | var index = require('../lib'); 9 | var log = index.log; 10 | 11 | var TestService = function(options) { 12 | this.node = options.node; 13 | }; 14 | util.inherits(TestService, Service); 15 | TestService.dependencies = []; 16 | 17 | TestService.prototype.start = function(callback) { 18 | callback(); 19 | }; 20 | TestService.prototype.stop = function(callback) { 21 | callback(); 22 | }; 23 | TestService.prototype.close = function(callback) { 24 | callback(); 25 | }; 26 | TestService.prototype.getPublishEvents = function() { 27 | return [ 28 | { 29 | name: 'test/testEvent', 30 | scope: this, 31 | subscribe: this.subscribe.bind(this, 'test/testEvent'), 32 | unsubscribe: this.unsubscribe.bind(this, 'test/testEvent') 33 | } 34 | ]; 35 | }; 36 | 37 | TestService.prototype.subscribe = function(name, emitter, params) { 38 | emitter.emit(name, params); 39 | }; 40 | 41 | TestService.prototype.unsubscribe = function(name, emitter) { 42 | emitter.emit('unsubscribe'); 43 | }; 44 | 45 | 46 | describe('Bus Functionality', function() { 47 | var sandbox = sinon.sandbox.create(); 48 | beforeEach(function() { 49 | sandbox.stub(log, 'info'); 50 | }); 51 | afterEach(function() { 52 | sandbox.restore(); 53 | }); 54 | 55 | it('should subscribe to testEvent', function(done) { 56 | var node = new BchNode({ 57 | datadir: './', 58 | network: 'testnet', 59 | port: 8888, 60 | services: [ 61 | { 62 | name: 'testService', 63 | config: {}, 64 | module: TestService 65 | } 66 | ] 67 | }); 68 | node.start(function() { 69 | var bus = node.openBus(); 70 | var params = 'somedata'; 71 | bus.on('test/testEvent', function(data) { 72 | data.should.be.equal(params); 73 | done(); 74 | }); 75 | bus.subscribe('test/testEvent', params); 76 | }); 77 | }); 78 | 79 | it('should unsubscribe from a testEvent', function(done) { 80 | var node = new BchNode({ 81 | datadir: './', 82 | network: 'testnet', 83 | port: 8888, 84 | services: [ 85 | { 86 | name: 'testService', 87 | config: {}, 88 | module: TestService 89 | } 90 | ] 91 | }); 92 | node.start(function() { 93 | var bus = node.openBus(); 94 | var params = 'somedata'; 95 | bus.on('unsubscribe', function() { 96 | done(); 97 | }); 98 | bus.subscribe('test/testEvent'); 99 | bus.unsubscribe('test/testEvent'); 100 | 101 | }); 102 | 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/bus.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var sinon = require('sinon'); 5 | var Bus = require('../lib/bus'); 6 | 7 | describe('Bus', function() { 8 | 9 | describe('#subscribe', function() { 10 | it('will call db and services subscribe function with the correct arguments', function() { 11 | var subscribeDb = sinon.spy(); 12 | var subscribeService = sinon.spy(); 13 | var node = { 14 | services: { 15 | db: { 16 | getPublishEvents: sinon.stub().returns([ 17 | { 18 | name: 'dbtest', 19 | scope: this, 20 | subscribe: subscribeDb 21 | } 22 | ]) 23 | }, 24 | service1: { 25 | getPublishEvents: sinon.stub().returns([ 26 | { 27 | name: 'test', 28 | scope: this, 29 | subscribe: subscribeService, 30 | } 31 | ]) 32 | } 33 | } 34 | }; 35 | var bus = new Bus({node: node}); 36 | bus.subscribe('dbtest', 'a', 'b', 'c'); 37 | bus.subscribe('test', 'a', 'b', 'c'); 38 | subscribeService.callCount.should.equal(1); 39 | subscribeDb.callCount.should.equal(1); 40 | subscribeDb.args[0][0].should.equal(bus); 41 | subscribeDb.args[0][1].should.equal('a'); 42 | subscribeDb.args[0][2].should.equal('b'); 43 | subscribeDb.args[0][3].should.equal('c'); 44 | subscribeService.args[0][0].should.equal(bus); 45 | subscribeService.args[0][1].should.equal('a'); 46 | subscribeService.args[0][2].should.equal('b'); 47 | subscribeService.args[0][3].should.equal('c'); 48 | }); 49 | }); 50 | 51 | describe('#unsubscribe', function() { 52 | it('will call db and services unsubscribe function with the correct arguments', function() { 53 | var unsubscribeDb = sinon.spy(); 54 | var unsubscribeService = sinon.spy(); 55 | var node = { 56 | services: { 57 | db: { 58 | getPublishEvents: sinon.stub().returns([ 59 | { 60 | name: 'dbtest', 61 | scope: this, 62 | unsubscribe: unsubscribeDb 63 | } 64 | ]) 65 | }, 66 | service1: { 67 | getPublishEvents: sinon.stub().returns([ 68 | { 69 | name: 'test', 70 | scope: this, 71 | unsubscribe: unsubscribeService, 72 | } 73 | ]) 74 | } 75 | } 76 | }; 77 | var bus = new Bus({node: node}); 78 | bus.unsubscribe('dbtest', 'a', 'b', 'c'); 79 | bus.unsubscribe('test', 'a', 'b', 'c'); 80 | unsubscribeService.callCount.should.equal(1); 81 | unsubscribeDb.callCount.should.equal(1); 82 | unsubscribeDb.args[0][0].should.equal(bus); 83 | unsubscribeDb.args[0][1].should.equal('a'); 84 | unsubscribeDb.args[0][2].should.equal('b'); 85 | unsubscribeDb.args[0][3].should.equal('c'); 86 | unsubscribeService.args[0][0].should.equal(bus); 87 | unsubscribeService.args[0][1].should.equal('a'); 88 | unsubscribeService.args[0][2].should.equal('b'); 89 | unsubscribeService.args[0][3].should.equal('c'); 90 | }); 91 | }); 92 | 93 | describe('#close', function() { 94 | it('will unsubscribe from all events', function() { 95 | var unsubscribeDb = sinon.spy(); 96 | var unsubscribeService = sinon.spy(); 97 | var node = { 98 | services: { 99 | db: { 100 | getPublishEvents: sinon.stub().returns([ 101 | { 102 | name: 'dbtest', 103 | scope: this, 104 | unsubscribe: unsubscribeDb 105 | } 106 | ]) 107 | }, 108 | service1: { 109 | getPublishEvents: sinon.stub().returns([ 110 | { 111 | name: 'test', 112 | scope: this, 113 | unsubscribe: unsubscribeService 114 | } 115 | ]) 116 | } 117 | } 118 | }; 119 | var bus = new Bus({node: node}); 120 | bus.close(); 121 | 122 | unsubscribeDb.callCount.should.equal(1); 123 | unsubscribeService.callCount.should.equal(1); 124 | unsubscribeDb.args[0].length.should.equal(1); 125 | unsubscribeDb.args[0][0].should.equal(bus); 126 | unsubscribeService.args[0].length.should.equal(1); 127 | unsubscribeService.args[0][0].should.equal(bus); 128 | }); 129 | }); 130 | 131 | }); 132 | -------------------------------------------------------------------------------- /test/data/badbitcoin.conf: -------------------------------------------------------------------------------- 1 | #testnet=1 2 | #irc=0 3 | #upnp=0 4 | server=1 5 | 6 | whitelist=127.0.0.1 7 | 8 | # listen on different ports 9 | port=20000 10 | 11 | rpcallowip=127.0.0.1 12 | 13 | rpcuser=bitcoin 14 | rpcpassword=local321 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/data/bitcoin-transactions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "sends to tx[1]", 4 | "hex":"0100000001dee8f4266e83072e0ad258125cc5a42ac25d2d2c73e6e2e873413b3939af1605000000006b483045022100ae987d056f81d2c982b71b0406f2374c1958b24bd289d77371347e275d2a62c002205148b17173be18af4e1e73ce2b0fd600734ea77087754bdba5dc7d645b01880a01210226ab3b46f85bf32f63778c680e16ef8b3fcb51d638a7980d651bfaeae6c17752ffffffff0170820300000000001976a9142baf68e3681df183375a4f4c10306de9a5c6cc7788ac00000000" 5 | }, 6 | { 7 | "comment": "spends from tx[0] (valid)", 8 | "hex":"0100000001f77c71cf8c272d22471f054cae7fb48561ebcf004b8ec8f9f65cd87af82a2944000000006a47304402203c2bc91a170facdc5ef4b5b94c413bc7a10f65e09b326d205f070b17aa94d67102205b684111af2a20171eb65db73e6c73f9e77e6e6f739e050bc052ed6ecc9feb4a01210365d8756a4f3fc738105cfab8d80a85189bdb4db5af83374e645b79e2aadd976effffffff01605b0300000000001976a9149e84d1295471958e5ffccd8d36a57bd5d220f8ed88ac00000000" 9 | }, 10 | { 11 | "comment": "spends from tx[0] (missing signature)", 12 | "hex":"0100000001f77c71cf8c272d22471f054cae7fb48561ebcf004b8ec8f9f65cd87af82a29440000000000ffffffff01605b0300000000001976a9149e84d1295471958e5ffccd8d36a57bd5d220f8ed88ac00000000" 13 | } 14 | ] -------------------------------------------------------------------------------- /test/data/bitcoin.conf: -------------------------------------------------------------------------------- 1 | #testnet=1 2 | #irc=0 3 | upnp=0 4 | server=1 5 | 6 | whitelist=127.0.0.1 7 | txindex=1 8 | addressindex=1 9 | timestampindex=1 10 | spentindex=1 11 | dbcache=8192 12 | checkblocks=144 13 | maxuploadtarget=1024 14 | zmqpubrawtx=tcp://127.0.0.1:28332 15 | zmqpubhashblock=tcp://127.0.0.1:28332 16 | 17 | port=20000 18 | rpcport=50001 19 | 20 | rpcallowip=127.0.0.1 21 | 22 | rpcuser=bitcoin 23 | rpcpassword=local321 24 | -------------------------------------------------------------------------------- /test/data/default.bitcoin.conf: -------------------------------------------------------------------------------- 1 | server=1 2 | whitelist=127.0.0.1 3 | txindex=1 4 | addressindex=1 5 | timestampindex=1 6 | spentindex=1 7 | zmqpubrawtx=tcp://127.0.0.1:28332 8 | zmqpubhashblock=tcp://127.0.0.1:28332 9 | rpcallowip=127.0.0.1 10 | rpcuser=bitcoin 11 | rpcpassword=local321 12 | uacomment=bch 13 | -------------------------------------------------------------------------------- /test/data/transaction.json: -------------------------------------------------------------------------------- 1 | [ 2 | "010000000ec9e1d96d51f7d0a5b726184cda744207e51a596f13b564109de6ffc0653055cf000000006a4730440220263d48a2f4c3a2aa6032f96253c853531131171d8ae3d30586a45de7ba7c4006022077db4b39926877939baf59e8d357effe7674e7b12ad986c0a4cd99f2b7acafca012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff0c16800642bdf90cbbd340be2e801bee9b907db6d59dc4c7cb269358c1e2593a000000006a4730440220085e39cb3a948559c1b1c88ba635eeef37a767322c1e4d96556e998302acb5dc0220421c03de78121692c538a9bc85a58fbe796384541fe87b6e91c39404318c390d012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff559fde3733e950db9e3faea1a27872047a5c8bc14e8e6ac4a7b4f7b5f90c42c2000000006b483045022100a46a109a5acfc34b13c591ab69392e2dc2c3ea12d0900c5f7a539ea888e57ae0022015372bad56d63c6d08a5e66e0c9a63b2fc8dce245aa765e37dac06acb84c18d501210207e2a01d4a334c2d7c9ebacc5ea8a0d4b86fd54599b1aebe125d9e664be012c2ffffffff92760c236f6f18751c4ecab1dfcfebac7358a431b31bcd72b0a09d336233bdce000000006a47304402200857fa82e5b287c6ed5624cbc4fcd068a756a7a9ef785a73ce4e51b15a8aa34b022042cbc6f478b711539c6345d0b05d4bc9a9b5c34b2e4d25f234cf6784ff2eed19012103cab1f64f3d5f20a3f4a070694e6f93f5248b502b3842e369d81f05d07dec01e3ffffffff158669557e8a0b71288df37626601e636c5a8b3797f7f3357590313e8efe790a000000006b48304502210085e62cb95066540730b74aeae133277e511b5bf79de8c0ad60e784638e681ddf022043b456285569e0da133f527412218893f05a781d6f77f8cddd12eb90bfdc5937012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffc1f9f57f1eda20f3b45669b3f0d1eae73b410ddf2b4fc1cfe10051a6051eff68000000006a47304402200fbe15c73446309040f4264567d0e8cc46691cf5d0626c443fc2dde716211e5402207f84e68e273755d140f346e029213dbc42b65cfb1e2ac41f72402c7ff45feffc012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff39fa27e16c540a9bb796e65d4ac624fc33878e522461d6955764bf7b83c03ce8000000006a4730440220131e6aed76da389f21dfd7cd271fad73c5c533b1c18fbfabd6799a0d0e7dc80602205c38855bea0f1dfbbb608bc1b56c75b91a39980de121ffd9f880b522d412d821012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff375fb7ae26eb637ccc007a669b9f50ed10fa53127504b80e0fd2be34c50becd7000000006b483045022100f0a9e585aa3113eae4bfb204f38f96e60dc613c04313aae931b677e4d8d7081d022014664874859f3d47447c6c0f257c28c74e8fdaedd5f781d752f3a4b651d3d179012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff6f9d6a3c847e6112bb920424ca0d8d6e0956b128acb206e8fb58b2d2f2d7d46b000000006a4730440220736b198484cf5226616a539146e037a93cc75963885eefe58fc29a7be8123c750220619a456c0fe7437ec67c642d88e890344fc1c51a7b3cfc0ae283d61d0f176c5e012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff3cccbd8090d60fcf28064333cf2f51ef0f838ba5e26a5e0f38807ee16d38a649000000006b483045022100e1ed25e9365e596d4fc3cbf278490d8ea765c4266c55f19311cf5da33f8d00750220361888a1738ebba827c0c947690b5f2a5f20e9f1be8956c3a34a4ba03f9e60f5012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff7f4d60a2e961490aa465a7df461bf09b151bdc0c162f3bef0c1cbed6160d02c7000000006a47304402204e79b15a1db9a356f00dc9f2d559e31561cad1343ba5809a65b52bd868e3963e022055b9326ed5de9aa9970ec67a2ebf1a9dbf9ee513b64bd13837c87320bb4d6947012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffe63b9370ba49a129e071750cbb300128015fdd90d7399f9c4e44934eabbaa2f7000000006b483045022100b9ceb2e376c0334d45bf08bfeb06dc250e7cb01d3a08e2fb3506388683552417022024c7c5bda385b904cca691fb6e1ad8c5eba5858a88a2112cb824dca72793b7a7012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffc78b96fddededb6cbc1dff9de51f2743fd42e91de2506794b121928af4729528000000006a47304402201f05caddee5a0ff590b27c4ce25be1cbbeb45dc39679a1b8b0e10b1a378d84bc02203e31b01e14d891e9809c43a4df54494c626c5e47eaeeeb99ab4e02bd73c3d6cd012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff30093916240e981a77cb869924fa0c38a894c24b1a6e7d26b117bb9caa7d5bbe000000006a4730440220483f297379daacee14babbf929708a861a991373bca3ed4eef240e2c156a162602205f1e93e375a897c6a9ddc3dc616ccf14137096ebd7888040e1053a769d21b945012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff022897d411000000001976a91415354ee1828ed12f243f80dcb92c3a8205285f0188ac3be68c02000000001976a9143ecf8ff79932c3de33829a001236985872d10be188ac00000000" 3 | ] 4 | -------------------------------------------------------------------------------- /test/index.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | 5 | describe('Index Exports', function() { 6 | it('will export bch-lib', function() { 7 | var bchLib = require('../'); 8 | should.exist(bchLib.lib); 9 | should.exist(bchLib.lib.Transaction); 10 | should.exist(bchLib.lib.Block); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/logger.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var chai = require('chai'); 5 | var should = chai.should(); 6 | var Logger = require('../lib/logger'); 7 | 8 | describe('Logger', function() { 9 | var sandbox = sinon.sandbox.create(); 10 | afterEach(function() { 11 | sandbox.restore(); 12 | }); 13 | 14 | it('will instatiate without options', function() { 15 | var logger = new Logger(); 16 | should.exist(logger); 17 | logger.formatting.should.equal(true); 18 | }); 19 | 20 | it('will instatiate with formatting option', function() { 21 | var logger = new Logger({ 22 | formatting: false 23 | }); 24 | logger.formatting.should.equal(false); 25 | var logger2 = new Logger({ 26 | formatting: true 27 | }); 28 | logger2.formatting.should.equal(true); 29 | }); 30 | 31 | it('will log with formatting', function() { 32 | var logger = new Logger({formatting: true}); 33 | 34 | sandbox.stub(console, 'info'); 35 | logger.info('Test info log'); 36 | console.info.callCount.should.equal(1); 37 | console.info.restore(); 38 | 39 | sandbox.stub(console, 'error'); 40 | logger.error(new Error('Test error log')); 41 | console.error.callCount.should.equal(1); 42 | console.error.restore(); 43 | 44 | sandbox.stub(console, 'debug'); 45 | logger.debug('Test debug log'); 46 | console.debug.callCount.should.equal(1); 47 | console.debug.restore(); 48 | 49 | sandbox.stub(console, 'warn'); 50 | logger.warn('Test warn log'); 51 | console.warn.callCount.should.equal(1); 52 | console.warn.restore(); 53 | }); 54 | 55 | it('will log without formatting', function() { 56 | var logger = new Logger({formatting: false}); 57 | 58 | sandbox.stub(console, 'info'); 59 | logger.info('Test info log'); 60 | console.info.callCount.should.equal(1); 61 | should.not.exist(console.info.args[0][0].match(/^\[/)); 62 | console.info.restore(); 63 | 64 | sandbox.stub(console, 'error'); 65 | logger.error(new Error('Test error log')); 66 | console.error.callCount.should.equal(1); 67 | console.error.args[0][0].should.be.instanceof(Error); 68 | console.error.restore(); 69 | 70 | sandbox.stub(console, 'debug'); 71 | logger.debug('Test debug log'); 72 | console.debug.callCount.should.equal(1); 73 | should.equal(console.debug.args[0][0].match(/^\[/), null); 74 | console.debug.restore(); 75 | 76 | sandbox.stub(console, 'warn'); 77 | logger.warn('Test warn log'); 78 | console.warn.callCount.should.equal(1); 79 | should.equal(console.warn.args[0][0].match(/^\[/), null); 80 | console.warn.restore(); 81 | }); 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /test/node.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var sinon = require('sinon'); 5 | var bchLib = require('@owstack/bch-lib'); 6 | var Networks = bchLib.Networks; 7 | var proxyquire = require('proxyquire'); 8 | var util = require('util'); 9 | var BaseService = require('../lib/service'); 10 | var index = require('../lib'); 11 | var log = index.log; 12 | 13 | describe('Bch Node', function() { 14 | 15 | var baseConfig = {}; 16 | 17 | var Node; 18 | 19 | before(function() { 20 | Node = proxyquire('../lib/node', {}); 21 | Node.prototype._loadConfiguration = sinon.spy(); 22 | Node.prototype._initialize = sinon.spy(); 23 | }); 24 | 25 | after(function() { 26 | Networks.disableRegtest(); 27 | }); 28 | 29 | describe('@constructor', function() { 30 | var TestService; 31 | before(function() { 32 | TestService = function TestService() {}; 33 | util.inherits(TestService, BaseService); 34 | }); 35 | it('will set properties', function() { 36 | var config = { 37 | services: [ 38 | { 39 | name: 'test1', 40 | module: TestService 41 | } 42 | ], 43 | }; 44 | var TestNode = proxyquire('../lib/node', {}); 45 | TestNode.prototype.start = sinon.spy(); 46 | var node = new TestNode(config); 47 | node._unloadedServices.length.should.equal(1); 48 | node._unloadedServices[0].name.should.equal('test1'); 49 | node._unloadedServices[0].module.should.equal(TestService); 50 | node.network.should.equal(Networks.defaultNetwork); 51 | var node2 = TestNode(config); 52 | node2._unloadedServices.length.should.equal(1); 53 | node2._unloadedServices[0].name.should.equal('test1'); 54 | node2._unloadedServices[0].module.should.equal(TestService); 55 | node2.network.should.equal(Networks.defaultNetwork); 56 | }); 57 | it('will set network to testnet', function() { 58 | var config = { 59 | network: 'testnet', 60 | services: [ 61 | { 62 | name: 'test1', 63 | module: TestService 64 | } 65 | ], 66 | }; 67 | var TestNode = proxyquire('../lib/node', {}); 68 | TestNode.prototype.start = sinon.spy(); 69 | var node = new TestNode(config); 70 | node.network.should.equal(Networks.testnet); 71 | }); 72 | it('will set network to regtest', function() { 73 | var config = { 74 | network: 'regtest', 75 | services: [ 76 | { 77 | name: 'test1', 78 | module: TestService 79 | } 80 | ], 81 | }; 82 | var TestNode = proxyquire('../lib/node', {}); 83 | TestNode.prototype.start = sinon.spy(); 84 | var node = new TestNode(config); 85 | var regtest = Networks.get('regtest'); 86 | should.exist(regtest); 87 | node.network.should.equal(regtest); 88 | }); 89 | it('will be able to disable log formatting', function() { 90 | var config = { 91 | network: 'regtest', 92 | services: [ 93 | { 94 | name: 'test1', 95 | module: TestService 96 | } 97 | ], 98 | formatLogs: false 99 | }; 100 | var TestNode = proxyquire('../lib/node', {}); 101 | var node = new TestNode(config); 102 | node.log.formatting.should.equal(false); 103 | 104 | var TestNode = proxyquire('../lib/node', {}); 105 | config.formatLogs = true; 106 | var node2 = new TestNode(config); 107 | node2.log.formatting.should.equal(true); 108 | }); 109 | }); 110 | 111 | describe('#openBus', function() { 112 | it('will create a new bus', function() { 113 | var node = new Node(baseConfig); 114 | var bus = node.openBus(); 115 | bus.node.should.equal(node); 116 | }); 117 | it('will use remoteAddress config option', function() { 118 | var node = new Node(baseConfig); 119 | var bus = node.openBus({remoteAddress: '127.0.0.1'}); 120 | bus.remoteAddress.should.equal('127.0.0.1'); 121 | }); 122 | }); 123 | 124 | describe('#getAllAPIMethods', function() { 125 | it('should return db methods and service methods', function() { 126 | var node = new Node(baseConfig); 127 | node.services = { 128 | db: { 129 | getAPIMethods: sinon.stub().returns(['db1', 'db2']), 130 | }, 131 | service1: { 132 | getAPIMethods: sinon.stub().returns(['mda1', 'mda2']) 133 | }, 134 | service2: { 135 | getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2']) 136 | } 137 | }; 138 | 139 | var methods = node.getAllAPIMethods(); 140 | methods.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); 141 | }); 142 | it('will handle service without getAPIMethods defined', function() { 143 | var node = new Node(baseConfig); 144 | node.services = { 145 | db: { 146 | getAPIMethods: sinon.stub().returns(['db1', 'db2']), 147 | }, 148 | service1: {}, 149 | service2: { 150 | getAPIMethods: sinon.stub().returns(['mdb1', 'mdb2']) 151 | } 152 | }; 153 | 154 | var methods = node.getAllAPIMethods(); 155 | methods.should.deep.equal(['db1', 'db2', 'mdb1', 'mdb2']); 156 | }); 157 | }); 158 | 159 | describe('#getAllPublishEvents', function() { 160 | it('should return services publish events', function() { 161 | var node = new Node(baseConfig); 162 | node.services = { 163 | db: { 164 | getPublishEvents: sinon.stub().returns(['db1', 'db2']), 165 | }, 166 | service1: { 167 | getPublishEvents: sinon.stub().returns(['mda1', 'mda2']) 168 | }, 169 | service2: { 170 | getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2']) 171 | } 172 | }; 173 | var events = node.getAllPublishEvents(); 174 | events.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); 175 | }); 176 | it('will handle service without getPublishEvents defined', function() { 177 | var node = new Node(baseConfig); 178 | node.services = { 179 | db: { 180 | getPublishEvents: sinon.stub().returns(['db1', 'db2']), 181 | }, 182 | service1: {}, 183 | service2: { 184 | getPublishEvents: sinon.stub().returns(['mdb1', 'mdb2']) 185 | } 186 | }; 187 | var events = node.getAllPublishEvents(); 188 | events.should.deep.equal(['db1', 'db2', 'mdb1', 'mdb2']); 189 | }); 190 | }); 191 | 192 | describe('#getServiceOrder', function() { 193 | it('should return the services in the correct order', function() { 194 | var node = new Node(baseConfig); 195 | node._unloadedServices = [ 196 | { 197 | name: 'chain', 198 | moduleName: 'chain', 199 | module: { 200 | dependencies: ['db'] 201 | } 202 | }, 203 | { 204 | name: 'db', 205 | moduleName: 'db', 206 | module: { 207 | dependencies: ['daemon', 'p2p'] 208 | } 209 | }, 210 | { 211 | name:'daemon', 212 | moduleName: 'daemon', 213 | module: { 214 | dependencies: [] 215 | } 216 | }, 217 | { 218 | name: 'p2p', 219 | moduleName: 'p2p', 220 | module: { 221 | dependencies: [] 222 | } 223 | } 224 | ]; 225 | var order = node.getServiceOrder(); 226 | order[0].name.should.equal('daemon'); 227 | order[1].name.should.equal('p2p'); 228 | order[2].name.should.equal('db'); 229 | order[3].name.should.equal('chain'); 230 | }); 231 | }); 232 | 233 | describe('#_startService', function() { 234 | var sandbox = sinon.sandbox.create(); 235 | beforeEach(function() { 236 | sandbox.stub(log, 'info'); 237 | }); 238 | afterEach(function() { 239 | sandbox.restore(); 240 | }); 241 | it('will instantiate an instance and load api methods', function() { 242 | var node = new Node(baseConfig); 243 | function TestService() {} 244 | util.inherits(TestService, BaseService); 245 | TestService.prototype.start = sinon.stub().callsArg(0); 246 | var getData = sinon.stub(); 247 | TestService.prototype.getData = getData; 248 | TestService.prototype.getAPIMethods = function() { 249 | return [ 250 | ['getData', this, this.getData, 1] 251 | ]; 252 | }; 253 | var service = { 254 | name: 'testservice', 255 | module: TestService, 256 | config: {} 257 | }; 258 | node._startService(service, function(err) { 259 | if (err) { 260 | throw err; 261 | } 262 | TestService.prototype.start.callCount.should.equal(1); 263 | should.exist(node.services.testservice); 264 | should.exist(node.getData); 265 | node.getData(); 266 | getData.callCount.should.equal(1); 267 | }); 268 | }); 269 | it('will handle config not being set', function() { 270 | var node = new Node(baseConfig); 271 | function TestService() {} 272 | util.inherits(TestService, BaseService); 273 | TestService.prototype.start = sinon.stub().callsArg(0); 274 | var getData = sinon.stub(); 275 | TestService.prototype.getData = getData; 276 | TestService.prototype.getAPIMethods = function() { 277 | return [ 278 | ['getData', this, this.getData, 1] 279 | ]; 280 | }; 281 | var service = { 282 | name: 'testservice', 283 | module: TestService, 284 | }; 285 | node._startService(service, function(err) { 286 | if (err) { 287 | throw err; 288 | } 289 | TestService.prototype.start.callCount.should.equal(1); 290 | should.exist(node.services.testservice); 291 | should.exist(node.getData); 292 | node.getData(); 293 | getData.callCount.should.equal(1); 294 | }); 295 | }); 296 | it('will give an error from start', function() { 297 | var node = new Node(baseConfig); 298 | function TestService() {} 299 | util.inherits(TestService, BaseService); 300 | TestService.prototype.start = sinon.stub().callsArgWith(0, new Error('test')); 301 | var service = { 302 | name: 'testservice', 303 | module: TestService, 304 | config: {} 305 | }; 306 | node._startService(service, function(err) { 307 | err.message.should.equal('test'); 308 | }); 309 | }); 310 | }); 311 | 312 | describe('#start', function() { 313 | var sandbox = sinon.sandbox.create(); 314 | beforeEach(function() { 315 | sandbox.stub(log, 'info'); 316 | }); 317 | afterEach(function() { 318 | sandbox.restore(); 319 | }); 320 | it('will call start for each service', function(done) { 321 | var node = new Node(baseConfig); 322 | 323 | function TestService() {} 324 | util.inherits(TestService, BaseService); 325 | TestService.prototype.start = sinon.stub().callsArg(0); 326 | TestService.prototype.getData = function() {}; 327 | TestService.prototype.getAPIMethods = function() { 328 | return [ 329 | ['getData', this, this.getData, 1] 330 | ]; 331 | }; 332 | 333 | function TestService2() {} 334 | util.inherits(TestService2, BaseService); 335 | TestService2.prototype.start = sinon.stub().callsArg(0); 336 | TestService2.prototype.getData2 = function() {}; 337 | TestService2.prototype.getAPIMethods = function() { 338 | return [ 339 | ['getData2', this, this.getData2, 1] 340 | ]; 341 | }; 342 | 343 | node.getServiceOrder = sinon.stub().returns([ 344 | { 345 | name: 'test1', 346 | module: TestService, 347 | config: {} 348 | }, 349 | { 350 | name: 'test2', 351 | module: TestService2, 352 | config: {} 353 | } 354 | ]); 355 | node.start(function() { 356 | TestService2.prototype.start.callCount.should.equal(1); 357 | TestService.prototype.start.callCount.should.equal(1); 358 | should.exist(node.getData2); 359 | should.exist(node.getData); 360 | done(); 361 | }); 362 | }); 363 | it('will error if there are conflicting API methods', function(done) { 364 | var node = new Node(baseConfig); 365 | 366 | function TestService() {} 367 | util.inherits(TestService, BaseService); 368 | TestService.prototype.start = sinon.stub().callsArg(0); 369 | TestService.prototype.getData = function() {}; 370 | TestService.prototype.getAPIMethods = function() { 371 | return [ 372 | ['getData', this, this.getData, 1] 373 | ]; 374 | }; 375 | 376 | function ConflictService() {} 377 | util.inherits(ConflictService, BaseService); 378 | ConflictService.prototype.start = sinon.stub().callsArg(0); 379 | ConflictService.prototype.getData = function() {}; 380 | ConflictService.prototype.getAPIMethods = function() { 381 | return [ 382 | ['getData', this, this.getData, 1] 383 | ]; 384 | }; 385 | 386 | node.getServiceOrder = sinon.stub().returns([ 387 | { 388 | name: 'test', 389 | module: TestService, 390 | config: {} 391 | }, 392 | { 393 | name: 'conflict', 394 | module: ConflictService, 395 | config: {} 396 | } 397 | ]); 398 | 399 | node.start(function(err) { 400 | should.exist(err); 401 | err.message.should.match(/^Existing API method\(s\) exists\:/); 402 | done(); 403 | }); 404 | 405 | }); 406 | it('will handle service with getAPIMethods undefined', function(done) { 407 | var node = new Node(baseConfig); 408 | 409 | function TestService() {} 410 | util.inherits(TestService, BaseService); 411 | TestService.prototype.start = sinon.stub().callsArg(0); 412 | TestService.prototype.getData = function() {}; 413 | 414 | node.getServiceOrder = sinon.stub().returns([ 415 | { 416 | name: 'test', 417 | module: TestService, 418 | config: {} 419 | }, 420 | ]); 421 | 422 | node.start(function() { 423 | TestService.prototype.start.callCount.should.equal(1); 424 | done(); 425 | }); 426 | 427 | }); 428 | }); 429 | 430 | describe('#getNetworkName', function() { 431 | afterEach(function() { 432 | bchLib.Networks.disableRegtest(); 433 | }); 434 | it('it will return the network name for livenet', function() { 435 | var node = new Node(baseConfig); 436 | node.getNetworkName().should.equal('livenet'); 437 | }); 438 | it('it will return the network name for testnet', function() { 439 | var baseConfig = { 440 | network: 'testnet' 441 | }; 442 | var node = new Node(baseConfig); 443 | node.getNetworkName().should.equal('testnet'); 444 | }); 445 | it('it will return the network for regtest', function() { 446 | var baseConfig = { 447 | network: 'regtest' 448 | }; 449 | var node = new Node(baseConfig); 450 | node.getNetworkName().should.equal('regtest'); 451 | }); 452 | }); 453 | 454 | describe('#stop', function() { 455 | var sandbox = sinon.sandbox.create(); 456 | beforeEach(function() { 457 | sandbox.stub(log, 'info'); 458 | }); 459 | afterEach(function() { 460 | sandbox.restore(); 461 | }); 462 | it('will call stop for each service', function(done) { 463 | var node = new Node(baseConfig); 464 | function TestService() {} 465 | util.inherits(TestService, BaseService); 466 | TestService.prototype.stop = sinon.stub().callsArg(0); 467 | TestService.prototype.getData = function() {}; 468 | TestService.prototype.getAPIMethods = function() { 469 | return [ 470 | ['getData', this, this.getData, 1] 471 | ]; 472 | }; 473 | node.services = { 474 | 'test1': new TestService({node: node}) 475 | }; 476 | node.test2 = {}; 477 | node.test2.stop = sinon.stub().callsArg(0); 478 | node.getServiceOrder = sinon.stub().returns([ 479 | { 480 | name: 'test1', 481 | module: TestService 482 | } 483 | ]); 484 | node.stop(function() { 485 | TestService.prototype.stop.callCount.should.equal(1); 486 | done(); 487 | }); 488 | }); 489 | }); 490 | }); 491 | -------------------------------------------------------------------------------- /test/scaffold/add.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var sinon = require('sinon'); 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | var proxyquire = require('proxyquire'); 8 | var mkdirp = require('mkdirp'); 9 | var rimraf = require('rimraf'); 10 | var add = require('../../lib/scaffold/add'); 11 | 12 | describe('#add', function() { 13 | 14 | var basePath = path.resolve(__dirname, '..'); 15 | var testDir = path.resolve(basePath, 'temporary-test-data'); 16 | var startConfig = { 17 | name: 'My Node', 18 | services: [] 19 | }; 20 | var startPackage = {}; 21 | 22 | before(function(done) { 23 | mkdirp(testDir + '/s0/s1', function(err) { 24 | if (err) { 25 | throw err; 26 | } 27 | fs.writeFile( 28 | testDir + '/s0/s1/bch-node.json', 29 | JSON.stringify(startConfig), 30 | function(err) { 31 | if (err) { 32 | throw err; 33 | } 34 | fs.writeFile( 35 | testDir + '/s0/s1/package.json', 36 | JSON.stringify(startPackage), 37 | done 38 | ); 39 | } 40 | ); 41 | }); 42 | }); 43 | 44 | after(function(done) { 45 | // cleanup testing directories 46 | rimraf(testDir, function(err) { 47 | if (err) { 48 | throw err; 49 | } 50 | done(); 51 | }); 52 | }); 53 | 54 | describe('will modify scaffold files', function() { 55 | 56 | it('will give an error if expected files do not exist', function(done) { 57 | add({ 58 | path: path.resolve(testDir, 's0'), 59 | services: ['a', 'b', 'c'] 60 | }, function(err) { 61 | should.exist(err); 62 | err.message.match(/^Invalid state/); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('will receive error from `npm install`', function(done) { 68 | var spawn = sinon.stub().returns({ 69 | stdout: { 70 | on: sinon.stub() 71 | }, 72 | stderr: { 73 | on: sinon.stub() 74 | }, 75 | on: sinon.stub().callsArgWith(1, 1) 76 | }); 77 | var addtest = proxyquire('../../lib/scaffold/add', { 78 | 'child_process': { 79 | spawn: spawn 80 | } 81 | }); 82 | 83 | addtest({ 84 | path: path.resolve(testDir, 's0/s1/'), 85 | services: ['a', 'b', 'c'] 86 | }, function(err) { 87 | should.exist(err); 88 | err.message.should.equal('There was an error installing service: a'); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('will update bch-node.json services', function(done) { 94 | var callCount = 0; 95 | var oldPackage = { 96 | dependencies: { 97 | 'bch-lib': '^v0.13.7', 98 | 'bch-node': '^v0.2.0' 99 | } 100 | }; 101 | var spawn = sinon.stub().returns({ 102 | stdout: { 103 | on: sinon.stub() 104 | }, 105 | stderr: { 106 | on: sinon.stub() 107 | }, 108 | on: sinon.stub().callsArgWith(1, 0) 109 | }); 110 | var addtest = proxyquire('../../lib/scaffold/add', { 111 | 'child_process': { 112 | spawn: spawn 113 | }, 114 | 'fs': { 115 | readFileSync: function() { 116 | if (callCount === 1){ 117 | oldPackage.dependencies.a = '^v0.1'; 118 | } else if (callCount === 2){ 119 | oldPackage.dependencies.b = '^v0.1'; 120 | } else if (callCount === 3){ 121 | oldPackage.dependencies.c = '^v0.1'; 122 | } 123 | callCount++; 124 | return JSON.stringify(oldPackage); 125 | } 126 | } 127 | }); 128 | addtest({ 129 | path: path.resolve(testDir, 's0/s1/'), 130 | services: ['a', 'b', 'c'] 131 | }, function(err) { 132 | should.not.exist(err); 133 | var configPath = path.resolve(testDir, 's0/s1/bch-node.json'); 134 | var config = JSON.parse(fs.readFileSync(configPath)); 135 | config.services.should.deep.equal(['a','b','c']); 136 | done(); 137 | }); 138 | }); 139 | 140 | }); 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /test/scaffold/call-method.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var sinon = require('sinon'); 5 | var proxyquire = require('proxyquire'); 6 | var EventEmitter = require('events').EventEmitter; 7 | 8 | describe('#callMethod', function() { 9 | 10 | var expectedUrl = 'http://localhost:3001'; 11 | var expectedOptions = { 12 | reconnection: false, 13 | connect_timeout: 5000 14 | }; 15 | 16 | var callOptions = { 17 | host: 'localhost', 18 | port: 3001, 19 | protocol: 'http' 20 | }; 21 | 22 | var callMethod; 23 | 24 | before(function() { 25 | callMethod = proxyquire('../../lib/scaffold/call-method', { 26 | 'socket.io-client': function(url, options) { 27 | url.should.equal(expectedUrl); 28 | options.should.deep.equal(expectedOptions); 29 | return new EventEmitter(); 30 | } 31 | }); 32 | }); 33 | 34 | it('handle a connection error', function(done) { 35 | var socket = callMethod(callOptions, 'getInfo', null, function(err) { 36 | should.exist(err); 37 | err.message.should.equal('connect'); 38 | done(); 39 | }); 40 | socket.emit('connect_error', new Error('connect')); 41 | }); 42 | 43 | it('give an error response', function(done) { 44 | var socket = callMethod(callOptions, 'getInfo', null, function(err) { 45 | should.exist(err); 46 | err.message.should.equal('response'); 47 | done(); 48 | }); 49 | socket.send = function(opts, callback) { 50 | opts.method.should.equal('getInfo'); 51 | should.equal(opts.params, null); 52 | var response = { 53 | error: { 54 | message: 'response' 55 | } 56 | }; 57 | callback(response); 58 | }; 59 | socket.emit('connect'); 60 | }); 61 | 62 | it('give result and close socket', function(done) { 63 | var expectedData = { 64 | version: 110000, 65 | protocolversion: 70002, 66 | blocks: 258614, 67 | timeoffset: -2, 68 | connections: 8, 69 | difficulty: 112628548.66634709, 70 | testnet: false, 71 | relayfee: 1000, 72 | errors: '' 73 | }; 74 | var socket = callMethod(callOptions, 'getInfo', null, function(err, data) { 75 | should.not.exist(err); 76 | data.should.deep.equal(expectedData); 77 | socket.close.callCount.should.equal(1); 78 | done(); 79 | }); 80 | socket.close = sinon.stub(); 81 | socket.send = function(opts, callback) { 82 | opts.method.should.equal('getInfo'); 83 | should.equal(opts.params, null); 84 | var response = { 85 | error: null, 86 | result: expectedData 87 | }; 88 | callback(response); 89 | }; 90 | socket.emit('connect'); 91 | }); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /test/scaffold/create.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var proxyquire = require('proxyquire'); 5 | var sinon = require('sinon'); 6 | var create = proxyquire('../../lib/scaffold/create', { 7 | 'child_process': { 8 | spawn: sinon.stub().returns({ 9 | stdout: { 10 | on: sinon.stub() 11 | }, 12 | stderr: { 13 | on: sinon.stub() 14 | }, 15 | on: function(event, cb) { 16 | cb(0); 17 | } 18 | }) 19 | } 20 | }); 21 | var fs = require('fs'); 22 | var mkdirp = require('mkdirp'); 23 | var rimraf = require('rimraf'); 24 | 25 | describe('#create', function() { 26 | 27 | var basePath = __dirname + '/../'; 28 | var testDir = basePath + 'temporary-test-data'; 29 | 30 | before(function(done) { 31 | // setup testing directories 32 | mkdirp(testDir, function(err) { 33 | if (err) { 34 | throw err; 35 | } 36 | mkdirp(testDir + '/.bitcoin', function(err) { 37 | if (err) { 38 | throw err; 39 | } 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | after(function(done) { 46 | // cleanup testing directories 47 | rimraf(testDir, function(err) { 48 | if (err) { 49 | throw err; 50 | } 51 | done(); 52 | }); 53 | }); 54 | 55 | it('will create scaffold files', function() { 56 | create({ 57 | cwd: testDir, 58 | dirname: 'mynode', 59 | name: 'My Node 1', 60 | isGlobal: false, 61 | datadir: './data' 62 | }, function(err) { 63 | if (err) { 64 | throw err; 65 | } 66 | 67 | var configPath = testDir + '/mynode/bch-node.json'; 68 | var packagePath = testDir + '/mynode/package.json'; 69 | 70 | should.equal(fs.existsSync(configPath), true); 71 | should.equal(fs.existsSync(packagePath), true); 72 | 73 | var config = JSON.parse(fs.readFileSync(configPath)); 74 | config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); 75 | config.datadir.should.equal('./data'); 76 | config.network.should.equal('livenet'); 77 | 78 | var pack = JSON.parse(fs.readFileSync(packagePath)); 79 | should.exist(pack.dependencies); 80 | 81 | }); 82 | 83 | }); 84 | 85 | it('will error if directory already exists', function() { 86 | 87 | create({ 88 | cwd: testDir, 89 | dirname: 'mynode', 90 | name: 'My Node 2', 91 | isGlobal: false, 92 | datadir: './data' 93 | }, function(err) { 94 | should.exist(err); 95 | err.message.should.match(/^Directory/); 96 | }); 97 | 98 | }); 99 | 100 | it('will not create a package.json if globally installed', function() { 101 | 102 | create({ 103 | cwd: testDir, 104 | dirname: 'mynode3', 105 | name: 'My Node 3', 106 | isGlobal: true, 107 | datadir: '../.bitcoin' 108 | }, function(err) { 109 | if (err) { 110 | throw err; 111 | } 112 | 113 | var packagePath = testDir + '/mynode3/package.json'; 114 | should.equal(fs.existsSync(packagePath), false); 115 | 116 | }); 117 | 118 | }); 119 | 120 | it('will receieve an error from npm', function() { 121 | var createtest = proxyquire('../../lib/scaffold/create', { 122 | 'child_process': { 123 | spawn: sinon.stub().returns({ 124 | stdout: { 125 | on: sinon.stub() 126 | }, 127 | stderr: { 128 | on: sinon.stub() 129 | }, 130 | on: function(event, cb) { 131 | cb(1); 132 | } 133 | }) 134 | } 135 | }); 136 | 137 | createtest({ 138 | cwd: testDir, 139 | dirname: 'mynode4', 140 | name: 'My Node 4', 141 | isGlobal: false, 142 | datadir: '../.bitcoin' 143 | }, function(err) { 144 | should.exist(err); 145 | err.message.should.equal('There was an error installing dependencies.'); 146 | }); 147 | 148 | }); 149 | 150 | }); 151 | -------------------------------------------------------------------------------- /test/scaffold/default-base-config.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var path = require('path'); 5 | var defaultBaseConfig = require('../../lib/scaffold/default-base-config'); 6 | 7 | describe('#defaultBaseConfig', function() { 8 | it('will return expected configuration', function() { 9 | var cwd = process.cwd(); 10 | var home = process.env.HOME; 11 | var info = defaultBaseConfig(); 12 | info.path.should.equal(cwd); 13 | info.config.network.should.equal('livenet'); 14 | info.config.port.should.equal(3001); 15 | info.config.services.should.deep.equal(['bitcoind', 'web']); 16 | var bitcoind = info.config.servicesConfig.bitcoind; 17 | bitcoind.spawn.datadir.should.equal(home + '/.bitcoin'); 18 | bitcoind.spawn.exec.should.equal(path.resolve(__dirname, '../../bin/bitcoind')); 19 | }); 20 | it('be able to specify a network', function() { 21 | var info = defaultBaseConfig({network: 'testnet'}); 22 | info.config.network.should.equal('testnet'); 23 | }); 24 | it('be able to specify a datadir', function() { 25 | var info = defaultBaseConfig({datadir: './data2', network: 'testnet'}); 26 | info.config.servicesConfig.bitcoind.spawn.datadir.should.equal('./data2'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/scaffold/default-config.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var should = require('chai').should(); 5 | var sinon = require('sinon'); 6 | var proxyquire = require('proxyquire'); 7 | 8 | describe('#defaultConfig', function() { 9 | var expectedExecPath = path.resolve(__dirname, '../../bin/bitcoind'); 10 | 11 | it('will return expected configuration', function() { 12 | var config = JSON.stringify({ 13 | network: 'livenet', 14 | port: 3001, 15 | services: [ 16 | 'bitcoind', 17 | 'web' 18 | ], 19 | servicesConfig: { 20 | bitcoind: { 21 | spawn: { 22 | datadir: process.env.HOME + '/.bch/data', 23 | exec: expectedExecPath 24 | } 25 | } 26 | } 27 | }, null, 2); 28 | var defaultConfig = proxyquire('../../lib/scaffold/default-config', { 29 | fs: { 30 | existsSync: sinon.stub().returns(false), 31 | writeFileSync: function(path, data) { 32 | path.should.equal(process.env.HOME + '/.bch/bch-node.json'); 33 | data.should.equal(config); 34 | }, 35 | readFileSync: function() { 36 | return config; 37 | } 38 | }, 39 | mkdirp: { 40 | sync: sinon.stub() 41 | } 42 | }); 43 | var home = process.env.HOME; 44 | var info = defaultConfig(); 45 | info.path.should.equal(home + '/.bch'); 46 | info.config.network.should.equal('livenet'); 47 | info.config.port.should.equal(3001); 48 | info.config.services.should.deep.equal(['bitcoind', 'web']); 49 | var bitcoind = info.config.servicesConfig.bitcoind; 50 | should.exist(bitcoind); 51 | bitcoind.spawn.datadir.should.equal(home + '/.bch/data'); 52 | bitcoind.spawn.exec.should.equal(expectedExecPath); 53 | }); 54 | it('will include additional services', function() { 55 | var config = JSON.stringify({ 56 | network: 'livenet', 57 | port: 3001, 58 | services: [ 59 | 'bitcoind', 60 | 'web', 61 | 'explorer-api', 62 | 'ows-explorer' 63 | ], 64 | servicesConfig: { 65 | bitcoind: { 66 | spawn: { 67 | datadir: process.env.HOME + '/.bch/data', 68 | exec: expectedExecPath 69 | } 70 | } 71 | } 72 | }, null, 2); 73 | var defaultConfig = proxyquire('../../lib/scaffold/default-config', { 74 | fs: { 75 | existsSync: sinon.stub().returns(false), 76 | writeFileSync: function(path, data) { 77 | path.should.equal(process.env.HOME + '/.bch/bch-node.json'); 78 | data.should.equal(config); 79 | }, 80 | readFileSync: function() { 81 | return config; 82 | } 83 | }, 84 | mkdirp: { 85 | sync: sinon.stub() 86 | } 87 | }); 88 | var home = process.env.HOME; 89 | var info = defaultConfig({ 90 | additionalServices: ['explorer-api', 'ows-explorer'] 91 | }); 92 | info.path.should.equal(home + '/.bch'); 93 | info.config.network.should.equal('livenet'); 94 | info.config.port.should.equal(3001); 95 | info.config.services.should.deep.equal([ 96 | 'bitcoind', 97 | 'web', 98 | 'explorer-api', 99 | 'ows-explorer' 100 | ]); 101 | var bitcoind = info.config.servicesConfig.bitcoind; 102 | should.exist(bitcoind); 103 | bitcoind.spawn.datadir.should.equal(home + '/.bch/data'); 104 | bitcoind.spawn.exec.should.equal(expectedExecPath); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/scaffold/find-config.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var should = require('chai').should(); 6 | var sinon = require('sinon'); 7 | var mkdirp = require('mkdirp'); 8 | var rimraf = require('rimraf'); 9 | 10 | var findConfig = require('../../lib/scaffold/find-config'); 11 | 12 | describe('#findConfig', function() { 13 | 14 | var testDir = path.resolve(__dirname, '../temporary-test-data'); 15 | var expectedConfig = { 16 | name: 'My Node' 17 | }; 18 | 19 | before(function(done) { 20 | // setup testing directories 21 | mkdirp(testDir + '/p2/p1/p0', function(err) { 22 | if (err) { 23 | throw err; 24 | } 25 | fs.writeFile( 26 | testDir + '/p2/bch-node.json', 27 | JSON.stringify(expectedConfig), 28 | function() { 29 | mkdirp(testDir + '/e0', function(err) { 30 | if (err) { 31 | throw err; 32 | } 33 | done(); 34 | }); 35 | } 36 | ); 37 | }); 38 | 39 | }); 40 | 41 | after(function(done) { 42 | // cleanup testing directories 43 | rimraf(testDir, function(err) { 44 | if (err) { 45 | throw err; 46 | } 47 | done(); 48 | }); 49 | }); 50 | 51 | 52 | describe('will find a configuration file', function() { 53 | 54 | it('in the current directory', function() { 55 | var config = findConfig(path.resolve(testDir, 'p2')); 56 | config.path.should.equal(path.resolve(testDir, 'p2')); 57 | config.config.should.deep.equal(expectedConfig); 58 | }); 59 | 60 | it('in a parent directory', function() { 61 | var config = findConfig(path.resolve(testDir, 'p2/p1')); 62 | config.path.should.equal(path.resolve(testDir, 'p2')); 63 | config.config.should.deep.equal(expectedConfig); 64 | }); 65 | 66 | it('recursively find in parent directories', function() { 67 | var config = findConfig(path.resolve(testDir, 'p2/p1/p0')); 68 | config.path.should.equal(path.resolve(testDir, 'p2')); 69 | config.config.should.deep.equal(expectedConfig); 70 | }); 71 | 72 | }); 73 | 74 | it('will return false if missing a configuration', function() { 75 | var config = findConfig(path.resolve(testDir, 'e0')); 76 | config.should.equal(false); 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /test/scaffold/remove.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var sinon = require('sinon'); 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | var proxyquire = require('proxyquire'); 8 | var mkdirp = require('mkdirp'); 9 | var rimraf = require('rimraf'); 10 | var remove = require('../../lib/scaffold/remove'); 11 | 12 | describe('#remove', function() { 13 | 14 | var basePath = path.resolve(__dirname, '..'); 15 | var testDir = path.resolve(basePath, 'temporary-test-data'); 16 | var startConfig = { 17 | name: 'My Node', 18 | services: ['a', 'b', 'c'] 19 | }; 20 | var startPackage = {}; 21 | 22 | before(function(done) { 23 | mkdirp(testDir + '/s0/s1', function(err) { 24 | if (err) { 25 | throw err; 26 | } 27 | fs.writeFile( 28 | testDir + '/s0/s1/bch-node.json', 29 | JSON.stringify(startConfig), 30 | function(err) { 31 | if (err) { 32 | throw err; 33 | } 34 | fs.writeFile( 35 | testDir + '/s0/s1/package.json', 36 | JSON.stringify(startPackage), 37 | done 38 | ); 39 | } 40 | ); 41 | }); 42 | }); 43 | 44 | after(function(done) { 45 | // cleanup testing directories 46 | rimraf(testDir, function(err) { 47 | if (err) { 48 | throw err; 49 | } 50 | done(); 51 | }); 52 | }); 53 | 54 | describe('will modify scaffold files', function() { 55 | 56 | it('will give an error if expected files do not exist', function(done) { 57 | remove({ 58 | path: path.resolve(testDir, 's0'), 59 | services: ['b'] 60 | }, function(err) { 61 | should.exist(err); 62 | err.message.match(/^Invalid state/); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('will update bch-node.json services', function(done) { 68 | var spawn = sinon.stub().returns({ 69 | stdout: { 70 | on: sinon.stub() 71 | }, 72 | stderr: { 73 | on: sinon.stub() 74 | }, 75 | on: sinon.stub().callsArgWith(1, 0) 76 | }); 77 | var removetest = proxyquire('../../lib/scaffold/remove', { 78 | 'child_process': { 79 | spawn: spawn 80 | }, 81 | 'npm': { 82 | load: sinon.stub().callsArg(0), 83 | commands: { 84 | ls: sinon.stub().callsArgWith(2, null, {}, { 85 | dependencies: {} 86 | }) 87 | } 88 | } 89 | }); 90 | removetest({ 91 | path: path.resolve(testDir, 's0/s1/'), 92 | services: ['b'] 93 | }, function(err) { 94 | should.not.exist(err); 95 | var configPath = path.resolve(testDir, 's0/s1/bch-node.json'); 96 | var config = JSON.parse(fs.readFileSync(configPath)); 97 | config.services.should.deep.equal(['a', 'c']); 98 | done(); 99 | }); 100 | }); 101 | 102 | it('will receive error from `npm uninstall`', function(done) { 103 | var spawn = sinon.stub().returns({ 104 | stdout: { 105 | on: sinon.stub() 106 | }, 107 | stderr: { 108 | on: sinon.stub() 109 | }, 110 | on: sinon.stub().callsArgWith(1, 1) 111 | }); 112 | var removetest = proxyquire('../../lib/scaffold/remove', { 113 | 'child_process': { 114 | spawn: spawn 115 | }, 116 | 'npm': { 117 | load: sinon.stub().callsArg(0), 118 | commands: { 119 | ls: sinon.stub().callsArgWith(2, null, {}, { 120 | dependencies: {} 121 | }) 122 | } 123 | } 124 | }); 125 | 126 | removetest({ 127 | path: path.resolve(testDir, 's0/s1/'), 128 | services: ['b'] 129 | }, function(err) { 130 | should.exist(err); 131 | err.message.should.equal('There was an error uninstalling service(s): b'); 132 | done(); 133 | }); 134 | }); 135 | 136 | }); 137 | 138 | }); 139 | -------------------------------------------------------------------------------- /test/scaffold/start.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var sinon = require('sinon'); 5 | var proxyquire = require('proxyquire'); 6 | var BitcoinService = require('../../lib/services/bitcoind'); 7 | var index = require('../../lib'); 8 | var log = index.log; 9 | 10 | describe('#start', function() { 11 | 12 | var sandbox = sinon.sandbox.create(); 13 | beforeEach(function() { 14 | sandbox.stub(log, 'error'); 15 | }); 16 | afterEach(function() { 17 | sandbox.restore(); 18 | }); 19 | 20 | describe('will dynamically create a node from a configuration', function() { 21 | 22 | it('require each bch-node service with default config', function(done) { 23 | var node; 24 | var TestNode = function(options) { 25 | options.services[0].should.deep.equal({ 26 | name: 'bitcoind', 27 | moduleName: 'bitcoind', 28 | module: BitcoinService, 29 | config: { 30 | spawn: { 31 | datadir: './data' 32 | } 33 | } 34 | }); 35 | }; 36 | TestNode.prototype.start = sinon.stub().callsArg(0); 37 | TestNode.prototype.on = sinon.stub(); 38 | TestNode.prototype.chain = { 39 | on: sinon.stub() 40 | }; 41 | 42 | var starttest = proxyquire('../../lib/scaffold/start', { 43 | '../node': TestNode 44 | }); 45 | 46 | starttest.registerExitHandlers = sinon.stub(); 47 | 48 | node = starttest({ 49 | path: __dirname, 50 | config: { 51 | services: [ 52 | 'bitcoind' 53 | ], 54 | servicesConfig: { 55 | bitcoind: { 56 | spawn: { 57 | datadir: './data' 58 | } 59 | } 60 | } 61 | } 62 | }); 63 | node.should.be.instanceof(TestNode); 64 | done(); 65 | }); 66 | it('shutdown with an error from start', function(done) { 67 | var TestNode = proxyquire('../../lib/node', {}); 68 | TestNode.prototype.start = function(callback) { 69 | setImmediate(function() { 70 | callback(new Error('error')); 71 | }); 72 | }; 73 | var starttest = proxyquire('../../lib/scaffold/start', { 74 | '../node': TestNode 75 | }); 76 | starttest.cleanShutdown = sinon.stub(); 77 | starttest.registerExitHandlers = sinon.stub(); 78 | 79 | starttest({ 80 | path: __dirname, 81 | config: { 82 | services: [], 83 | servicesConfig: {} 84 | } 85 | }); 86 | setImmediate(function() { 87 | starttest.cleanShutdown.callCount.should.equal(1); 88 | done(); 89 | }); 90 | }); 91 | it('require each bch-node service with explicit config', function(done) { 92 | var node; 93 | var TestNode = function(options) { 94 | options.services[0].should.deep.equal({ 95 | name: 'bitcoind', 96 | moduleName: 'bitcoind', 97 | module: BitcoinService, 98 | config: { 99 | param: 'test', 100 | spawn: { 101 | datadir: './data' 102 | } 103 | } 104 | }); 105 | }; 106 | TestNode.prototype.start = sinon.stub().callsArg(0); 107 | TestNode.prototype.on = sinon.stub(); 108 | TestNode.prototype.chain = { 109 | on: sinon.stub() 110 | }; 111 | 112 | var starttest = proxyquire('../../lib/scaffold/start', { 113 | '../node': TestNode 114 | }); 115 | starttest.registerExitHandlers = sinon.stub(); 116 | 117 | node = starttest({ 118 | path: __dirname, 119 | config: { 120 | services: [ 121 | 'bitcoind' 122 | ], 123 | servicesConfig: { 124 | 'bitcoind': { 125 | param: 'test', 126 | spawn: { 127 | datadir: './data' 128 | } 129 | } 130 | }, 131 | 132 | } 133 | }); 134 | node.should.be.instanceof(TestNode); 135 | done(); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/scaffold/start.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var EventEmitter = require('events').EventEmitter; 5 | var path = require('path'); 6 | var sinon = require('sinon'); 7 | var proxyquire = require('proxyquire'); 8 | var start = require('../../lib/scaffold/start'); 9 | 10 | describe('#start', function() { 11 | describe('#checkConfigVersion2', function() { 12 | var sandbox = sinon.sandbox.create(); 13 | beforeEach(function() { 14 | sandbox.stub(console, 'warn'); 15 | }); 16 | afterEach(function() { 17 | sandbox.restore(); 18 | }); 19 | it('will give true with "datadir" at root', function() { 20 | var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2; 21 | var v2 = checkConfigVersion2({datadir: '/home/user/.bch/data', services: []}); 22 | v2.should.equal(true); 23 | }); 24 | it('will give true with "address" service enabled', function() { 25 | var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2; 26 | var v2 = checkConfigVersion2({services: ['address']}); 27 | v2.should.equal(true); 28 | }); 29 | it('will give true with "db" service enabled', function() { 30 | var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2; 31 | var v2 = checkConfigVersion2({services: ['db']}); 32 | v2.should.equal(true); 33 | }); 34 | it('will give false without "datadir" at root and "address", "db" services disabled', function() { 35 | var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2; 36 | var v2 = checkConfigVersion2({services: []}); 37 | v2.should.equal(false); 38 | }); 39 | }); 40 | describe('#setupServices', function() { 41 | var cwd = process.cwd(); 42 | var setupServices = proxyquire('../../lib/scaffold/start', {}).setupServices; 43 | it('will require an internal module', function() { 44 | function InternalService() {} 45 | InternalService.dependencies = []; 46 | InternalService.prototype.start = sinon.stub(); 47 | InternalService.prototype.stop = sinon.stub(); 48 | var expectedPath = path.resolve(__dirname, '../../lib/services/internal'); 49 | var testRequire = function(p) { 50 | p.should.equal(expectedPath); 51 | return InternalService; 52 | }; 53 | var config = { 54 | services: ['internal'], 55 | servicesConfig: { 56 | internal: { 57 | param: 'value' 58 | } 59 | } 60 | }; 61 | var services = setupServices(testRequire, cwd, config); 62 | services[0].name.should.equal('internal'); 63 | services[0].config.should.deep.equal({param: 'value'}); 64 | services[0].module.should.equal(InternalService); 65 | }); 66 | it('will require a local module', function() { 67 | function LocalService() {} 68 | LocalService.dependencies = []; 69 | LocalService.prototype.start = sinon.stub(); 70 | LocalService.prototype.stop = sinon.stub(); 71 | var notfoundPath = path.resolve(__dirname, '../../lib/services/local'); 72 | var testRequire = function(p) { 73 | if (p === notfoundPath) { 74 | throw new Error(); 75 | } else if (p === 'local') { 76 | return LocalService; 77 | } else if (p === 'local/package.json') { 78 | return { 79 | name: 'local' 80 | }; 81 | } 82 | }; 83 | var config = { 84 | services: ['local'] 85 | }; 86 | var services = setupServices(testRequire, cwd, config); 87 | services[0].name.should.equal('local'); 88 | services[0].module.should.equal(LocalService); 89 | }); 90 | it('will require a local module with "owsNode" in package.json', function() { 91 | function LocalService() {} 92 | LocalService.dependencies = []; 93 | LocalService.prototype.start = sinon.stub(); 94 | LocalService.prototype.stop = sinon.stub(); 95 | var notfoundPath = path.resolve(__dirname, '../../lib/services/local'); 96 | var testRequire = function(p) { 97 | if (p === notfoundPath) { 98 | throw new Error(); 99 | } else if (p === 'local/package.json') { 100 | return { 101 | name: 'local', 102 | owsNode: 'lib/owsNode.js' 103 | }; 104 | } else if (p === 'local/lib/owsNode.js') { 105 | return LocalService; 106 | } 107 | }; 108 | var config = { 109 | services: ['local'] 110 | }; 111 | var services = setupServices(testRequire, cwd, config); 112 | services[0].name.should.equal('local'); 113 | services[0].module.should.equal(LocalService); 114 | }); 115 | it('will throw error if module is incompatible', function() { 116 | var internal = {}; 117 | var testRequire = function() { 118 | return internal; 119 | }; 120 | var config = { 121 | services: ['bitcoind'] 122 | }; 123 | (function() { 124 | setupServices(testRequire, cwd, config); 125 | }).should.throw('Could not load service'); 126 | }); 127 | }); 128 | describe('#cleanShutdown', function() { 129 | it('will call node stop and process exit', function() { 130 | var log = { 131 | info: sinon.stub(), 132 | error: sinon.stub() 133 | }; 134 | var cleanShutdown = proxyquire('../../lib/scaffold/start', { 135 | '../': { 136 | log: log 137 | } 138 | }).cleanShutdown; 139 | var node = { 140 | stop: sinon.stub().callsArg(0) 141 | }; 142 | var _process = { 143 | exit: sinon.stub() 144 | }; 145 | cleanShutdown(_process, node); 146 | setImmediate(function() { 147 | node.stop.callCount.should.equal(1); 148 | _process.exit.callCount.should.equal(1); 149 | _process.exit.args[0][0].should.equal(0); 150 | }); 151 | }); 152 | it('will log error during shutdown and exit with status 1', function() { 153 | var log = { 154 | info: sinon.stub(), 155 | error: sinon.stub() 156 | }; 157 | var cleanShutdown = proxyquire('../../lib/scaffold/start', { 158 | '../': { 159 | log: log 160 | } 161 | }).cleanShutdown; 162 | var node = { 163 | stop: sinon.stub().callsArgWith(0, new Error('test')) 164 | }; 165 | var _process = { 166 | exit: sinon.stub() 167 | }; 168 | cleanShutdown(_process, node); 169 | setImmediate(function() { 170 | node.stop.callCount.should.equal(1); 171 | log.error.callCount.should.equal(1); 172 | _process.exit.callCount.should.equal(1); 173 | _process.exit.args[0][0].should.equal(1); 174 | }); 175 | }); 176 | }); 177 | describe('#registerExitHandlers', function() { 178 | var log = { 179 | info: sinon.stub(), 180 | error: sinon.stub() 181 | }; 182 | var registerExitHandlers = proxyquire('../../lib/scaffold/start', { 183 | '../': { 184 | log: log 185 | } 186 | }).registerExitHandlers; 187 | it('log, stop and exit with an `uncaughtException`', function(done) { 188 | var proc = new EventEmitter(); 189 | proc.exit = sinon.stub(); 190 | var node = { 191 | stop: sinon.stub().callsArg(0) 192 | }; 193 | registerExitHandlers(proc, node); 194 | proc.emit('uncaughtException', new Error('test')); 195 | setImmediate(function() { 196 | node.stop.callCount.should.equal(1); 197 | proc.exit.callCount.should.equal(1); 198 | done(); 199 | }); 200 | }); 201 | it('stop and exit on `SIGINT`', function(done) { 202 | var proc = new EventEmitter(); 203 | proc.exit = sinon.stub(); 204 | var node = { 205 | stop: sinon.stub().callsArg(0) 206 | }; 207 | registerExitHandlers(proc, node); 208 | proc.emit('SIGINT'); 209 | setImmediate(function() { 210 | node.stop.callCount.should.equal(1); 211 | proc.exit.callCount.should.equal(1); 212 | done(); 213 | }); 214 | }); 215 | }); 216 | describe('#registerExitHandlers', function() { 217 | var stub; 218 | var registerExitHandlers = require('../../lib/scaffold/start').registerExitHandlers; 219 | 220 | before(function() { 221 | stub = sinon.stub(process, 'on'); 222 | }); 223 | 224 | after(function() { 225 | stub.restore(); 226 | }); 227 | 228 | it('should setup two listeners on process when registering exit handlers', function() { 229 | registerExitHandlers(process, {}); 230 | stub.callCount.should.equal(2); 231 | }); 232 | 233 | describe('#exitHandler', function() { 234 | var sandbox; 235 | var cleanShutdown; 236 | var exitHandler; 237 | var logStub; 238 | 239 | before(function() { 240 | sandbox = sinon.sandbox.create(); 241 | var start = require('../../lib/scaffold/start'); 242 | var log = require('../../lib').log; 243 | logStub = sandbox.stub(log, 'error'); 244 | cleanShutdown = sandbox.stub(start, 'cleanShutdown'); 245 | exitHandler = require('../../lib/scaffold/start').exitHandler; 246 | }); 247 | 248 | after(function() { 249 | sandbox.restore(); 250 | }); 251 | 252 | it('should replace the listener for SIGINT after the first SIGINT is handled', function() { 253 | var options = { sigint: true }; 254 | var node = {}; 255 | exitHandler(options, process, node); 256 | cleanShutdown.callCount.should.equal(1); 257 | exitHandler(options, process, node); 258 | cleanShutdown.callCount.should.equal(1); 259 | }); 260 | 261 | it('should log all errors and stops the services nonetheless', function() { 262 | var options = { sigint: true }; 263 | var stop = sinon.stub(); 264 | var node = { 265 | stop: stop 266 | }; 267 | exitHandler(options, process, node, new Error('some error')); 268 | logStub.callCount.should.equal(2); 269 | stop.callCount.should.equal(1); 270 | }); 271 | 272 | }); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /test/services/web.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var sinon = require('sinon'); 5 | var EventEmitter = require('events').EventEmitter; 6 | var proxyquire = require('proxyquire'); 7 | 8 | var index = require('../../lib'); 9 | var log = index.log; 10 | 11 | var httpStub = { 12 | createServer: sinon.spy() 13 | }; 14 | var httpsStub = { 15 | createServer: sinon.spy() 16 | }; 17 | var fsStub = { 18 | readFileSync: function(arg1) { 19 | return arg1 + '-buffer'; 20 | } 21 | }; 22 | 23 | var fakeSocketListener = new EventEmitter(); 24 | var fakeSocket = new EventEmitter(); 25 | 26 | fakeSocket.on('test/event1', function(data) { 27 | data.should.equal('testdata'); 28 | }); 29 | 30 | fakeSocketListener.emit('connection', fakeSocket); 31 | fakeSocket.emit('subscribe', 'test/event1'); 32 | 33 | var WebService = proxyquire('../../lib/services/web', {http: httpStub, https: httpsStub, fs: fsStub}); 34 | 35 | describe('WebService', function() { 36 | var defaultNode = new EventEmitter(); 37 | 38 | describe('@constructor', function() { 39 | it('will set socket rpc settings', function() { 40 | var web = new WebService({node: defaultNode, enableSocketRPC: false}); 41 | web.enableSocketRPC.should.equal(false); 42 | 43 | var web2 = new WebService({node: defaultNode, enableSocketRPC: true}); 44 | web2.enableSocketRPC.should.equal(true); 45 | 46 | var web3 = new WebService({node: defaultNode}); 47 | web3.enableSocketRPC.should.equal(WebService.DEFAULT_SOCKET_RPC); 48 | }); 49 | it('will set configuration options for max payload', function() { 50 | var web = new WebService({node: defaultNode, jsonRequestLimit: '200kb'}); 51 | web.jsonRequestLimit.should.equal('200kb'); 52 | }); 53 | }); 54 | 55 | describe('#start', function() { 56 | beforeEach(function() { 57 | httpStub.createServer.reset(); 58 | httpsStub.createServer.reset(); 59 | }); 60 | it('should create an http server if no options are specified and node is not configured for https', function(done) { 61 | var web = new WebService({node: defaultNode}); 62 | web.deriveHttpsOptions = sinon.spy(); 63 | web.start(function(err) { 64 | should.not.exist(err); 65 | httpStub.createServer.called.should.equal(true); 66 | done(); 67 | }); 68 | }); 69 | 70 | it('should create an https server if no options are specified and node is configured for https', function(done) { 71 | var node = new EventEmitter(); 72 | node.https = true; 73 | 74 | var web = new WebService({node: node}); 75 | web.transformHttpsOptions = sinon.spy(); 76 | web.start(function(err) { 77 | should.not.exist(err); 78 | httpsStub.createServer.called.should.equal(true); 79 | done(); 80 | }); 81 | }); 82 | it('should pass json request limit to json body parser', function(done) { 83 | var node = new EventEmitter(); 84 | var jsonStub = sinon.stub(); 85 | var TestWebService = proxyquire('../../lib/services/web', { 86 | http: { 87 | createServer: sinon.stub() 88 | }, 89 | https: { 90 | createServer: sinon.stub() 91 | }, 92 | fs: fsStub, 93 | express: sinon.stub().returns({ 94 | use: sinon.stub() 95 | }), 96 | 'body-parser': { 97 | json: jsonStub 98 | }, 99 | 'socket.io': { 100 | listen: sinon.stub().returns({ 101 | on: sinon.stub() 102 | }) 103 | } 104 | }); 105 | var web = new TestWebService({node: node}); 106 | web.start(function(err) { 107 | if (err) { 108 | return done(err); 109 | } 110 | jsonStub.callCount.should.equal(1); 111 | jsonStub.args[0][0].limit.should.equal('100kb'); 112 | done(); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('#stop', function() { 118 | it('should close the server if it exists', function(done) { 119 | var web = new WebService({node: defaultNode}); 120 | web.server = { 121 | close: sinon.spy() 122 | }; 123 | 124 | web.stop(function(err) { 125 | should.not.exist(err); 126 | web.server.close.callCount.should.equal(1); 127 | done(); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('#setupAllRoutes', function() { 133 | it('should call setupRoutes on each module', function() { 134 | var node = { 135 | on: sinon.spy(), 136 | services: { 137 | one: { 138 | setupRoutes: sinon.spy(), 139 | getRoutePrefix: sinon.stub().returns('one') 140 | }, 141 | two: { 142 | setupRoutes: sinon.spy(), 143 | getRoutePrefix: sinon.stub().returns('two') 144 | } 145 | } 146 | }; 147 | 148 | var web = new WebService({node: node}); 149 | web.app = { 150 | use: sinon.spy() 151 | }; 152 | 153 | web.setupAllRoutes(); 154 | node.services.one.setupRoutes.callCount.should.equal(1); 155 | should.exist(node.services.one.setupRoutes.args[0][0].engine); 156 | should.exist(node.services.one.setupRoutes.args[0][0].get); 157 | should.exist(node.services.one.setupRoutes.args[0][0].post); 158 | should.exist(node.services.one.setupRoutes.args[0][0].set); 159 | node.services.two.setupRoutes.callCount.should.equal(1); 160 | }); 161 | }); 162 | 163 | describe('#createMethodsMap', function() { 164 | it('should create the methodsMap correctly', function(done) { 165 | var Module1 = function() {}; 166 | Module1.prototype.getAPIMethods = function() { 167 | return [ 168 | ['one', this, this.one, 1], 169 | ['two', this, this.two, 2] 170 | ]; 171 | }; 172 | Module1.prototype.one = function(param1, callback) { 173 | callback(null, param1); 174 | }; 175 | Module1.prototype.two = function(param1, param2, callback) { 176 | callback(null, param1 + param2); 177 | }; 178 | 179 | var module1 = new Module1(); 180 | 181 | var node = { 182 | on: sinon.spy(), 183 | getAllAPIMethods: sinon.stub().returns(module1.getAPIMethods()) 184 | }; 185 | 186 | var web = new WebService({node: node}); 187 | web.createMethodsMap(); 188 | 189 | Object.keys(web.methodsMap).length.should.equal(2); 190 | web.methodsMap.one.args.should.equal(1); 191 | web.methodsMap.two.args.should.equal(2); 192 | web.methodsMap.one.fn(1, function(err, result) { 193 | should.not.exist(err); 194 | result.should.equal(1); 195 | 196 | web.methodsMap.two.fn(1, 2, function(err, result) { 197 | should.not.exist(err); 198 | result.should.equal(3); 199 | done(); 200 | }); 201 | }); 202 | }); 203 | }); 204 | 205 | describe('#getEventNames', function() { 206 | it('should get event names', function() { 207 | var Module1 = function() {}; 208 | Module1.prototype.getPublishEvents = function() { 209 | return [ 210 | { 211 | name: 'event1', 212 | extraEvents: ['event2'] 213 | } 214 | ]; 215 | }; 216 | 217 | var module1 = new Module1(); 218 | var node = { 219 | on: sinon.spy(), 220 | getAllPublishEvents: sinon.stub().returns(module1.getPublishEvents()) 221 | }; 222 | 223 | var web = new WebService({node: node}); 224 | var events = web.getEventNames(); 225 | 226 | events.should.deep.equal(['event1', 'event2']); 227 | }); 228 | 229 | it('should throw an error if there is a duplicate event', function() { 230 | var Module1 = function() {}; 231 | Module1.prototype.getPublishEvents = function() { 232 | return [ 233 | { 234 | name: 'event1', 235 | extraEvents: ['event1'] 236 | } 237 | ]; 238 | }; 239 | 240 | var module1 = new Module1(); 241 | var node = { 242 | on: sinon.spy(), 243 | getAllPublishEvents: sinon.stub().returns(module1.getPublishEvents()) 244 | }; 245 | 246 | var web = new WebService({node: node}); 247 | (function() { 248 | var events = web.getEventNames(); 249 | }).should.throw('Duplicate event event1'); 250 | }); 251 | }); 252 | 253 | describe('#_getRemoteAddress', function() { 254 | it('will get remote address from cloudflare header', function() { 255 | var web = new WebService({node: defaultNode}); 256 | var socket = {}; 257 | socket.conn = {}; 258 | socket.client = {}; 259 | socket.client.request = {}; 260 | socket.client.request.headers = { 261 | 'cf-connecting-ip': '127.0.0.1' 262 | }; 263 | var remoteAddress = web._getRemoteAddress(socket); 264 | remoteAddress.should.equal('127.0.0.1'); 265 | }); 266 | it('will get remote address from connection', function() { 267 | var web = new WebService({node: defaultNode}); 268 | var socket = {}; 269 | socket.conn = {}; 270 | socket.conn.remoteAddress = '127.0.0.1'; 271 | socket.client = {}; 272 | socket.client.request = {}; 273 | socket.client.request.headers = {}; 274 | var remoteAddress = web._getRemoteAddress(socket); 275 | remoteAddress.should.equal('127.0.0.1'); 276 | }); 277 | }); 278 | 279 | describe('#socketHandler', function() { 280 | var sandbox = sinon.sandbox.create(); 281 | beforeEach(function() { 282 | sandbox.stub(log, 'info'); 283 | }); 284 | afterEach(function() { 285 | sandbox.restore(); 286 | }); 287 | 288 | var bus = new EventEmitter(); 289 | bus.remoteAddress = '127.0.0.1'; 290 | 291 | var Module1 = function() {}; 292 | Module1.prototype.getPublishEvents = function() { 293 | return [ 294 | { 295 | name: 'event1', 296 | extraEvents: ['event2'] 297 | } 298 | ]; 299 | }; 300 | 301 | var module1 = new Module1(); 302 | var node = { 303 | on: sinon.spy(), 304 | openBus: sinon.stub().returns(bus), 305 | getAllPublishEvents: sinon.stub().returns(module1.getPublishEvents()) 306 | }; 307 | 308 | var web; 309 | var socket; 310 | 311 | it('on message should call socketMessageHandler', function(done) { 312 | web = new WebService({node: node}); 313 | web.eventNames = web.getEventNames(); 314 | web.socketMessageHandler = function(param1) { 315 | param1.should.equal('data'); 316 | done(); 317 | }; 318 | socket = new EventEmitter(); 319 | socket.conn = {}; 320 | socket.conn.remoteAddress = '127.0.0.1'; 321 | socket.client = {}; 322 | socket.client.request = {}; 323 | socket.client.request.headers = {}; 324 | web.socketHandler(socket); 325 | socket.emit('message', 'data'); 326 | }); 327 | 328 | it('on message should NOT call socketMessageHandler if not enabled', function(done) { 329 | web = new WebService({node: node, enableSocketRPC: false}); 330 | web.eventNames = web.getEventNames(); 331 | web.socketMessageHandler = sinon.stub(); 332 | socket = new EventEmitter(); 333 | socket.conn = {}; 334 | socket.conn.remoteAddress = '127.0.0.1'; 335 | socket.client = {}; 336 | socket.client.request = {}; 337 | socket.client.request.headers = {}; 338 | web.socketHandler(socket); 339 | socket.on('message', function() { 340 | web.socketMessageHandler.callCount.should.equal(0); 341 | done(); 342 | }); 343 | socket.emit('message', 'data'); 344 | }); 345 | 346 | it('on subscribe should call bus.subscribe', function(done) { 347 | bus.subscribe = function(param1) { 348 | param1.should.equal('data'); 349 | done(); 350 | }; 351 | 352 | socket.emit('subscribe', 'data'); 353 | }); 354 | 355 | it('on unsubscribe should call bus.unsubscribe', function(done) { 356 | bus.unsubscribe = function(param1) { 357 | param1.should.equal('data'); 358 | done(); 359 | }; 360 | 361 | socket.emit('unsubscribe', 'data'); 362 | }); 363 | 364 | it('publish events from bus should be emitted from socket', function(done) { 365 | socket.once('event2', function(param1, param2) { 366 | param1.should.equal('param1'); 367 | param2.should.equal('param2'); 368 | done(); 369 | }); 370 | socket.connected = true; 371 | bus.emit('event2', 'param1', 'param2'); 372 | }); 373 | 374 | it('on disconnect should close bus', function(done) { 375 | bus.close = function() { 376 | done(); 377 | }; 378 | 379 | socket.emit('disconnect'); 380 | }); 381 | }); 382 | 383 | describe('#socketMessageHandler', function() { 384 | var node = { 385 | on: sinon.spy() 386 | }; 387 | 388 | var web = new WebService({node: node}); 389 | web.methodsMap = { 390 | one: { 391 | fn: function(param1, param2, callback) { 392 | var result = param1 + param2; 393 | if(result > 0) { 394 | return callback(null, result); 395 | } else { 396 | return callback(new Error('error')); 397 | } 398 | }, 399 | args: 2 400 | } 401 | }; 402 | 403 | it('should give a Method Not Found error if method does not exist', function(done) { 404 | var message = { 405 | method: 'two', 406 | params: [1, 2] 407 | }; 408 | web.socketMessageHandler(message, function(response) { 409 | should.exist(response.error); 410 | response.error.message.should.equal('Method Not Found'); 411 | done(); 412 | }); 413 | }); 414 | 415 | it('should call the method and return the result', function(done) { 416 | var message = { 417 | method: 'one', 418 | params: [1, 2] 419 | }; 420 | web.socketMessageHandler(message, function(response) { 421 | should.not.exist(response.error); 422 | response.result.should.equal(3); 423 | done(); 424 | }); 425 | }); 426 | 427 | it('should give an error if there is a param count mismatch', function(done) { 428 | var message = { 429 | method: 'one', 430 | params: [1] 431 | }; 432 | web.socketMessageHandler(message, function(response) { 433 | should.exist(response.error); 434 | response.error.message.should.equal('Expected 2 parameter(s)'); 435 | done(); 436 | }); 437 | }); 438 | 439 | it('should give an error if the method gave an error', function(done) { 440 | var message = { 441 | method: 'one', 442 | params: [-1, -2] 443 | }; 444 | web.socketMessageHandler(message, function(response) { 445 | should.exist(response.error); 446 | response.error.message.should.equal('Error: error'); 447 | done(); 448 | }); 449 | }); 450 | }); 451 | 452 | describe('#deriveHttpsOptions', function() { 453 | it('should read key and cert from files specified', function() { 454 | var web = new WebService({ 455 | node: defaultNode, 456 | https: true, 457 | httpsOptions: { 458 | key: 'key', 459 | cert: 'cert' 460 | } 461 | }); 462 | 463 | web.transformHttpsOptions(); 464 | web.httpsOptions.key.should.equal('key-buffer'); 465 | web.httpsOptions.cert.should.equal('cert-buffer'); 466 | }); 467 | it('should throw an error if https is specified but key or cert is not specified', function() { 468 | var web = new WebService({ 469 | node: defaultNode, 470 | https: true, 471 | httpsOptions: { 472 | key: 'key' 473 | } 474 | }); 475 | 476 | (function() { 477 | web.transformHttpsOptions(); 478 | }).should.throw('Missing https options'); 479 | }); 480 | }); 481 | 482 | }); 483 | -------------------------------------------------------------------------------- /test/utils.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('chai').should(); 4 | var utils = require('../lib/utils'); 5 | 6 | describe('Utils', function() { 7 | 8 | describe('#isHash', function() { 9 | 10 | it('false for short string', function() { 11 | var a = utils.isHash('ashortstring'); 12 | a.should.equal(false); 13 | }); 14 | 15 | it('false for long string', function() { 16 | var a = utils.isHash('00000000000000000000000000000000000000000000000000000000000000000'); 17 | a.should.equal(false); 18 | }); 19 | 20 | it('false for correct length invalid char', function() { 21 | var a = utils.isHash('z000000000000000000000000000000000000000000000000000000000000000'); 22 | a.should.equal(false); 23 | }); 24 | 25 | it('false for invalid type (buffer)', function() { 26 | var a = utils.isHash(new Buffer('abcdef', 'hex')); 27 | a.should.equal(false); 28 | }); 29 | 30 | it('false for invalid type (number)', function() { 31 | var a = utils.isHash(123456); 32 | a.should.equal(false); 33 | }); 34 | 35 | it('true for hash', function() { 36 | var a = utils.isHash('fc63629e2106c3440d7e56751adc8cfa5266a5920c1b54b81565af25aec1998b'); 37 | a.should.equal(true); 38 | }); 39 | 40 | }); 41 | 42 | describe('#isSafeNatural', function() { 43 | 44 | it('false for float', function() { 45 | var a = utils.isSafeNatural(0.1); 46 | a.should.equal(false); 47 | }); 48 | 49 | it('false for string float', function() { 50 | var a = utils.isSafeNatural('0.1'); 51 | a.should.equal(false); 52 | }); 53 | 54 | it('false for string integer', function() { 55 | var a = utils.isSafeNatural('1'); 56 | a.should.equal(false); 57 | }); 58 | 59 | it('false for negative integer', function() { 60 | var a = utils.isSafeNatural(-1); 61 | a.should.equal(false); 62 | }); 63 | 64 | it('false for negative integer string', function() { 65 | var a = utils.isSafeNatural('-1'); 66 | a.should.equal(false); 67 | }); 68 | 69 | it('false for infinity', function() { 70 | var a = utils.isSafeNatural(Infinity); 71 | a.should.equal(false); 72 | }); 73 | 74 | it('false for NaN', function() { 75 | var a = utils.isSafeNatural(NaN); 76 | a.should.equal(false); 77 | }); 78 | 79 | it('false for unsafe number', function() { 80 | var a = utils.isSafeNatural(Math.pow(2, 53)); 81 | a.should.equal(false); 82 | }); 83 | 84 | it('true for positive integer', function() { 85 | var a = utils.isSafeNatural(1000); 86 | a.should.equal(true); 87 | }); 88 | 89 | }); 90 | 91 | describe('#startAtZero', function() { 92 | 93 | it('will set key to zero if not set', function() { 94 | var obj = {}; 95 | utils.startAtZero(obj, 'key'); 96 | obj.key.should.equal(0); 97 | }); 98 | 99 | it('not if already set', function() { 100 | var obj = { 101 | key: 10 102 | }; 103 | utils.startAtZero(obj, 'key'); 104 | obj.key.should.equal(10); 105 | }); 106 | 107 | it('not if set to false', function() { 108 | var obj = { 109 | key: false 110 | }; 111 | utils.startAtZero(obj, 'key'); 112 | obj.key.should.equal(false); 113 | }); 114 | 115 | it('not if set to undefined', function() { 116 | var obj = { 117 | key: undefined 118 | }; 119 | utils.startAtZero(obj, 'key'); 120 | should.equal(obj.key, undefined); 121 | }); 122 | 123 | it('not if set to null', function() { 124 | var obj = { 125 | key: null 126 | }; 127 | utils.startAtZero(obj, 'key'); 128 | should.equal(obj.key, null); 129 | }); 130 | 131 | }); 132 | 133 | describe('#parseParamsWithJSON', function() { 134 | it('will parse object', function() { 135 | var paramsArg = ['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', '{"start": 100, "end": 1}']; 136 | var params = utils.parseParamsWithJSON(paramsArg); 137 | params.should.deep.equal(['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', {start: 100, end: 1}]); 138 | }); 139 | it('will parse array', function() { 140 | var paramsArg = ['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', '[0, 1]']; 141 | var params = utils.parseParamsWithJSON(paramsArg); 142 | params.should.deep.equal(['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', [0, 1]]); 143 | }); 144 | it('will parse numbers', function() { 145 | var paramsArg = ['3', 0, 'b', '0', 0x12, '0.0001']; 146 | var params = utils.parseParamsWithJSON(paramsArg); 147 | params.should.deep.equal([3, 0, 'b', 0, 0x12, 0.0001]); 148 | }); 149 | }); 150 | 151 | }); 152 | --------------------------------------------------------------------------------