diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 000000000..07c493398 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: DvrDb09a8vhPlVf6DT4cGBjcFOi6DfZN1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index d72450d43..0e43f9512 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ node_modules/ node_modules/* coverage/* -out/ -out/* -build/ -build/* .lock-wscript -Makefile.gyp *.swp *.Makefile *.target.gyp.mk @@ -19,15 +14,16 @@ Makefile.gyp *.filters *.user *.project -test.js **/*.dylib **/*.so **/*.old **/*.files **/*.config **/*.creator -libbitcoind -libbitcoind* -libbitcoind.includes *.log .DS_Store +bin/bitcoin* +bin/SHA256SUMS.asc +regtest/data/node1/regtest +regtest/data/node2/regtest +regtest/data/node3/regtest diff --git a/.travis.yml b/.travis.yml index edcb86073..fbc8be0eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: false language: node_js env: - - BITCORENODE_ENV=test BITCORENODE_ASSUME_YES=true CXX=g++-4.8 CC=gcc-4.8 + - CXX=g++-4.8 CC=gcc-4.8 addons: apt: sources: @@ -9,14 +9,14 @@ addons: packages: - g++-4.8 - gcc-4.8 + - libzmq3-dev node_js: + - "v0.10.25" - "v0.12.7" - "v4" script: - - _mocha -R spec integration/regtest.js - - _mocha -R spec integration/regtest-node.js - - _mocha -R spec integration/p2p.js - - _mocha -R spec --recursive -cache: - directories: - - cache + - npm run regtest + - npm run test + - npm run jshint +after_success: + - npm run coveralls \ No newline at end of file diff --git a/PATCH_VERSION b/PATCH_VERSION deleted file mode 100644 index 4b8f7b07e..000000000 --- a/PATCH_VERSION +++ /dev/null @@ -1 +0,0 @@ -v0.11.2 diff --git a/README.md b/README.md index 2da202a50..28063355c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Bitcore Node ============ -A Bitcoin full node for building applications and services with Node.js. A node is extensible and can be configured to run additional services. At the minimum a node has native bindings to Bitcoin Core with the [Bitcoin Service](docs/services/bitcoind.md). Additional services can be enabled to make a node more useful such as exposing new APIs, adding new indexes for addresses with the [Address Service](docs/services/address.md), running a block explorer, wallet service, and other customizations. +A Bitcoin full node for building applications and services with Node.js. A node is extensible and can be configured to run additional services. At the minimum a node has an interface to [Bitcoin Core with additional indexing](https://github.com/bitpay/bitcoin/tree/0.12-bitcore) for more advanced address queries. Additional services can be enabled to make a node more useful such as exposing new APIs, running a block explorer and wallet service. ## Install @@ -10,14 +10,15 @@ npm install -g bitcore-node bitcore-node start ``` -Note: For your convenience, we distribute binaries for x86_64 Linux and x86_64 Mac OS X. Upon npm install, the binaries for your platform will be downloaded. For more detailed installation instructions, or if you want to compile the project yourself, then please see the [Build & Install](docs/build.md) documentation to build the project from source. +Note: For your convenience, we distribute bitcoind binaries for x86_64 Linux and x86_64 Mac OS X. Upon npm install, the binaries for your platform will be downloaded. For more detailed installation instructions, or if you want to compile the project yourself, then please see the Bitcore branch of [Bitcoin Core with additional indexing](https://github.com/bitpay/bitcoin/tree/0.12-bitcore). ## Prerequisites -- Node.js v0.12 or v4.2 -- ~100GB of disk storage +- GNU/Linux x86_32/x86_64, or OSX 64bit *(for bitcoind distributed binaries)* +- Node.js v0.10, v0.12 or v4 +- ZeroMQ *(libzmq3-dev for Ubuntu/Debian or zeromq on OSX)* +- ~200GB of disk storage - ~4GB of RAM -- Mac OS X >= 10.9, Ubuntu >= 12.04 (libc >= 2.15 and libstdc++ >= 6.0.16) ## Configuration @@ -32,12 +33,6 @@ bitcore-node install https://github.com/yourname/helloworld 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). -To start bitcore-node as a daemon: - -```bash -bitcore-node start --daemon -``` - ## Add-on Services There are several add-on services available to extend the functionality of Bitcore: @@ -48,17 +43,13 @@ There are several add-on services available to extend the functionality of Bitco ## Documentation +- [Upgrade Notes](docs/upgrade.md) - [Services](docs/services.md) - - [Bitcoind](docs/services/bitcoind.md) - Native bindings to Bitcoin Core - - [Database](docs/services/db.md) - The foundation API methods for getting information about blocks and transactions. - - [Address](docs/services/address.md) - Adds additional API methods for querying and subscribing to events with bitcoin addresses. + - [Bitcoind](docs/services/bitcoind.md) - Interface to Bitcoin Core - [Web](docs/services/web.md) - Creates an express application over which services can expose their web/API content -- [Build & Install](docs/build.md) - How to build and install from source -- [Testing & Development](docs/testing.md) - Developer guide for testing +- [Development Environment](docs/development.md) - Guide for setting up a development environment - [Node](docs/node.md) - Details on the node constructor - [Bus](docs/bus.md) - Overview of the event bus constructor -- [Errors](docs/errors.md) - Reference for error handling and types -- [Patch](docs/patch.md) - Information about the patch applied to Bitcoin Core - [Release Process](docs/release.md) - Information about verifying a release and the release process. ## Contributing diff --git a/benchmarks/blockhandler.js b/benchmarks/blockhandler.js deleted file mode 100644 index 3b46e750d..000000000 --- a/benchmarks/blockhandler.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -var benchmark = require('benchmark'); -var async = require('async'); -var sinon = require('sinon'); -var bitcore = require('bitcore-lib'); -var Block = bitcore.Block; -var AddressService = require('../lib/services/address'); -var maxTime = 20; - -var blockData1 = require('./data/block-367238.json'); -var blockData2 = require('./data/block-367239.json'); -var blockData3 = require('./data/block-367240.json'); - -console.log('Address Service Block Handler'); -console.log('-----------------------------'); - -async.series([ - function(next) { - - var c = 0; - var blocks = [ - Block.fromBuffer(new Buffer(blockData1, 'hex')), - Block.fromBuffer(new Buffer(blockData2, 'hex')), - Block.fromBuffer(new Buffer(blockData3, 'hex')) - ]; - var blocksLength = 3; - var node = { - services: { - bitcoind : { - on: sinon.stub() - } - } - }; - var addressService = new AddressService({node: node}); - - function blockHandler(deffered) { - if (c >= blocksLength) { - c = 0; - } - var block = blocks[c]; - addressService.blockHandler(block, true, function(err, operations) { - if (err) { - throw err; - } - deffered.resolve(); - }); - c++; - } - - var suite = new benchmark.Suite(); - - suite.add('blockHandler', blockHandler, { - defer: true, - maxTime: maxTime - }); - - suite - .on('cycle', function(event) { - console.log(String(event.target)); - }) - .on('complete', function() { - console.log('Fastest is ' + this.filter('fastest').pluck('name')); - console.log('----------------------------------------------------------------------'); - next(); - }) - .run(); - } -], function(err) { - if (err) { - throw err; - } - console.log('Finished'); - process.exit(); -}); diff --git a/bin/build b/bin/build deleted file mode 100755 index 8c0fbe648..000000000 --- a/bin/build +++ /dev/null @@ -1,187 +0,0 @@ -#!/bin/bash -root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.." -options=`cat ${root_dir}/bin/config_options.sh` -host=$(${root_dir}/bin/variables.sh host) || exit -1 -depends_dir=$($root_dir/bin/variables.sh depends_dir) -btc_dir="${root_dir}/libbitcoind" -sys=$($root_dir/bin/variables.sh sys) -patch_sha=$($root_dir/bin/variables.sh patch_sha) -config_lib_dir=$($root_dir/bin/variables.sh config_lib_dir) -export CPPFLAGS="-I${depends_dir}/${host}/include/boost -I${depends_dir}/${host}/include -L${depends_dir}/${host}/lib" -echo "Using BTC directory: ${btc_dir}" - -cd "${root_dir}" || exit -1 - -build_dependencies () { - if [ -d "${btc_dir}" ]; then - pushd "${depends_dir}" || exit -1 - echo "using host for dependencies: ${host}" - if [ "${test}" = true ]; then - make HOST=${host} NO_QT=1 NO_UPNP=1 - else - make HOST=${host} NO_QT=1 NO_WALLET=1 NO_UPNP=1 - fi - if test $? -eq 0; then - popd || exit -1 - else - echo "Bitcoin's dependency building failed, please check the previous output for details." - exit -1 - fi - fi -} - -get_patch_file () { - if test -e "${root_dir/PATCH_VERSION}"; then - tag=`cat "${root_dir}/PATCH_VERSION" | xargs` || exit -1 - else - echo "no tag file found, please create it in the root of the project as so: 'echo \"v0.10.2\" > PATCH_VERSION'" - exit 1 - fi -} - -compare_patch () { - cd "${btc_dir}" || exit -1 - get_patch_file - echo "running the diff command from HEAD to ${tag}" - last_commit=$(git rev-parse HEAD) - diff=$(git show ${last_commit}) - stripped_diff=$( echo -n "${diff}" | tail -n $( expr `echo -n "${diff}" | wc -l` - 5 ) ) - matching_patch=`echo -n "${stripped_diff}" | diff -w "${root_dir}/etc/bitcoin.patch" -` -} - -cache_files () { - cache_file="${root_dir}"/cache/cache.tar - pushd "${btc_dir}" || exit -1 - find src depends/${host} -type f \( -name "*.h" -or -name "*.hpp" -or -name \ -"*.ipp" -or -name "*.a" \) | tar -cf "${cache_file}" -T - - if test $? -ne 0; then - echo "We were trying to copy over your cached artifacts, but there was an issue." - exit -1 - fi - tar xf "${cache_file}" -C "${root_dir}"/cache - if test $? -ne 0; then - echo "We were trying to untar your cache, but there was an issue." - exit -1 - fi - rm -fr "${cache_file}" >/dev/null 2>&1 - popd || exit -1 -} - -debug= -if [ "${BITCORENODE_ENV}" == "debug" ]; then - options=`cat ${root_dir}/bin/config_options_debug.sh` || exit -1 -fi - -test=false -if [ "${BITCORENODE_ENV}" == "test" ]; then - test=true - options=`cat ${root_dir}/bin/config_options_test.sh` || exit -1 -fi - -if hash shasum 2>/dev/null; then - shasum_cmd="shasum -a 256" -else - shasum_cmd="sha256sum" -fi - -patch_file_sha=$(${shasum_cmd} "${root_dir}/etc/bitcoin.patch" | awk '{print $1}') -last_patch_file_sha= -if [ -e "${patch_sha}" ]; then - echo "Patch file sha exists, let's see if the patch has changed since last build..." - last_patch_file_sha=$(cat "${patch_sha}") -fi -shared_file_built=false -if [ "${last_patch_file_sha}" == "${patch_file_sha}" ]; then - echo "Patch file contents matches the sha from the patch file itself, so no reason to rebuild the bindings unless there are no prebuilt bindings." - shared_file_built=true -fi - -if [ "${shared_file_built}" = false ]; then - echo "Looks like the patch to bitcoin changed since last build -or- this is the first build, so rebuilding libbitcoind itself..." - mac_response=$($root_dir/bin/variables.sh mac_dependencies) - if [ "${mac_response}" != "" ]; then - echo "${mac_response}" - exit -1 - fi - only_make=false - if [ -d "${btc_dir}" ]; then - echo "running compare patch..." - compare_patch - repatch=false - if [[ "${matching_patch}" =~ [^\s\\] ]]; then - echo "Warning! libbitcoind is not patched with:\ - ${root_dir}/etc/bitcoin.patch." - echo -n "Would you like to remove the current patch, checkout the tag: ${tag} and \ -apply the current patch from "${root_dir}"/etc/bitcoin.patch? (y/N): " - if [ "${BITCORENODE_ASSUME_YES}" = true ]; then - input=y - echo "" - else - read input - fi - if [[ "${input}" =~ ^y|^Y ]]; then - repatch=true - echo "Removing directory: \"${btc_dir}\" and starting over!" - rm -fr "${btc_dir}" >/dev/null 2>&1 - fi - fi - if [ "${repatch}" = false ]; then - echo "Running make inside libbitcoind (assuming you've previously patched and configured libbitcoind)..." - cd "${btc_dir}" || exit -1 - only_make=true - fi - fi - - if [ "${only_make}" = false ]; then - echo "Cloning, patching, and building libbitcoind..." - get_patch_file - echo "attempting to checkout tag: ${tag} of bitcoin from github..." - cd "${root_dir}" || exit -1 - #versions of git prior to 2.x will not clone correctly with --branch - git clone --depth 1 https://github.com/bitcoin/bitcoin.git libbitcoind - cd "${btc_dir}" || exit -1 - git fetch --tags - git checkout "${tag}" - echo '../patch-bitcoin.sh' "${btc_dir}" - ../bin/patch-bitcoin "${btc_dir}" - - if ! test -d .git; then - echo 'Please point this script to an upstream bitcoin git repo.' - exit -1 - fi - - fi - build_dependencies - echo './autogen.sh' - ./autogen.sh || exit -1 - - config_host="--host ${host}" - full_options="${options} ${config_host} ${config_lib_dir}" - echo "running the configure script with the following options:\n :::[\"${full_options}\"]:::" - ${full_options} - - echo 'make V=1' - make V=1 || exit -1 - - echo "Creating the sha marker for the patching in libbitcoind..." - echo "Writing patch sha file to: \"${patch_sha}\"" - echo -n `${shasum_cmd} "${root_dir}"/etc/bitcoin.patch | awk '{print $1}'` > "${patch_sha}" - cache_files - echo 'Build finished successfully.' -else - echo 'Using existing static library.' -fi - -# Building the Bindings - -set -e - -cd "${root_dir}" - -debug=--debug=false -if test x"$1" = x'debug'; then - debug=--debug -fi - -echo "running::: 'node-gyp ${sys} ${debug} rebuild'" -node-gyp ${sys} ${debug} rebuild diff --git a/bin/clean b/bin/clean deleted file mode 100755 index 414df579b..000000000 --- a/bin/clean +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.." -cd "${root_dir}" -pushd "${root_dir}"/libbitcoind -make clean -popd -node-gyp clean -rm -fr cache/* diff --git a/bin/config_options.sh b/bin/config_options.sh deleted file mode 100644 index d51d99a0f..000000000 --- a/bin/config_options.sh +++ /dev/null @@ -1,2 +0,0 @@ -./configure --enable-tests=no --enable-daemonlib --with-gui=no --without-qt --without-miniupnpc --without-bdb --disable-wallet --without-utils - diff --git a/bin/config_options_debug.sh b/bin/config_options_debug.sh deleted file mode 100644 index fb5845e08..000000000 --- a/bin/config_options_debug.sh +++ /dev/null @@ -1,2 +0,0 @@ -./configure --enable-debug --enable-tests=no --enable-daemonlib --with-gui=no --without-qt --without-miniupnpc --without-bdb --disable-wallet --without-utils - diff --git a/bin/config_options_test.sh b/bin/config_options_test.sh deleted file mode 100644 index 3ee837016..000000000 --- a/bin/config_options_test.sh +++ /dev/null @@ -1,2 +0,0 @@ -./configure --enable-debug --enable-tests=no --enable-daemonlib --with-gui=no --without-qt --without-miniupnpc - diff --git a/bin/get-tarball-name.js b/bin/get-tarball-name.js deleted file mode 100644 index c7ea0ffb3..000000000 --- a/bin/get-tarball-name.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var execSync = require('child_process').execSync; - -function getTarballName() { - var packageRoot = __dirname + '/..'; - var version = require(packageRoot + '/package.json').version; - var platform = process.platform; - var arch = execSync(packageRoot + '/bin/variables.sh arch').toString(); - var abi = process.versions.modules; - var tarballName = 'libbitcoind-' + version + '-node' + abi + '-' + platform + '-' + arch + '.tgz'; - return tarballName; -} - -if (require.main === module) { - process.stdout.write(getTarballName()); -} - -module.exports = getTarballName; diff --git a/bin/install b/bin/install deleted file mode 100755 index c6e3a9167..000000000 --- a/bin/install +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.." - -cd "${root_dir}" - -tarball_name=`node bin/get-tarball-name.js` -bucket_name="bitcore-node" -binary_url="https://${bucket_name}.s3.amazonaws.com/${tarball_name}" - -echo "Downloading binary: ${binary_url}" - -is_curl=true -if hash curl 2>/dev/null; then - curl --fail -I $binary_url >/dev/null 2>&1 -else - is_curl=false - wget --server-response --spider $binary_url >/dev/null 2>&1 -fi - -if test $? -eq 0; then - if [ "${is_curl}" = true ]; then - curl $binary_url > $tarball_name - else - wget $binary_url - fi - if test -e "${tarball_name}"; then - echo "Unpacking binary distribution" - tar -xvzf $tarball_name - if test $? -eq 0; then - exit 0 - fi - fi -fi -echo "Prebuild binary could not be downloaded, building from source..." -./bin/build diff --git a/bin/package.js b/bin/package.js deleted file mode 100644 index c03639bdf..000000000 --- a/bin/package.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -var exec = require('child_process').exec; -var bindings = require('bindings'); -var index = require('../lib'); -var log = index.log; - -var packageRoot = bindings.getRoot(bindings.getFileName()); -var binaryPath = bindings({ - path: true, - bindings: 'bitcoind.node' -}); -var relativeBinaryPath = binaryPath.replace(packageRoot + '/', ''); -var tarballName = require('./get-tarball-name')(); - -log.info('Signing binding binary: "' + binaryPath + '"'); - -var signCommand = 'gpg --yes --out ' + binaryPath + '.sig --detach-sig ' + binaryPath; - -var signchild = exec(signCommand, function(error, stdout, stderr) { - if (error) { - throw error; - } - - if (stdout) { - log.info('GPG:', stdout); - } - - if (stderr) { - log.error(stderr); - } - - log.info('Packaging tarball: "' + tarballName + '"'); - - // Create a tarball of both the binding and the signature - var tarCommand = 'tar -C ' + - packageRoot + ' -cvzf ' + - tarballName + ' ' + - relativeBinaryPath + ' ' + - relativeBinaryPath + '.sig'; - - var tarchild = exec(tarCommand, function (error, stdout, stderr) { - - if (error) { - throw error; - } - - if (stdout) { - log.info('Tar:', stdout); - } - - if (stderr) { - log.error(stderr); - } - - }); - -}); diff --git a/bin/patch-bitcoin b/bin/patch-bitcoin deleted file mode 100755 index aeaab6b19..000000000 --- a/bin/patch-bitcoin +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.." -#root_dir="$(readlink -f "$(dirname "$0")")/.." -cd "$root_dir" -dir=$(test -n "$1" && echo "$1" || echo "${HOME}/bitcoin") -patch_file="$(pwd)/etc/bitcoin.patch" - -cd "$dir" || exit 1 - -if ! test -d .git; then - echo 'Please point this script to an upstream bitcoin git repo.' - exit 1 -fi - -if test $? -ne 0; then - echo 'Unable to checkout necessary commit.' - echo 'Please pull the latest HEAD from the upstream bitcoin repo.' - exit 1 -fi -git checkout -b "libbitcoind-$(date '+%Y.%m.%d')" || exit 1 - -patch -p1 < "$patch_file" || exit 1 - -git add --all || exit 1 - -[ -n "$( git config user.name )" ] \ - || git config user.name 'Bitcore Build' - -[ -n "$( git config user.email )" ] \ - || git config user.email "$( id -n -u )@$( hostname -f )" - -git commit -a -m 'allow compiling of libbitcoind.so.' || exit 1 - -echo 'Patch completed successfully.' -exit 0 diff --git a/bin/start-libbitcoind.js b/bin/start-libbitcoind.js deleted file mode 100644 index e2f1c6372..000000000 --- a/bin/start-libbitcoind.js +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -var index = require('..'); -var log = index.log; - -process.title = 'libbitcoind'; - -/** - * daemon - */ -var daemon = require('../').services.Bitcoin({ - node: { - datadir: process.env.BITCORENODE_DIR || process.env.HOME + '/.bitcoin', - network: { - name: process.env.BITCORENODE_NETWORK || 'livenet' - } - } -}); - -daemon.start(function() { - log.info('ready'); -}); - -daemon.on('error', function(err) { - log.info('error="%s"', err.message); -}); - -daemon.on('open', function(status) { - log.info('status="%s"', status); -}); - -function exitHandler(options, err) { - log.info('Stopping daemon'); - if (err) { - log.error('uncaught exception:', err); - if(err.stack) { - console.log(err.stack); - } - process.exit(-1); - } - if (options.sigint) { - daemon.stop(function(err) { - if(err) { - log.error('Failed to stop services: ' + err); - return process.exit(1); - } - - log.info('Halted'); - process.exit(0); - }); - } -} - -//catches uncaught exceptions - - -process.on('uncaughtException', exitHandler.bind(null, {exit:true})); -//catches ctrl+c event -process.on('SIGINT', exitHandler.bind(null, {sigint:true})); diff --git a/bin/start.js b/bin/start.js deleted file mode 100755 index 6fda5aff8..000000000 --- a/bin/start.js +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -var start = require('../lib/scaffold/start'); -var defaultConfig = require('../lib/scaffold/default-config'); - -start(defaultConfig()); diff --git a/bin/upload.js b/bin/upload.js deleted file mode 100644 index 4376bb20b..000000000 --- a/bin/upload.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var AWS = require('aws-sdk'); -var bindings = require('bindings'); -var index = require('../lib'); -var log = index.log; - -var config = require(process.env.HOME + '/.bitcore-node-upload.json'); - -AWS.config.region = config.region; -AWS.config.update({ - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey -}); - -var packageRoot = bindings.getRoot(bindings.getFileName()); -var tarballName = require('./get-tarball-name')(); -var bucketName = 'bitcore-node'; -var url = 'https://' + bucketName + '.s3.amazonaws.com/' + tarballName; -var localPath = packageRoot + '/' + tarballName; - -log.info('Uploading package: ' + localPath); - -var fileStream = fs.createReadStream(localPath); - -fileStream.on('error', function(err) { - if (err) { - throw err; - } -}); - -fileStream.on('open', function() { - - var s3 = new AWS.S3(); - - var params = { - ACL: 'public-read', - Key: tarballName, - Body: fileStream, - Bucket: bucketName - }; - - s3.putObject(params, function(err, data) { - if (err) { - throw err; - } else { - log.info('Successfully uploaded to: ' + url); - } - }); - -}); diff --git a/bin/variables.sh b/bin/variables.sh deleted file mode 100755 index 0988e0fa2..000000000 --- a/bin/variables.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/bin/bash - -exec 2> /dev/null -root_dir="$(cd "$(dirname $0)" && pwd)/.." -if [ "${root_dir}" == "" ]; then - root_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/.." -fi -bitcoin_dir="${root_dir}"/libbitcoind -cache_dir="${root_dir}"/cache - -get_host_and_platform () { - platform=`uname -a | awk '{print tolower($1)}'` - arch=`uname -m` - if [ "${arch:0:3}" == "arm" ]; then - platform="linux-gnueabihf" - arch="arm" - fi - if [ -n "${CXX}" ] && [ -n "${CC}" ]; then - cc_target=$("${CC}" -v 2>&1 | awk '/Target:/ {print $2}') - cxx_target=$("${CXX}" -v 2>&1 | awk '/Target:/ {print $2}') - IFS='-' read -ra SYS <<< "${cc_target}" - if [ "${SYS[0]}" != "${arch}" ]; then - if [ -n "${SYS[1]}" ] && [ -n "${SYS[2]}" ] && hash "${CXX}" && hash "${CC}" && [ -n "${cc_target}" ] && [ -n "${cxx_target}" ]; then - #try and see if we've got a cross compiler, if not then auto detect - arch="${SYS[0]}" - platform="${SYS[1]}"-"${SYS[2]}" - else - error_message="You've specified a cross compiler, but we could not compute the host-platform-triplet for cross compilation. Please set CC and CXX environment variables with host-platform-triplet-*. Also ensure the cross compiler exists on your system and is available on your path. Example: CC=arm-linux-gnueabihf-gcc CXX=arm-linux-gnueabihf-g++" - return_error_message - fi - fi - fi -} - -return_error_message () { - echo "${error_message}" - exit -1 -} - -get_host_and_platform -host="${arch}"-"${platform}" - -mac_response= -check_mac_build_system () { - if [ "${platform}" == "darwin" ]; then - if [ ! -e "/usr/include/stdlib.h" ]; then - if hash xcode-select 2>/dev/null; then - mac_response="Please run 'xcode-select --install' from the command line because it seems that you've got Xcode, but not the Xcode command line tools that are required for compiling this project from source..." - else - mac_response="please use the App Store to install Xcode and Xcode command line tools. After Xcode is installed, please run: 'xcode-select --install' from the command line" - fi - fi - fi -} - -depends_dir="${bitcoin_dir}"/depends -thread="${cache_dir}"/depends/"${host}"/lib/libboost_thread-mt.a -filesystem="${cache_dir}"/depends/"${host}"/lib/libboost_filesystem-mt.a -chrono="${cache_dir}"/depends/"${host}"/lib/libboost_chrono-mt.a -program_options="${cache_dir}"/depends/"${host}"/lib/libboost_program_options-mt.a -system="${cache_dir}"/depends/"${host}"/lib/libboost_system-mt.a -leveldb="${cache_dir}"/src/leveldb/libleveldb.a -memenv="${cache_dir}"/src/leveldb/libmemenv.a -libsecp256k1="${cache_dir}"/src/secp256k1/.libs/libsecp256k1.a -ssl="${cache_dir}"/depends/"${host}"/lib/libssl.a -crypto="${cache_dir}"/depends/"${host}"/lib/libcrypto.a - -config_lib_dir= -if [ "${platform}" == "darwin" ]; then - config_lib_dir="--with-boost-libdir=${depends_dir}/${host}/lib" -else - config_lib_dir="--prefix=${depends_dir}/${host}" -fi - -if test x"$1" = x'anl'; then - if [ "${platform}" != "darwin" ]; then - echo -n "-lanl" - fi -fi - -if test x"$1" = x'cache_dir'; then - echo -n "${cache_dir}" -fi - -if test x"$1" = x'btcdir'; then - echo -n "${bitcoin_dir}" -fi - -if test -z "$1" -o x"$1" = x'thread'; then - echo -n "${thread}" -fi - -if test -z "$1" -o x"$1" = x'filesystem'; then - echo -n "${filesystem}" -fi - -if test -z "$1" -o x"$1" = x'program_options'; then - echo -n "${program_options}" -fi - -if test -z "$1" -o x"$1" = x'system'; then - echo -n "${system}" -fi - -if test -z "$1" -o x"$1" = x'ssl'; then - echo -n "${ssl}" -fi - -if test -z "$1" -o x"$1" = x'crypto'; then - echo -n "${crypto}" -fi - -if test -z "$1" -o x"$1" = x'chrono'; then - echo -n "${chrono}" -fi - -if test -z "$1" -o x"$1" = x'depends_dir'; then - echo -n "${depends_dir}" -fi - -if test -z "$1" -o x"$1" = x'leveldb'; then - echo -n "${leveldb}" -fi - -if test -z "$1" -o x"$1" = x'memenv'; then - echo -n "${memenv}" -fi - -if test -z "$1" -o x"$1" = x'libsecp256k1'; then - echo -n "${libsecp256k1}" -fi - -if test -z "$1" -o x"$1" = x'host'; then - echo -n "${host}" -fi - -if test -z "$1" -o x"$1" = x'arch'; then - echo -n "${arch}" -fi - -if test -z "$1" -o x"$1" = x'bdb'; then - if [ "${BITCORENODE_ENV}" == "test" ]; then - echo -n "${cache_dir}"/depends/"${host}"/lib/libdb_cxx.a - fi -fi - -if test -z "$1" -o x"$1" = x'patch_sha'; then - echo -n "${root_dir}"/cache/patch_sha.txt -fi - -if test -z "$1" -o x"$1" = x'load_archive'; then - if [ "${os}" == "osx" ]; then - echo -n "-Wl,-all_load -Wl,--no-undefined" - else - echo -n "-Wl,--whole-archive ${filesystem} ${thread} "${cache_dir}"/src/.libs/libbitcoind.a -Wl,--no-whole-archive" - fi -fi - -if test -z "$1" -o x"$1" = x'mac_dependencies'; then - check_mac_build_system - echo -n "${mac_response}" -fi - -if test -z "$1" -o x"$1" = x'wallet_enabled'; then - if [ "${BITCORENODE_ENV}" == "test" ]; then - echo -n "-DENABLE_WALLET" - fi -fi - -if test -z "$1" -o x"$1" = x'sys'; then - if [ -n "${SYS}" ]; then - echo -n "--arch=${SYS[0]}" - fi -fi - -if test -z "$1" -o x"$1" = x'bitcoind'; then - echo -n "${cache_dir}"/src/.libs/libbitcoind.a -fi - -if test -z "$1" -o x"$1" = x'config_lib_dir'; then - echo -n "${config_lib_dir}" -fi diff --git a/binding.gyp b/binding.gyp deleted file mode 100644 index 3db1adb1a..000000000 --- a/binding.gyp +++ /dev/null @@ -1,59 +0,0 @@ -{ - "targets": [ - { - "target_name": "libbitcoind", - "include_dirs" : [ - "= 22) should also work with this project. The directions for Ubuntu should generally work except the installation of system utilities and libraries is a bit different. Git is already installed and ready for use without installation. - -```bash -yum install libtool automake autoconf pkgconfig openssl make gcc gcc-c++ kernel-devel openssl-devel.x86_64 patch -``` - -## Mac OS X Yosemite -If Xcode is not already installed, it can be installed via the Mac App Store (will take several minutes). XCode includes "Clang", "git" and other build tools. Once Xcode is installed, you'll then need to install "xcode-select" via running in a terminal and following the prompts: - -```bash -xcode-select --install -``` - -If "Homebrew" is not yet installed, it's needed to install "autoconf" and others. You can install it using the script at [http://brew.sh](http://brew.sh) and following the directions at [https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Installation.md](https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Installation.md) And then run in a terminal: - -```bash -brew install autoconf automake libtool openssl pkg-config -``` - -If Node.js v0.12 and associated commands "node", "npm" and "nvm" are not already installed, you can use "nvm" by running the script at [https://github.com/creationix/nvm#install-script](https://github.com/creationix/nvm#install-script) And then run this command to install Node.js v0.12 - -```bash -nvm install v0.12 -``` - -Clone the bitcore-node repository locally: - -```bash -git clone https://github.com/bitpay/bitcore-node.git -cd bitcore-node -``` - -And finally run the build which will take several minutes. A script in the "bin" directory will download Bitcoin Core v0.11, apply a patch (see more info below), and compile the static library and Node.js bindings. You can start this by running: - -```bash -npm install -``` - -## Cross Compilation -If you desire to cross compile to ARM or Windows from a system that has cross compilation tools available for use, please use the following directions: - -Using a Debian (Jessie) system as the host system (the system that will be doing the compiling): - -```bash -echo -n "deb http://emdebian.org/tools/debian/ jessie main" | sudo tee -a /etc/apt/sources.list -sudo dpkg --add-architecture armhf #or whatever arch you are interested in compiling for -sudo apt-get update #you will get GPG KEY warnings, you can decide if you would like to trust the key -sudo apt-get install crossbuild-essential-armhf -``` - -Next is to use the cross compilation toolchain instead of the defaults: - -```bash -CXX=arm-linux-gnueabihf-g++ CC=arm-linux-gnueabihf-gcc npm install -``` - -The only thing different is the setting of CC/CXX environment variables. Please make sure those compilers (arm-linux-gnueabihf-gcc) actually exist and are on your path. - -```bash -arm-linux-gnueabihf-g++ -v -arm-linux-gnueabihf-gcc -v -``` - -You should get output with the last line ending with something like this: -gcc version 4.9.2 ( 4.9.2-10) - -Once everything is built, you can run bitcore-node via: - -```bash -npm start -``` - -This will then start the syncing process for Bitcoin Core and the extended capabilities as provided by the built-in Address Module (details below). diff --git a/docs/bus.md b/docs/bus.md index 153b9a9c3..8746d682c 100644 --- a/docs/bus.md +++ b/docs/bus.md @@ -20,11 +20,11 @@ bus.close(); ```javascript // subscribe to all transaction events -bus.subscribe('db/transaction'); +bus.subscribe('bitcoind/rawtransaction'); -// only subscribe to events relevant to a bitcoin address -bus.subscribe('address/transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); +// to subscribe to new block hashes +bus.subscribe('bitcoind/hashblock'); // unsubscribe -bus.unsubscribe('db/transaction'); +bus.unsubscribe('bitcoind/rawtransaction'); ``` diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 000000000..39683df6d --- /dev/null +++ b/docs/development.md @@ -0,0 +1,162 @@ +# Setting up Development Environment + +## Install Node.js + +Install Node.js by your favorite method, or use Node Version Manager by following directions at https://github.com/creationix/nvm + +```bash +nvm install v4 +``` + +## Fork and Download Repositories + +To develop bitcore-node: + +```bash +cd ~ +git clone git@github.com:/bitcore-node.git +git clone git@github.com:/bitcore-lib.git +``` + +To develop bitcoin or to compile from source: + +```bash +git clone git@github.com:/bitcoin.git +git fetch origin : +git checkout +``` +**Note**: See bitcoin documentation for building bitcoin on your platform. + + +## Install Development Dependencies + +For Ubuntu: +```bash +sudo apt-get install libzmq3-dev +sudo apt-get install build-essential +``` +**Note**: Make sure that libzmq-dev is not installed, it should be removed when installing libzmq3-dev. + + +For Mac OS X: +```bash +brew install zeromq +``` + +## Install and Symlink + +```bash +cd bitcore-lib +npm install +cd ../bitcore-node +npm install +``` +**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. + + +We now will setup symlinks in `bitcore-node` *(repeat this for any other modules you're planning on developing)*: +```bash +cd node_modules +rm -rf bitcore-lib +ln -s ~/bitcore-lib +rm -rf bitcoind-rpc +ln -s ~/bitcoind-rpc +``` + +And if you're compiling or developing bitcoin: +```bash +cd ../bin +ln -sf ~/bitcoin/src/bitcoind +``` + +## Run Tests + +If you do not already have mocha installed: +```bash +npm install mocha -g +``` + +To run all test suites: +```bash +cd bitcore-node +npm run regtest +npm run test +``` + +To run a specific unit test in watch mode: +```bash +mocha -w -R spec test/services/bitcoind.unit.js +``` + +To run a specific regtest: +```bash +mocha -R spec regtest/bitcoind.js +``` + +## Running a Development Node + +To test running the node, you can setup a configuration that will specify development versions of all of the services: + +```bash +cd ~ +mkdir devnode +cd devnode +mkdir node_modules +touch bitcore-node.json +touch package.json +``` + +Edit `bitcore-node.json` with something similar to: +```json +{ + "network": "livenet", + "port": 3001, + "services": [ + "bitcoind", + "web", + "insight-api", + "insight-ui", + "" + ], + "servicesConfig": { + "bitcoind": { + "spawn": { + "datadir": "/home//.bitcoin", + "exec": "/home//bitcoin/src/bitcoind" + } + } + } +} +``` + +**Note**: To install services [insight-api](https://github.com/bitpay/insight-api) and [insight-ui](https://github.com/bitpay/insight-ui) you'll need to clone the repositories locally. + +Setup symlinks for all of the services and dependencies: + +```bash +cd node_modules +ln -s ~/bitcore-lib +ln -s ~/bitcore-node +ln -s ~/insight-api +ln -s ~/insight-ui +``` + +Make sure that the `/bitcoin.conf` has the necessary settings, for example: +``` +server=1 +whitelist=127.0.0.1 +txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +zmqpubrawtx=tcp://127.0.0.1:28332 +zmqpubhashblock=tcp://127.0.0.1:28332 +rpcallowip=127.0.0.1 +rpcuser=bitcoin +rpcpassword=local321 +``` + +From within the `devnode` directory with the configuration file, start the node: +```bash +../bitcore-node/bin/bitcore-node start +``` \ No newline at end of file diff --git a/docs/errors.md b/docs/errors.md deleted file mode 100644 index e033466b9..000000000 --- a/docs/errors.md +++ /dev/null @@ -1,16 +0,0 @@ -# Errors -Many times there are cases where an error condition can be gracefully handled depending on a particular use. To assist in better error handling, errors will have different types so that it's possible to determine the type of error and handle appropriately. - -```js -node.services.address.getUnspentOutputs('00000000839a8...', function(err, outputs) { - - if (err instanceof errors.NoOutputs) { - // the address hasn't received any transactions - } - - // otherwise the address has outputs (which may be unspent/spent) - -}); -``` - -For more information about different types of errors, please see `lib/errors.js`. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index aec8f91c5..000000000 --- a/docs/index.md +++ /dev/null @@ -1,54 +0,0 @@ -A Bitcoin full node for building applications and services with Node.js. A node is extensible and can be configured to run additional services. At the minimum a node has native bindings to Bitcoin Core with the [Bitcoin Service](services/bitcoind.md). Additional services can be enabled to make a node more useful such as exposing new APIs, adding new indexes for addresses with the [Address Service](services/address.md), running a block explorer, wallet service, and other customizations. - -# Install - -```bash -npm install -g bitcore-node -bitcore-node start -``` - -Note: For your convenience, we distribute binaries for x86_64 Linux and x86_64 Mac OS X. Upon npm install, the binaries for your platform will be downloaded. For more detailed installation instructions, or if you want to compile the project yourself, then please see the [Build & Install](build.md) documentation to build the project from source. - -# Prerequisites -- Node.js v0.12 or v4.2 -- ~100GB of disk storage -- ~4GB of RAM -- Mac OS X >= 10.9, Ubuntu >= 12.04 (libc >= 2.15 and libstdc++ >= 6.0.16) - -# Configuration -Bitcore includes a Command Line Interface (CLI) for managing, configuring and interfacing with your Bitcore Node. - -```bash -bitcore-node create -d mynode -cd mynode -bitcore-node install -bitcore-node install https://github.com/yourname/helloworld -``` - -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](services.md). - -To start bitcore as a daemon: - -```bash -bitcore start --daemon -``` - -# Add-on Services -There are several add-on services available to extend the functionality of Bitcore Node: -- [Insight API](https://github.com/bitpay/insight-api/tree/v0.3.0) -- [Insight UI](https://github.com/bitpay/insight/tree/v0.3.0) - -# Documentation -- [Services](services.md) - - [Bitcoind](services/bitcoind.md) - Native bindings to Bitcoin Core - - [Database](services/db.md) - The foundation API methods for getting information about blocks and transactions. - - [Address](services/address.md) - Adds additional API methods for querying and subscribing to events with bitcoin addresses. - - [Web](services/web.md) - Creates an express application over which services can expose their web/API content - -- [Build & Install](build.md) - How to build and install from source -- [Testing & Development](testing.md) - Developer guide for testing -- [Node](node.md) - Details on the node constructor -- [Bus](bus.md) - Overview of the event bus constructor -- [Errors](errors.md) - Reference for error handling and types -- [Patch](patch.md) - Information about the patch applied to Bitcoin Core -- [Release Process](release.md) - Information about verifying a release and the release process. diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 000000000..32d46ee88 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/docs/patch.md b/docs/patch.md deleted file mode 100644 index 86c366c69..000000000 --- a/docs/patch.md +++ /dev/null @@ -1,6 +0,0 @@ -# Static Library Patch -To provide native bindings to JavaScript _(or any other language for that matter)_, Bitcoin code, itself, must be linkable. Currently, Bitcoin Core provides a JSON RPC interface to bitcoind as well as a shared library for script validation _(and hopefully more)_ called libbitcoinconsensus. There is a node module, [node-libbitcoinconsensus](https://github.com/bitpay/node-libbitcoinconsensus), that exposes these methods. While these interfaces are useful for several use cases, there are additional use cases that are not fulfilled, and being able to implement customized interfaces is necessary. To be able to do this a few simple changes need to be made to Bitcoin Core to compile as a static library. - -The patch is located at `etc/bitcoin.patch` and adds a configure option `--enable-daemonlib` to compile all object files with `-fPIC` (Position Independent Code - needed to create a shared object), exposes leveldb variables and objects, exposes the threadpool to the bindings, and conditionally includes the main function. - -Every effort will be made to ensure that this patch stays up-to-date with the latest release of Bitcoin. At the very least, this project began supporting Bitcoin Core v0.11. diff --git a/docs/release.md b/docs/release.md index 29deb20f8..217d29fab 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,55 +1,29 @@ # Release Process -Binaries for the C++ binding file (which includes libbitcoind statically linked in) are distributed for convenience. The binary binding file `bitcoind.node` is signed and published to S3 for later download and installation. Source files can also be built if binaries are not desired. -## How to Verify Signatures - -``` -cd build/Release -gpg --verify bitcoind.node.sig bitcoind.node -``` - -To verify signatures, use the following PGP keys: -- @braydonf: [https://pgp.mit.edu/pks/lookup?op=get&search=0x9BBF07CAC07A276D](https://pgp.mit.edu/pks/lookup?op=get&search=0x9BBF07CAC07A276D) -- @kleetus: [https://pgp.mit.edu/pks/lookup?op=get&search=0x33195D27EF6BDB7F](https://pgp.mit.edu/pks/lookup?op=get&search=0x33195D27EF6BDB7F) -- @pnagurny: [https://pgp.mit.edu/pks/lookup?op=get&search=0x0909B33F0AA53013](https://pgp.mit.edu/pks/lookup?op=get&search=0x0909B33F0AA53013) +Binaries for bitcoind are distributed for convenience and built deterministically with Gitian, signatures for bitcoind are located at the [gitian.sigs](https://github.com/bitpay/gitian.sigs) respository. ## How to Release -Ensure you've followed the instructions in the README.md for building the project from source. When building for any platform, be sure to keep in mind the minimum supported C and C++ system libraries and build from source using this library. Example, Ubuntu 12.04 has the earliest system library for Linux that we support, so it would be easiest to build the Linux artifact using this version. You will be using node-gyp to build the C++ bindings. A script will then upload the bindings to S3 for later use. You will also need credentials for BitPay's bitcore-node S3 bucket and be listed as an author for the bitcore-node's npm module. -- Create a file `.bitcore-node-upload.json` in your home directory -- The format of this file should be: - -```json -{ - "region": "us-west-2", - "accessKeyId": "xxx", - "secretAccessKey": "yyy" -} -``` When publishing to npm, the .gitignore file is used to exclude files from the npm publishing process. Be sure that the bitcore-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). -To make a release, bump the `version` and `lastBuild` of the `package.json`: +To make a release, bump the `version` of the `package.json`: ```bash git checkout master git pull upstream master -git commit -a -m "Bump package version to " npm install -npm run package -npm run upload -npm publish -``` - -And then update the `version` of the `package.json` for development (e.g. "0.3.2-dev"): - -```bash -git commit -a -m "Bump development version to " +npm run test +npm run regtest +npm run jshint +git commit -a -m "Bump package version to " git push upstream master +npm publish ``` Create a release tag and push it to the BitPay Github repo: ```bash +git tag -s v -m 'v' git tag git push upstream ``` diff --git a/docs/services.md b/docs/services.md index 34f948f21..852713473 100644 --- a/docs/services.md +++ b/docs/services.md @@ -10,7 +10,7 @@ The `bitcore-node.json` file describes which services will load for a node: ```json { "services": [ - "bitcoind", "db", "address", "insight-api" + "bitcoind", "web" ] } ``` @@ -37,9 +37,7 @@ If, instead, you would like to run a custom node, you can include services by in var bitcore = require('bitcore-node'); //Services -var Address = bitcore.services.Address; var Bitcoin = bitcore.services.Bitcoin; -var DB = bitcore.services.DB; var Web = bitcore.services.Web; var myNode = new bitcore.Node({ @@ -48,21 +46,11 @@ var myNode = new bitcore.Node({ name: 'livenet' }, "services": [ - { - name: "address", - module: Address, - config: {} - }, { name: 'bitcoind', module: Bitcoin, config: {} }, - { - name: 'db', - module: DB, - config: {} - }, { name: 'web', module: Web, @@ -77,8 +65,8 @@ var myNode = new bitcore.Node({ 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. ```js -myNode.services.address.getBalance('1HB5XMLmzFVj8ALj6mfBsbifRoD4miY36v', false, function(err, total) { - console.log(total); //Satoshi amount of this address +myNode.services.bitcoind.getAddressBalance('1HB5XMLmzFVj8ALj6mfBsbifRoD4miY36v', false, function(err, total) { + console.log(total.balance); //Satoshi amount of this address }); ``` @@ -96,140 +84,3 @@ The `package.json` for the service module can either export the `Node.Service` d Please take a look at some of the existing services for implementation specifics. -### Adding an index -One quite useful feature exposed to services is the ability to index arbitrary data in the blockchain. To do so we make use of leveldb, a simple key-value store. As a service we can expose a 'blockHandler' function which is called each time a new block is added or removed from the blockchain. This gives us access to every new transaction received, allowing us to index them. Let's take a look at an example where we will index the time that a transaction was confirmed. - -```js -//Index prefix, so that we can determine the difference between our index -//and the indexes provided by other services -MyService.datePrefix = new Buffer('10', 'hex'); - -MyService.minPosition = new Buffer('00000', 'hex'); -MyService.maxPosition = new Buffer('99999', 'hex'); - -//This function is automatically called when a block is added or receieved -MyService.prototype.prototype.blockHandler = function(block, addOutput, callback) { - - //Determine if the block is added or removed, and therefore whether we are adding - //or deleting indexes - var databaseAction = 'put'; - if (!addOutput) { - databaseAction = 'del'; - } - - //An array of all leveldb operations we will be committing - var operations = []; - - //Timestamp of the current block - var blocktime = new Buffer(4); - blocktime.writeUInt32BE(block.header.time); - - for (var i = 0; i < block.transactions.length; i++) { - var transaction = block.transactions[i]; - var txid = new Buffer(transaction.id, 'hex'); - var position = new Buffer(('0000' + i).slice(-5), 'hex'); - - //To be able to query this txid by the block date we create an index, leading with the prefix we - //defined earlier, the the current blocktime, and finally a differentiator, in this case the index - //of this transaction in the block's transaction list - var indexOperation = { - type: databaseAction, - key: Buffer.concat([this.datePrefix, blockTime, position]), - value: txid - }; - - //Now we push this index into our list of operations that should be performed - operations.push(indexOperation); - } - - //Send the list of db operations back so they can be performed - setImmediate(function() { - callback(null, operations); - }); -}; -``` - -### Retrieving data using an index -With our block handler code every transaction in the blockchain will now be indexed. However, if we want to query this data we need to add a method to our service to expose it. - -```js - -MyService.prototype.getTransactionIdsByDate = function(startDateBuffer, endDateBuffer, callback) { - - var error; - var transactions = []; - - //Read data from leveldb which is between our startDate and endDate - var stream = this.node.services.db.store.createReadStream({ - gte: Buffer.concat([ - MyService.datePrefix, - startDateBuffer, - MyService.minPosition - ]), - lte: Buffer.concat([ - MyService.datePrefix, - endDateBuffer, - MyService.maxPosition - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - - stream.on('data', function(data) { - transactions.push(data.value.toString('hex')); - }); - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('close', function() { - if (error) { - return callback(error); - } - callback(null, transactions); - }); -}; -``` - -If you're new to leveldb and would like to better understand how createReadStream works you can find [more information here](https://github.com/Level/levelup#dbcreatereadstreamoptions). - -### Understanding indexes -You may notice there are several pieces to the index itself. Let's take a look at each piece to make them easier to understand. - -#### Prefixes -Since leveldb is just a simple key-value store we need something to differentiate which keys are part of which index. If we had two services trying to index on the same key, say a txid, they would overwrite each other and their queries would return results from the other index. By introducing a unique prefix per index type that we can prepend our indexes with prevents these collisions. - -```js -//A simple example of indexing the number of inputs and ouputs given a transaction id - -/** Wrong way **/ -var index1key = new Buffer(transaction.id, 'hex'); -var index1value = transaction.inputs.length; - -//Since this key has the same value it would just overwrite index1 when we write to the db -var index2key = new Buffer(transaction.id, 'hex'); -var index2value = transaction.outputs.length; - - -/** Right way **/ -var index1prefix = new Buffer('11', 'hex'); -var index2prefix = new Buffer('12', 'hex'); - -var index1key = Buffer.concat([index1prefix, new Buffer(transaction.id, 'hex')]); -var index1value = transaction.inputs.length; - -//Now that the keys are different, this won't overwrite the index -var index2key = Buffer.concat([index2prefix, new Buffer(transaction.id, 'hex')]); -var index2value = transaction.outputs.length; -``` - -Remember that all indexes are global, so check to make sure no other services you are using make use of the same prefix you plan to use in your service. We recommend documenting which prefixes you use and that you check for collisions with popular services if you plan to release your service for others to use. - -#### Index Key -The index key is the value you want to query by. This value should be deterministic so that it can be removed in the case of a [re-org](https://en.bitcoin.it/wiki/Chain_Reorganization) resulting in a block removal. The value should be unique, as no two indexes can be the same value. If you need two indexes with the same key value, consider adding a deterministic differentiator, such as a position in an array, or instead storing multiple values within the same index data. - -#### Index Data -This is the data which is returned when you search by the index's key. This can be whatever you would like to retrieve. Try to be efficient by not storing data that is already available elsewhere, such as storing a transaction ID instead of an entire transaction. diff --git a/docs/services/address.md b/docs/services/address.md deleted file mode 100644 index 7f46d3ad7..000000000 --- a/docs/services/address.md +++ /dev/null @@ -1,136 +0,0 @@ -# Address Service -The address service builds on the [Bitcoin Service](bitcoind.md) and the [Database Service](db.md) to add additional functionality for querying and subscribing to information based on bitcoin addresses. This will typically represent the core functionality for wallet applications. - -## API Documentation -These methods are exposed over the JSON-RPC interface and can be called directly from a node via: - -```js -node.services.address. -``` - -**Get Unspent Outputs** - -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: - -```js -var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; -var includeMempool = true; -node.services.address.getUnspentOutputs(address, includeMempool, function(err, unspentOutputs) { - // see below -}); -``` - -The `unspentOutputs` will have the format: - -```js -[ - { - address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW', - txid: '9d956c5d324a1c2b12133f3242deff264a9b9f61be701311373998681b8c1769', - outputIndex: 1, - height: 150, - satoshis: 1000000000, - script: '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac', - confirmations: 3 - } -] -``` - -**View Balances** - -```js -var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; -var includeMempool = true; -node.services.address.getBalance(address, includeMempool, function(err, balance) { - // balance will be in satoshis -}); -``` - -**View Address History** - -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. - -```js -var addresses = ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']; -var options = { - start: 345000, - end: 344000, - queryMempool: true -}; -node.services.address.getAddressHistory(addresses, options, function(err, history) { - // see below -}); -``` - -The history format will be: - -```js -{ - totalCount: 1, // The total number of items within "start" and "end" - items: [ - { - addresses: { - 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW': { - inputIndexes: [], - outputIndexes: [0] - } - }, - satoshis: 1000000000, - height: 150, // the block height of the transaction - confirmations: 3, - timestamp: 1442948127, // in seconds - fees: 191, - tx: // the populated transaction - } - ] -} -``` - -**View Address Summary** - -```js -var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; -var options = { - noTxList: false -}; - -node.services.address.getAddressSummary(address, options, function(err, summary) { - // see below -}); -``` - -The `summary` will have the format (values are in satoshis): - -```js -{ - totalReceived: 1000000000, - totalSpent: 0, - balance: 1000000000, - unconfirmedBalance: 1000000000, - appearances: 1, // number of transactions - unconfirmedAppearances: 0, - txids: [ - '3f7d13efe12e82f873f4d41f7e63bb64708fc4c942eb8c6822fa5bd7606adb00' - ] -} -``` - -## Events -For details on instantiating a bus for a node, see the [Bus Documentation](../bus.md). -- Name: `address/transaction`, Arguments: `[address, address...]` -- Name: `address/balance`, Arguments: `[address, address...]` - -**Examples:** - -```js -bus.subscribe('address/transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); -bus.subscribe('address/balance', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); - -bus.on('address/transaction', function(transaction) { - -}); - -bus.on('address/balance', function(balance) { - -}); -``` diff --git a/docs/services/bitcoind.md b/docs/services/bitcoind.md index 922965c45..d38f303e3 100644 --- a/docs/services/bitcoind.md +++ b/docs/services/bitcoind.md @@ -1,20 +1,120 @@ # Bitcoin Service -The Bitcoin Service adds a native [Node.js](https://nodejs.org) interface to [Bitcoin Core](https://github.com/bitcoin/bitcoin) for querying information about the Bitcoin blockchain. Bindings are linked to Bitcoin Core compiled as a static library. + +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/bitpay/bitcoin/tree/0.12-bitcore) 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. + +## Configuration + +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. + +```json + "servicesConfig": { + "bitcoind": { + "spawn": { + "datadir": "/home/bitcore/.bitcoin", + "exec": "/home/bitcore/bitcoin/src/bitcoind" + } + } + } +``` + +It's also possible to connect to separately managed `bitcoind` processes with round-robin quering, for example: + +```json + "servicesConfig": { + "bitcoind": { + "connect": [ + { + "rpchost": "127.0.0.1", + "rpcport": 30521, + "rpcuser": "bitcoin", + "rpcpassword": "local321", + "zmqpubrawtx": "tcp://127.0.0.1:30611" + }, + { + "rpchost": "127.0.0.1", + "rpcport": 30522, + "rpcuser": "bitcoin", + "rpcpassword": "local321", + "zmqpubrawtx": "tcp://127.0.0.1:30622" + }, + { + "rpchost": "127.0.0.1", + "rpcport": 30523, + "rpcuser": "bitcoin", + "rpcpassword": "local321", + "zmqpubrawtx": "tcp://127.0.0.1:30633" + } + ] + } + } +``` + +**Note**: For detailed example configuration see [`regtest/cluster.js`](regtest/cluster.js) + ## API Documentation -These methods are currently only available via directly interfacing with a node: +Methods are available by directly interfacing with the service: ```js node.services.bitcoind. ``` +### Chain + +**Getting Latest Blocks** + +```js +// gives the block hashes sorted from low to high within a range of timestamps +var high = 1460393372; // Mon Apr 11 2016 12:49:25 GMT-0400 (EDT) +var low = 1460306965; // Mon Apr 10 2016 12:49:25 GMT-0400 (EDT) +node.services.bitcoind.getBlockHashesByTimestamp(high, low, function(err, blockHashes) { + //... +}); + +// get the current tip of the chain +node.services.bitcoind.getBestBlockHash(function(err, blockHash) { + //... +}) +``` + +**Getting Synchronization and Node Status** + +```js +// gives a boolean if the daemon is fully synced (not the initial block download) +node.services.bitcoind.isSynced(function(err, synced) { + //... +}) + +// gives the current estimate of blockchain download as a percentage +node.services.bitcoind.syncPercentage(function(err, percent) { + //... +}); + +// gives information about the chain including total number of blocks +node.services.bitcoind.getInfo(function(err, info) { + //... +}); +``` + +**Generate Blocks** + +```js +// will generate a block for the "regtest" network (development purposes) +var numberOfBlocks = 10; +node.services.bitcoind.generateBlock(numberOfBlocks, function(err, blockHashes) { + //... +}); +``` + +### Blocks and Transactions + **Getting Block Information** -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 Bitcore: +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 Bitcore: ```js var blockHeight = 0; -node.services.bitcoind.getBlock(blockHeight, function(err, blockBuffer) { +node.services.bitcoind.getRawBlock(blockHeight, function(err, blockBuffer) { if (err) { throw err; } @@ -22,36 +122,43 @@ node.services.bitcoind.getBlock(blockHeight, function(err, blockBuffer) { console.log(block); }; -// check if the block is part of the main chain -var mainChain = node.services.bitcoind.isMainChain(block.hash); -console.log(mainChain); +// get a bitcore object of the block (as above) +node.services.bitcoind.getBlock(blockHash, function(err, block) { + //... +}; + +// get only the block header and index (including chain work, height, and previous hash) +node.services.bitcoind.getBlockHeader(blockHeight, function(err, blockHeader) { + //... +}); -// get only the block index (including chain work and previous hash) -var blockIndex = node.services.bitcoind.getBlockIndex(blockHeight); -console.log(blockIndex); +// get the block with a list of txids +node.services.bitcoind.getBlockOverview(blockHash, function(err, blockOverview) { + //... +}; ``` **Retrieving and Sending Transactions** -Get a transaction asynchronously by reading it from disk, with an argument to optionally not include the mempool: +Get a transaction asynchronously by reading it from disk: ```js var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; -var queryMempool = true; -node.services.bitcoind.getTransaction(txid, queryMempool, function(err, transactionBuffer) { +node.services.bitcoind.getRawTransaction(txid, function(err, transactionBuffer) { if (err) { throw err; } var transaction = bitcore.Transaction().fromBuffer(transactionBuffer); }); +// get a bitcore object of the transaction (as above) +node.services.bitcoind.getTransaction(txid, function(err, transaction) { + //... +}); -// also retrieve the block timestamp and height -node.services.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, info) { - console.log(info.blockHash); - console.log(info.height); - console.log(info.timestamp); // in seconds - var transaction = bitcore.Transaction().fromBuffer(transactionBuffer); +// retrieve the transaction with input values, fees, spent and block info +node.services.bitcoind.getDetailedTransaction(txid, function(err, transaction) { + //... }); ``` @@ -59,71 +166,157 @@ Send a transaction to the network: ```js var numberOfBlocks = 3; -var feesPerKilobyte = node.services.bitcoind.estimateFee(numberOfBlocks); // in satoshis +node.services.bitcoind.estimateFee(numberOfBlocks, function(err, feesPerKilobyte) { + //... +}); -try { - node.services.bitcoind.sendTransaction(transaction.serialize()); -} catch(err) { - // handle error -} +node.services.bitcoind.sendTransaction(transaction.serialize(), function(err, hash) { + //... +}); +``` + +### Addresses + +**Get Unspent Outputs** + +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: + +```js +var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; +node.services.bitcoind.getAddressUnspentOutputs(address, options, function(err, unspentOutputs) { + // see below +}); +``` + +The `unspentOutputs` will have the format: + +```js +[ + { + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW', + txid: '9d956c5d324a1c2b12133f3242deff264a9b9f61be701311373998681b8c1769', + outputIndex: 1, + height: 150, + satoshis: 1000000000, + script: '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac', + confirmations: 3 + } +] ``` -Get all of the transactions in the mempool: +**View Balances** ```js -var mempool = node.services.bitcoind.getMempoolTransactions(); -var transactions = []; -for (var i = 0; i < mempool.length; i++) { - transactions.push(bitcore.Transaction().fromBuffer(transactions[i])); +var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; +node.services.bitcoind.getAddressBalance(address, options, function(err, balance) { + // balance will be in satoshis with "received" and "balance" +}); +``` + +**View Address History** + +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. + +```js +var addresses = ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']; +var options = { + start: 345000, + end: 344000, + queryMempool: true +}; +node.services.bitcoind.getAddressHistory(addresses, options, function(err, history) { + // see below +}); +``` + +The history format will be: + +```js +{ + totalCount: 1, // The total number of items within "start" and "end" + items: [ + { + addresses: { + 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW': { + inputIndexes: [], + outputIndexes: [0] + } + }, + satoshis: 1000000000, + tx: // the same format as getDetailedTransaction + } + ] } ``` -Determine if an output is spent (excluding the mempool): +**View Address Summary** ```js -var spent = node.services.bitcoind.isSpent(txid, outputIndex); -console.log(spent); +var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; +var options = { + noTxList: false +}; + +node.services.bitcoind.getAddressSummary(address, options, function(err, summary) { + // see below +}); ``` -**Miscellaneous** -- `bitcoind.start(callback)` - Start the JavaScript Bitcoin node, the callback is called when the daemon is ready. -- `bitcoind.getInfo()` - Basic information about the chain including total number of blocks. -- `bitcoind.isSynced()` - Returns a boolean if the daemon is fully synced (not the initial block download) -- `bitcoind.syncPercentage()` - Returns the current estimate of blockchain download as a percentage. -- `bitcoind.stop(callback)` - Stop the JavaScript bitcoin node safely, the callback will be called when bitcoind is closed. This will also be done automatically on `process.exit`. It also takes the bitcoind node off the libuv event loop. If the daemon object is the only thing on the event loop. Node will simply close. +The `summary` will have the format (values are in satoshis): + +```js +{ + totalReceived: 1000000000, + totalSpent: 0, + balance: 1000000000, + unconfirmedBalance: 1000000000, + appearances: 1, // number of transactions + unconfirmedAppearances: 0, + txids: [ + '3f7d13efe12e82f873f4d41f7e63bb64708fc4c942eb8c6822fa5bd7606adb00' + ] +} +``` ## Events -The Bitcoin Service doesn't expose any events via the Bus, however there are a few events that can be directly registered: +The Bitcoin Service exposes two events via the Bus, and there are a few events that can be directly registered: ```js node.services.bitcoind.on('tip', function(blockHash) { - // a new block tip has been added + // a new block tip has been added, if there is a rapid update (with a second) this will not emit every tip update }); -node.services.bitcoind.on('tx', function(txInfo) { +node.services.bitcoind.on('tx', function(transactionBuffer) { // a new transaction has entered the mempool }); -node.services.bitcoind.on('txleave', function(txLeaveInfo) { - // a new transaction has left the mempool +node.services.bitcoind.on('block', function(blockHash) { + // a new block has been added }); ``` -The `txInfo` object will have the format: +For details on instantiating a bus for a node, see the [Bus Documentation](../bus.md). +- Name: `bitcoind/rawtransaction` +- Name: `bitcoind/hashblock` +- Name: `bitcoind/addresstxid`, Arguments: [address, address...] + +**Examples:** ```js -{ - buffer: , - mempool: true, // will currently always be true - hash: '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623' -} -``` +bus.subscribe('bitcoind/rawtransaction'); +bus.subscribe('bitcoind/hashblock'); +bus.subscribe('bitcoind/addresstxid', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); -The `txLeaveInfo` object will have the format: +bus.on('bitcoind/rawtransaction', function(transactionHex) { + //... +}); -```js -{ - buffer: , - hash: '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623' -} +bus.on('bitcoind/hashblock', function(blockhashHex) { + //... +}); + +bus.on('bitcoind/addresstxid', function(data) { + // data.address; + // data.txid; +}); ``` diff --git a/docs/services/db.md b/docs/services/db.md deleted file mode 100644 index 73a6cbb2e..000000000 --- a/docs/services/db.md +++ /dev/null @@ -1,113 +0,0 @@ -# Database Service -This service synchronizes a leveldb database with the [Bitcoin Service](bitcoind.md) block chain by connecting and disconnecting blocks to build new indexes that can be queried. Other services can extend the data that is indexed by implementing a `blockHandler` method, similar to the built-in [Address Service](address.md). - -## How to Reindex - -If you need to be able to recreate the database from historical transactions in blocks: -- Shutdown your node -- Remove the `bitcore-node.db` directory in the data directory (e.g. `~/.bitcore/bitcore-node.db`) -- Start your node again - -The database will then ask bitcoind for all the blocks again and recreate the database. This is sometimes required during upgrading as the format of the keys and values has changed. For "livenet" this can take half a day or more, for "testnet" this can take around an hour. - -## Adding Indexes -For a service to include additional block data, it can implement a `blockHandler` method that will be run to when there are new blocks added or removed. - -```js -CustomService.prototype.blockHandler = function(block, add, callback) { - var transactions = block.transactions; - var operations = []; - operations.push({ - type: add ? 'put' : 'del', - key: 'key', - value: 'value' - }); - callback(null, operations); -}; -``` - -Take a look at the Address Service implementation for more details about how to encode the key, value for the best efficiency and ways to format the keys for streaming reads. - -## API Documentation -These methods are exposed over the JSON-RPC interface and can be called directly from a node via: - -```js -node.services.db. -``` - -**Query Blocks by Date** - -One of the additional indexes created by the Database Service is querying for blocks by ranges of dates: - -```js -var newest = 1441914000; // Notice time is in seconds not milliseconds -var oldest = 1441911000; - -node.services.db.getBlockHashesByTimestamp(newest, oldest, function(err, hashes) { - // hashes will be an array of block hashes -}); -``` - -**Working with Blocks and Transactions as Bitcore Instances** - -```js - -var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e'; -var includeMempool = true; -node.services.db.getTransaction(txid, includeMempool, function(err, transaction) { - console.log(transaction.toObject()); -}); - -var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e'; -var includeMempool = true; -node.services.db.getTransactionWithBlockInfo(txid, includeMempool, function(err, transaction) { - console.log(transaction.toObject()); - console.log(transaction.__blockHash); - console.log(transaction.__height); - console.log(transaction.__timestamp); -}); - -var blockHash = '00000000d17332a156a807b25bc5a2e041d2c730628ceb77e75841056082a2c2'; -node.services.db.getBlock(blockHash, function(err, block) { - console.log(block.toObject()); -}); - -// contruct a transaction -var transaction = bitcore.Transaction(); - -node.services.db.sendTransaction(transaction, function(err) { - if (err) { - throw err; - } - // otherwise the transaction has been sent -}); -``` - -## Events -For details on instantiating a bus for a node, see the [Bus Documentation](../bus.md). -- Name: `db/transaction` -- Name: `db/block` - -**Examples:** - -```js -bus.subscribe('db/transaction'); -bus.subscribe('db/block'); - -bus.on('db/block', function(blockHash) { - // blockHash will be a hex string of the block hash -}); - -bus.on('db/transaction', function(txInfo) { - // see below -}); -``` - -The `txInfo` object will have the format: - -```js -{ - rejected: true, // If the transaction was rejected into the mempool - tx: // a Bitcore Transaction instance -} -``` diff --git a/docs/testing.md b/docs/testing.md deleted file mode 100644 index 30658bb62..000000000 --- a/docs/testing.md +++ /dev/null @@ -1,52 +0,0 @@ -# Development & Testing -To run all of the JavaScript tests: - -```bash -npm run test -``` - -To run tests against the bindings, as defined in `bindings.gyp` the regtest feature of Bitcoin Core is used, and to enable this feature we currently need to build with the wallet enabled _(not a part of the regular build)_. To do this, export an environment variable and recompile: - -```bash -export BITCORENODE_ENV=test -npm run build -``` - -If you do not already have mocha installed: - -```bash -npm install mocha -g -``` - -To run the integration tests: - -```bash -mocha -R spec integration/regtest.js -``` - -If any changes have been made to the bindings in the "src" directory, manually compile the Node.js bindings, as defined in `bindings.gyp`, you can run (-d for debug): - -```bash -$ node-gyp -d rebuild -``` - -Note: `node-gyp` can be installed with `npm install node-gyp -g` - -To be able to debug you'll need to have `gdb` and `node` compiled for debugging with gdb using `--gdb` (sometimes called node_g), and you can then run: - -```bash -$ gdb --args node examples/node.js -``` - -To run mocha from within gdb (notice `_mocha` and not `mocha` so that the tests run in the same process): - -```bash -$ gdb --args node /path/to/_mocha -R spec integration/regtest.js -``` - -To run the benchmarks: - -```bash -$ cd benchmarks -$ node index.js -``` diff --git a/docs/upgrade.md b/docs/upgrade.md new file mode 100644 index 000000000..c568554a2 --- /dev/null +++ b/docs/upgrade.md @@ -0,0 +1,77 @@ +# Upgrade Notes + +## From Bitcore 3.0.0 to 4.0.0 + +`bitcore-node@2.1.1` to `bitcore-node@3.0.0` + +This major upgrade includes changes to indexes, API methods and services. Please review below details before upgrading. + +### Indexes + +Indexes include *more information* and are now also *faster*. Because of this a **reindex will be necessary** when upgrading as the address and database indexes are now a part of bitcoind with three new `bitcoin.conf` options: +- `-addressindex` +- `-timestampindex` +- `-spentindex` + +To start reindexing add `reindex=1` during the **first startup only**. + +### Configuration Options + +- The `bitcoin.conf` file in will need to be updated to include additional indexes *(see below)*. +- The `datadir` option is now a part of `bitcoind` spawn configuration, and there is a new option to connect to multiple bitcoind processes (Please see [Bitcoin Service Docs](docs/services/bitcoind.md) for more details). The services `db` and `address` are now a part of the `bitcoind` service. Here is how to update `bitcore-node.json` configuration options: + +**Before**: +```json +{ + "datadir": "/home//.bitcoin", + "network": "livenet", + "port": 3001, + "services": [ + "address", + "bitcoind", + "db", + "web" + ] +} +``` + +**After**: +```json +{ + "network": "livenet", + "port": 3001, + "services": [ + "bitcoind", + "web" + ], + "servicesConfig": { + "bitcoind": { + "spawn": { + "datadir": "/home//.bitcoin", + "exec": "/home//bitcore-node/bin/bitcoind" + } + } + } +} +``` + +It will also be necessary to update `bitcoin.conf` settings, to include these fields: +``` +server=1 +whitelist=127.0.0.1 +txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +zmqpubrawtx=tcp://127.0.0.1: +zmqpubhashblock=tcp://127.0.0.1: +rpcallowip=127.0.0.1 +rpcuser= +rpcpassword= +``` + +**Important**: Once changes have been made you'll also need to add the `reindex=1` option **only for the first startup** to regenerate the indexes. Once this is complete you should be able to remove the `bitcore-node.db` directory with the old indexes. + +### API and Service Changes +- Many API methods that were a part of the `db` and `address` services are now a part of the `bitcoind` service. Please see [Bitcoin Service Docs](docs/services/bitcoind.md) for more details. +- The `db` and `address` services are deprecated, most of the functionality still exists. Any services that were extending indexes with the `db` service, will need to manage chain state itself, or build the indexes within `bitcoind`. \ No newline at end of file diff --git a/etc/bitcoin.patch b/etc/bitcoin.patch deleted file mode 100644 index d03b22d42..000000000 --- a/etc/bitcoin.patch +++ /dev/null @@ -1,387 +0,0 @@ -diff --git a/config_me.sh b/config_me.sh -new file mode 100644 -index 0000000..19e9a1b ---- /dev/null -+++ b/config_me.sh -@@ -0,0 +1 @@ -+./configure --enable-tests=no --enable-daemonlib --with-gui=no --without-qt --without-miniupnpc --without-bdb --enable-debug --disable-wallet --without-utils -diff --git a/configure.ac b/configure.ac -index 5debd21..3bfc59e 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -119,6 +119,12 @@ AC_ARG_ENABLE([reduce-exports], - [use_reduce_exports=$enableval], - [use_reduce_exports=no]) - -+AC_ARG_ENABLE([daemonlib], -+ [AS_HELP_STRING([--enable-daemonlib], -+ [compile all of bitcoind as a library (default is no)])], -+ [use_daemonlib=$enableval], -+ [use_daemonlib=no]) -+ - AC_ARG_ENABLE([ccache], - [AS_HELP_STRING([--enable-ccache], - [use ccache for building (default is yes if ccache is found)])], -@@ -402,6 +408,9 @@ fi - if test x$use_hardening != xno; then - AX_CHECK_COMPILE_FLAG([-Wstack-protector],[HARDENED_CXXFLAGS="$HARDENED_CXXFLAGS -Wstack-protector"]) - AX_CHECK_COMPILE_FLAG([-fstack-protector-all],[HARDENED_CXXFLAGS="$HARDENED_CXXFLAGS -fstack-protector-all"]) -+ if test x$use_daemonlib = xno; then -+ AX_CHECK_COMPILE_FLAG([-fPIE],[HARDENED_CXXFLAGS="$HARDENED_CXXFLAGS -fPIE"]) -+ fi - - AX_CHECK_PREPROC_FLAG([-D_FORTIFY_SOURCE=2],[ - AX_CHECK_PREPROC_FLAG([-U_FORTIFY_SOURCE],[ -@@ -415,7 +424,7 @@ if test x$use_hardening != xno; then - AX_CHECK_LINK_FLAG([[-Wl,-z,relro]], [HARDENED_LDFLAGS="$HARDENED_LDFLAGS -Wl,-z,relro"]) - AX_CHECK_LINK_FLAG([[-Wl,-z,now]], [HARDENED_LDFLAGS="$HARDENED_LDFLAGS -Wl,-z,now"]) - -- if test x$TARGET_OS != xwindows; then -+ if test x$TARGET_OS != xwindows -a x$use_daemonlib = xno; then - # All windows code is PIC, forcing it on just adds useless compile warnings - AX_CHECK_COMPILE_FLAG([-fPIE],[HARDENED_CXXFLAGS="$HARDENED_CXXFLAGS -fPIE"]) - AX_CHECK_LINK_FLAG([[-pie]], [HARDENED_LDFLAGS="$HARDENED_LDFLAGS -pie"]) -@@ -433,6 +442,17 @@ if test x$use_hardening != xno; then - OBJCXXFLAGS="$CXXFLAGS" - fi - -+AC_DEFINE([ENABLE_DAEMONLIB],[0],[Enable daemonlib.]) -+AM_CONDITIONAL([ENABLE_DAEMONLIB],[false]) -+if test x$use_daemonlib != xno; then -+ AX_CHECK_COMPILE_FLAG([-fPIC],[DAEMONLIB_CXXFLAGS="$DAEMONLIB_CXXFLAGS -fPIC"]) -+ AC_DEFINE([ENABLE_DAEMONLIB],[1],[Enable daemonlib.]) -+ AM_CONDITIONAL([ENABLE_DAEMONLIB],[true]) -+ CXXFLAGS="$CXXFLAGS $DAEMONLIB_CXXFLAGS" -+ CPPFLAGS="$CPPFLAGS $DAEMONLIB_CPPFLAGS" -+ OBJCXXFLAGS="$CXXFLAGS" -+fi -+ - dnl this flag screws up non-darwin gcc even when the check fails. special-case it. - if test x$TARGET_OS = xdarwin; then - AX_CHECK_LINK_FLAG([[-Wl,-dead_strip]], [LDFLAGS="$LDFLAGS -Wl,-dead_strip"]) -@@ -483,11 +503,18 @@ AC_LINK_IFELSE([AC_LANG_SOURCE([ - ] - ) - --if test x$use_reduce_exports = xyes; then -+if test x$use_reduce_exports = xyes -a x$use_daemonlib = xno; then - AX_CHECK_COMPILE_FLAG([-fvisibility=hidden],[RE_CXXFLAGS="-fvisibility=hidden"], - [AC_MSG_ERROR([Cannot set default symbol visibility. Use --disable-reduce-exports.])]) - fi - -+AC_MSG_CHECKING([whether to compile as daemonlib]) -+if test x$use_daemonlib != xno; then -+ AC_MSG_RESULT([yes]) -+else -+ AC_MSG_RESULT([no]) -+fi -+ - LEVELDB_CPPFLAGS= - LIBLEVELDB= - LIBMEMENV= -diff --git a/depends/hosts/linux.mk b/depends/hosts/linux.mk -index b13a0f1..0513394 100644 ---- a/depends/hosts/linux.mk -+++ b/depends/hosts/linux.mk -@@ -10,15 +10,15 @@ linux_debug_CXXFLAGS=$(linux_debug_CFLAGS) - linux_debug_CPPFLAGS=-D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC - - ifeq (86,$(findstring 86,$(build_arch))) --i686_linux_CC=gcc -m32 --i686_linux_CXX=g++ -m32 -+i686_linux_CC=${CC} -m32 -+i686_linux_CXX=${CXX} -m32 - i686_linux_AR=ar - i686_linux_RANLIB=ranlib - i686_linux_NM=nm - i686_linux_STRIP=strip - --x86_64_linux_CC=gcc -m64 --x86_64_linux_CXX=g++ -m64 -+x86_64_linux_CC=${CC} -m64 -+x86_64_linux_CXX=${CXX} -m64 - x86_64_linux_AR=ar - x86_64_linux_RANLIB=ranlib - x86_64_linux_NM=nm -diff --git a/depends/packages/bdb.mk b/depends/packages/bdb.mk -index 68841af..65a105b 100644 ---- a/depends/packages/bdb.mk -+++ b/depends/packages/bdb.mk -@@ -9,6 +9,7 @@ define $(package)_set_vars - $(package)_config_opts=--disable-shared --enable-cxx --disable-replication - $(package)_config_opts_mingw32=--enable-mingw - $(package)_config_opts_linux=--with-pic -+$(package)_cxxflags_darwin=-stdlib=libc++ - endef - - define $(package)_preprocess_cmds -diff --git a/depends/packages/boost.mk b/depends/packages/boost.mk -index e7aa48d..df0f7ae 100644 ---- a/depends/packages/boost.mk -+++ b/depends/packages/boost.mk -@@ -1,9 +1,8 @@ - package=boost --$(package)_version=1_55_0 --$(package)_download_path=http://sourceforge.net/projects/boost/files/boost/1.55.0 -+$(package)_version=1_57_0 -+$(package)_download_path=http://sourceforge.net/projects/boost/files/boost/1.57.0 - $(package)_file_name=$(package)_$($(package)_version).tar.bz2 --$(package)_sha256_hash=fff00023dd79486d444c8e29922f4072e1d451fc5a4d2b6075852ead7f2b7b52 --$(package)_patches=darwin_boost_atomic-1.patch darwin_boost_atomic-2.patch gcc_5_no_cxx11.patch -+$(package)_sha256_hash=910c8c022a33ccec7f088bd65d4f14b466588dda94ba2124e78b8c57db264967 - - define $(package)_set_vars - $(package)_config_opts_release=variant=release -@@ -11,7 +10,7 @@ $(package)_config_opts_debug=variant=debug - $(package)_config_opts=--layout=tagged --build-type=complete --user-config=user-config.jam - $(package)_config_opts+=threading=multi link=static -sNO_BZIP2=1 -sNO_ZLIB=1 - $(package)_config_opts_linux=threadapi=pthread runtime-link=shared --$(package)_config_opts_darwin=--toolset=darwin-4.2.1 runtime-link=shared -+$(package)_config_opts_darwin=--toolset=clang runtime-link=shared - $(package)_config_opts_mingw32=binary-format=pe target-os=windows threadapi=win32 runtime-link=static - $(package)_config_opts_x86_64_mingw32=address-model=64 - $(package)_config_opts_i686_mingw32=address-model=32 -@@ -20,15 +19,14 @@ $(package)_toolset_$(host_os)=gcc - $(package)_archiver_$(host_os)=$($(package)_ar) - $(package)_toolset_darwin=darwin - $(package)_archiver_darwin=$($(package)_libtool) --$(package)_config_libraries=chrono,filesystem,program_options,system,thread,test --$(package)_cxxflags=-fvisibility=hidden --$(package)_cxxflags_linux=-fPIC -+$(package)_config_libraries=chrono,filesystem,program_options,system,thread -+$(package)_cxxflags=-fvisibility=default -fPIC -+$(package)_cxxflags_darwin=-std=c++11 -stdlib=libc++ -+$(package)_linkflags=-stdlib=libc++ - endef - -+ - define $(package)_preprocess_cmds -- patch -p2 < $($(package)_patch_dir)/darwin_boost_atomic-1.patch && \ -- patch -p2 < $($(package)_patch_dir)/darwin_boost_atomic-2.patch && \ -- patch -p2 < $($(package)_patch_dir)/gcc_5_no_cxx11.patch && \ - echo "using $(boost_toolset_$(host_os)) : : $($(package)_cxx) : \"$($(package)_cxxflags) $($(package)_cppflags)\" \"$($(package)_ldflags)\" \"$(boost_archiver_$(host_os))\" \"$(host_STRIP)\" \"$(host_RANLIB)\" \"$(host_WINDRES)\" : ;" > user-config.jam - endef - -diff --git a/src/Makefile.am b/src/Makefile.am -index 2461f82..e7e9ecf 100644 ---- a/src/Makefile.am -+++ b/src/Makefile.am -@@ -1,6 +1,10 @@ - DIST_SUBDIRS = secp256k1 - AM_LDFLAGS = $(PTHREAD_CFLAGS) $(LIBTOOL_LDFLAGS) - -+noinst_LTLIBRARIES = -+libbitcoind_la_LIBADD = -+libbitcoind_la_LDFLAGS = -no-undefined -+STATIC_EXTRA_LIBS = $(LIBLEVELDB) $(LIBMEMENV) - - if EMBEDDED_LEVELDB - LEVELDB_CPPFLAGS += -I$(srcdir)/leveldb/include -@@ -49,16 +53,16 @@ BITCOIN_INCLUDES += $(BDB_CPPFLAGS) - EXTRA_LIBRARIES += libbitcoin_wallet.a - endif - --if BUILD_BITCOIN_LIBS --lib_LTLIBRARIES = libbitcoinconsensus.la --LIBBITCOIN_CONSENSUS=libbitcoinconsensus.la --else --LIBBITCOIN_CONSENSUS= --endif -- -+LIBBITCOIN_CONSENSUS = - bin_PROGRAMS = - TESTS = - -+if BUILD_BITCOIN_LIBS -+noinst_LTLIBRARIES += libbitcoinconsensus.la -+LIBBITCOIN_CONSENSUS += libbitcoinconsensus.la -+endif -+ -+if !ENABLE_DAEMONLIB - if BUILD_BITCOIND - bin_PROGRAMS += bitcoind - endif -@@ -66,6 +70,9 @@ endif - if BUILD_BITCOIN_UTILS - bin_PROGRAMS += bitcoin-cli bitcoin-tx - endif -+else -+noinst_LTLIBRARIES += libbitcoind.la -+endif - - .PHONY: FORCE - # bitcoin core # -@@ -170,8 +177,11 @@ obj/build.h: FORCE - @$(MKDIR_P) $(builddir)/obj - @$(top_srcdir)/share/genbuild.sh $(abs_top_builddir)/src/obj/build.h \ - $(abs_top_srcdir) --libbitcoin_util_a-clientversion.$(OBJEXT): obj/build.h - -+ARCH_PLATFORM = $(shell ../../bin/variables.sh host) -+ -+libbitcoin_util_a-clientversion.$(OBJEXT): obj/build.h -+clientversion.cpp: obj/build.h - # server: shared between bitcoind and bitcoin-qt - libbitcoin_server_a_CPPFLAGS = $(BITCOIN_INCLUDES) $(MINIUPNPC_CPPFLAGS) - libbitcoin_server_a_SOURCES = \ -@@ -310,9 +320,18 @@ nodist_libbitcoin_util_a_SOURCES = $(srcdir)/obj/build.h - bitcoind_SOURCES = bitcoind.cpp - bitcoind_CPPFLAGS = $(BITCOIN_INCLUDES) - bitcoind_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) -+libbitcoind_la_SOURCES = bitcoind.cpp -+libbitcoind_la_SOURCES += $(libbitcoin_util_a_SOURCES) -+libbitcoind_la_SOURCES += $(libbitcoin_univalue_a_SOURCES) -+libbitcoind_la_SOURCES += $(libbitcoin_crypto_a_SOURCES) -+libbitcoind_la_SOURCES += $(libbitcoin_common_a_SOURCES) -+libbitcoind_la_SOURCES += $(libbitcoin_server_a_SOURCES) -+libbitcoind_la_SOURCES += $(crypto_libbitcoin_crypto_a_SOURCES) -+libbitcoind_la_SOURCES += $(univalue_libbitcoin_univalue_a_SOURCES) - - if TARGET_WINDOWS - bitcoind_SOURCES += bitcoind-res.rc -+libbitcoind_la_SOURCES += bitcoind-res.rc - endif - - bitcoind_LDADD = \ -@@ -327,10 +346,17 @@ bitcoind_LDADD = \ - - if ENABLE_WALLET - bitcoind_LDADD += libbitcoin_wallet.a -+libbitcoind_la_SOURCES += $(libbitcoin_wallet_a_SOURCES) - endif - - bitcoind_LDADD += $(BOOST_LIBS) $(BDB_LIBS) $(SSL_LIBS) $(CRYPTO_LIBS) $(MINIUPNPC_LIBS) --# -+libbitcoind_la_LIBADD += $(SSL_LIBS) $(LIBSECP256K1) $(CRYPTO_LIBS) $(STATIC_EXTRA_LIBS) -+libbitcoind_la_CPPFLAGS = $(BITCOIN_INCLUDES) -+if TARGET_DARWIN -+libbitcoind_la_LDFLAGS += -Wl,-all_load -+else -+libbitcoind_la_LDFLAGS += -Wl,--whole-archive $(STATIC_EXTRA_LIBS) -Wl,--no-whole-archive -+endif - - # bitcoin-cli binary # - bitcoin_cli_SOURCES = bitcoin-cli.cpp -diff --git a/src/bitcoind.cpp b/src/bitcoind.cpp -index 6e2758a..0352a9d 100644 ---- a/src/bitcoind.cpp -+++ b/src/bitcoind.cpp -@@ -33,6 +33,10 @@ - - static bool fDaemon; - -+#if ENABLE_DAEMONLIB -+extern void WaitForShutdown(boost::thread_group* threadGroup); -+#endif -+ - void WaitForShutdown(boost::thread_group* threadGroup) - { - bool fShutdown = ShutdownRequested(); -@@ -166,6 +170,7 @@ bool AppInit(int argc, char* argv[]) - return fRet; - } - -+#if !ENABLE_DAEMONLIB - int main(int argc, char* argv[]) - { - SetupEnvironment(); -@@ -175,3 +180,4 @@ int main(int argc, char* argv[]) - - return (AppInit(argc, argv) ? 0 : 1); - } -+#endif -diff --git a/src/init.cpp b/src/init.cpp -index a04e4e0..33d0bc7 100644 ---- a/src/init.cpp -+++ b/src/init.cpp -@@ -638,21 +638,6 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) - umask(077); - } - -- // Clean shutdown on SIGTERM -- struct sigaction sa; -- sa.sa_handler = HandleSIGTERM; -- sigemptyset(&sa.sa_mask); -- sa.sa_flags = 0; -- sigaction(SIGTERM, &sa, NULL); -- sigaction(SIGINT, &sa, NULL); -- -- // Reopen debug.log on SIGHUP -- struct sigaction sa_hup; -- sa_hup.sa_handler = HandleSIGHUP; -- sigemptyset(&sa_hup.sa_mask); -- sa_hup.sa_flags = 0; -- sigaction(SIGHUP, &sa_hup, NULL); -- - #if defined (__SVR4) && defined (__sun) - // ignore SIGPIPE on Solaris - signal(SIGPIPE, SIG_IGN); -diff --git a/src/init.h b/src/init.h -index dcb2b29..5ce68ba 100644 ---- a/src/init.h -+++ b/src/init.h -@@ -18,6 +18,11 @@ class thread_group; - - extern CWallet* pwalletMain; - -+#if ENABLE_DAEMONLIB -+#include -+#include -+#endif -+ - void StartShutdown(); - bool ShutdownRequested(); - void Shutdown(); -diff --git a/src/main.cpp b/src/main.cpp -index fe072ec..9f677cf 100644 ---- a/src/main.cpp -+++ b/src/main.cpp -@@ -1105,6 +1105,7 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa - - // Store transaction in memory - pool.addUnchecked(hash, entry, !IsInitialBlockDownload()); -+ GetNodeSignals().TxToMemPool(tx); - } - - SyncWithWallets(tx, NULL); -diff --git a/src/net.cpp b/src/net.cpp -index e4b22f9..33fe6f9 100644 ---- a/src/net.cpp -+++ b/src/net.cpp -@@ -432,8 +432,10 @@ void CNode::PushVersion() - LogPrint("net", "send version message: version %d, blocks=%d, us=%s, them=%s, peer=%d\n", PROTOCOL_VERSION, nBestHeight, addrMe.ToString(), addrYou.ToString(), id); - else - LogPrint("net", "send version message: version %d, blocks=%d, us=%s, peer=%d\n", PROTOCOL_VERSION, nBestHeight, addrMe.ToString(), id); -+ std::vector bitcore; -+ bitcore.push_back("bitcore"); //the dash character is removed from the comments section - PushMessage("version", PROTOCOL_VERSION, nLocalServices, nTime, addrYou, addrMe, -- nLocalHostNonce, FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, std::vector()), nBestHeight, true); -+ nLocalHostNonce, FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, bitcore), nBestHeight, true); - } - - -diff --git a/src/net.h b/src/net.h -index 17502b9..c9ae1b2 100644 ---- a/src/net.h -+++ b/src/net.h -@@ -99,6 +99,8 @@ struct CNodeSignals - { - boost::signals2::signal GetHeight; - boost::signals2::signal ProcessMessages; -+ boost::signals2::signal TxToMemPool; -+ boost::signals2::signal TxLeaveMemPool; - boost::signals2::signal SendMessages; - boost::signals2::signal InitializeNode; - boost::signals2::signal FinalizeNode; -diff --git a/src/txmempool.cpp b/src/txmempool.cpp -index c3d1b60..03e265d 100644 ---- a/src/txmempool.cpp -+++ b/src/txmempool.cpp -@@ -133,6 +133,7 @@ void CTxMemPool::remove(const CTransaction &origTx, std::list& rem - if (!mapTx.count(hash)) - continue; - const CTransaction& tx = mapTx[hash].GetTx(); -+ GetNodeSignals().TxLeaveMemPool(tx); - if (fRecursive) { - for (unsigned int i = 0; i < tx.vout.size(); i++) { - std::map::iterator it = mapNextTx.find(COutPoint(hash, i)); diff --git a/example/client.js b/example/client.js deleted file mode 100644 index ded2b61e1..000000000 --- a/example/client.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -var socket = require('socket.io-client')('http://localhost:3000'); -socket.on('connect', function(){ - console.log('connected'); -}); - -socket.on('disconnect', function(){ - console.log('disconnected'); -}); - -var message = { - method: 'getOutputs', - params: ['2NChMRHVCxTPq9KeyvHQUSbfLaQY55Zzzp8', true] -}; - -socket.send(message, function(response) { - if(response.error) { - console.log('Error', response.error); - return; - } - - console.log(response.result); -}); - -var message2 = { - method: 'getTransaction', - params: ['4f793f67fc7465f14fa3a8d3727fa7d133cdb2f298234548b94a5f08b6f4103e', true] -}; - -socket.send(message2, function(response) { - if(response.error) { - console.log('Error', response.error); - return; - } - - console.log(response.result); -}); - -socket.on('transaction', function(obj) { - console.log(JSON.stringify(obj, null, 2)); -}); - -socket.on('address/transaction', function(obj) { - console.log(JSON.stringify(obj, null, 2)); -}); - -socket.emit('subscribe', 'transaction'); -socket.emit('subscribe', 'address/transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); \ No newline at end of file diff --git a/index.js b/index.js index 4b2aa56a6..6626fe18b 100644 --- a/index.js +++ b/index.js @@ -1,26 +1,12 @@ 'use strict'; -var semver = require('semver'); -var packageData = require('./package.json'); - -function nodeVersionCheck(version, expected) { - if (!semver.satisfies(version, expected)) { - throw new Error('Node.js version ' + version + ' is expected to be ' + expected); - } -} -nodeVersionCheck(process.versions.node, packageData.engines.node); - module.exports = require('./lib'); -module.exports.nodeVersionCheck = nodeVersionCheck; module.exports.Node = require('./lib/node'); -module.exports.Transaction = require('./lib/transaction'); module.exports.Service = require('./lib/service'); module.exports.errors = require('./lib/errors'); module.exports.services = {}; -module.exports.services.Address = require('./lib/services/address'); module.exports.services.Bitcoin = require('./lib/services/bitcoind'); -module.exports.services.DB = require('./lib/services/db'); module.exports.services.Web = require('./lib/services/web'); module.exports.scaffold = {}; diff --git a/integration/data/bitcoin.conf b/integration/data/bitcoin.conf deleted file mode 100644 index ac6a78122..000000000 --- a/integration/data/bitcoin.conf +++ /dev/null @@ -1,9 +0,0 @@ -server=1 -whitelist=127.0.0.1 -rpcssl=1 -rpcsslcertificatechainfile=../bitcoind.crt -rpcsslprivatekeyfile=../bitcoind_no_pass.key -txindex=1 -rpcallowip=127.0.0.1 -rpcuser=bitcoin -rpcpassword=local321 diff --git a/integration/regtest.js b/integration/regtest.js deleted file mode 100644 index e7dba7a82..000000000 --- a/integration/regtest.js +++ /dev/null @@ -1,493 +0,0 @@ -'use strict'; - -// These tests require bitcore-node Bitcoin Core bindings to be compiled with -// the environment variable BITCORENODE_ENV=test. This enables the use of regtest -// functionality by including the wallet in the build. -// To run the tests: $ mocha -R spec integration/regtest.js - -var index = require('..'); -var log = index.log; - -if (process.env.BITCORENODE_ENV !== 'test') { - log.info('Please set the environment variable BITCORENODE_ENV=test and make sure bindings are compiled for testing'); - process.exit(); -} - -var chai = require('chai'); -var bitcore = require('bitcore-lib'); -var BN = bitcore.crypto.BN; -var async = require('async'); -var rimraf = require('rimraf'); -var bitcoind; - -/* jshint unused: false */ -var should = chai.should(); -var assert = chai.assert; -var sinon = require('sinon'); -var BitcoinRPC = require('bitcoind-rpc'); -var transactionData = []; -var blockHashes = []; -var utxos; -var client; -var coinbasePrivateKey; -var privateKey = bitcore.PrivateKey(); -var destKey = bitcore.PrivateKey(); - -describe('Daemon Binding Functionality', function() { - - before(function(done) { - this.timeout(30000); - - // Add the regtest network - bitcore.Networks.remove(bitcore.Networks.testnet); - bitcore.Networks.add({ - name: 'regtest', - alias: 'regtest', - pubkeyhash: 0x6f, - privatekey: 0xef, - scripthash: 0xc4, - xpubkey: 0x043587cf, - xprivkey: 0x04358394, - networkMagic: 0xfabfb5da, - port: 18444, - dnsSeeds: [ ] - }); - - var datadir = __dirname + '/data'; - - rimraf(datadir + '/regtest', function(err) { - - if (err) { - throw err; - } - - bitcoind = require('../').services.Bitcoin({ - node: { - datadir: datadir, - network: { - name: 'regtest' - } - } - }); - - bitcoind.on('error', function(err) { - log.error('error="%s"', err.message); - }); - - log.info('Waiting for Bitcoin Core to initialize...'); - - bitcoind.start(function() { - log.info('Bitcoind started'); - - client = new BitcoinRPC({ - protocol: 'https', - host: '127.0.0.1', - port: 18332, - user: 'bitcoin', - pass: 'local321', - rejectUnauthorized: false - }); - - log.info('Generating 100 blocks...'); - - // Generate enough blocks so that the initial coinbase transactions - // can be spent. - - setImmediate(function() { - client.generate(150, function(err, response) { - if (err) { - throw err; - } - blockHashes = response.result; - - log.info('Preparing test data...'); - - // Get all of the unspent outputs - client.listUnspent(0, 150, function(err, response) { - utxos = response.result; - - async.mapSeries(utxos, function(utxo, next) { - async.series([ - function(finished) { - // Load all of the transactions for later testing - client.getTransaction(utxo.txid, function(err, txresponse) { - if (err) { - throw err; - } - // add to the list of transactions for testing later - transactionData.push(txresponse.result.hex); - finished(); - }); - }, - function(finished) { - // Get the private key for each utxo - client.dumpPrivKey(utxo.address, function(err, privresponse) { - if (err) { - throw err; - } - utxo.privateKeyWIF = privresponse.result; - finished(); - }); - } - ], next); - }, function(err) { - if (err) { - throw err; - } - done(); - }); - }); - }); - }); - }); - }); - }); - - after(function(done) { - this.timeout(20000); - bitcoind.stop(function(err, result) { - done(); - }); - }); - - describe('get blocks by hash', function() { - - [0,1,2,3,5,6,7,8,9].forEach(function(i) { - it('generated block ' + i, function(done) { - bitcoind.getBlock(blockHashes[i], function(err, response) { - if (err) { - throw err; - } - should.exist(response); - var block = bitcore.Block.fromBuffer(response); - block.hash.should.equal(blockHashes[i]); - done(); - }); - }); - }); - }); - - describe('get blocks by height', function() { - - [0,1,2,3,4,5,6,7,8,9].forEach(function(i) { - it('generated block ' + i, function(done) { - // add the genesis block - var height = i + 1; - bitcoind.getBlock(i + 1, function(err, response) { - if (err) { - throw err; - } - should.exist(response); - var block = bitcore.Block.fromBuffer(response); - block.hash.should.equal(blockHashes[i]); - done(); - }); - }); - }); - - it('will get error with number greater than tip', function(done) { - bitcoind.getBlock(1000000000, function(err, response) { - should.exist(err); - done(); - }); - }); - - }); - - describe('get transactions by hash', function() { - [0,1,2,3,4,5,6,7,8,9].forEach(function(i) { - it('for tx ' + i, function(done) { - var txhex = transactionData[i]; - var tx = new bitcore.Transaction(); - tx.fromString(txhex); - bitcoind.getTransaction(tx.hash, true, function(err, response) { - if (err) { - throw err; - } - assert(response.toString('hex') === txhex, 'incorrect tx data result'); - done(); - }); - }); - }); - - it('will return null if the transaction does not exist', function(done) { - var txid = '6226c407d0e9705bdd7158e60983e37d0f5d23529086d6672b07d9238d5aa618'; - bitcoind.getTransaction(txid, true, function(err, response) { - if (err) { - throw err; - } - should.not.exist(response); - done(); - }); - }); - - }); - - describe('get block index', function() { - var expectedWork = new BN(6); - [1,2,3,4,5,6,7,8,9].forEach(function(i) { - it('generate block ' + i, function() { - var blockIndex = bitcoind.getBlockIndex(blockHashes[i]); - should.exist(blockIndex); - should.exist(blockIndex.chainWork); - var work = new BN(blockIndex.chainWork, 'hex'); - work.cmp(expectedWork).should.equal(0); - expectedWork = expectedWork.add(new BN(2)); - should.exist(blockIndex.prevHash); - blockIndex.hash.should.equal(blockHashes[i]); - blockIndex.prevHash.should.equal(blockHashes[i - 1]); - blockIndex.height.should.equal(i + 1); - }); - }); - it('will get null prevHash for the genesis block', function() { - var blockIndex = bitcoind.getBlockIndex(0); - should.exist(blockIndex); - should.equal(blockIndex.prevHash, null); - }); - }); - - describe('get block index by height', function() { - var expectedWork = new BN(6); - [2,3,4,5,6,7,8,9].forEach(function(i) { - it('generate block ' + i, function() { - var blockIndex = bitcoind.getBlockIndex(i); - should.exist(blockIndex); - should.exist(blockIndex.chainWork); - var work = new BN(blockIndex.chainWork, 'hex'); - work.cmp(expectedWork).should.equal(0); - expectedWork = expectedWork.add(new BN(2)); - should.exist(blockIndex.prevHash); - blockIndex.hash.should.equal(blockHashes[i - 1]); - blockIndex.prevHash.should.equal(blockHashes[i - 2]); - blockIndex.height.should.equal(i); - }); - }); - it('will get null with number greater than tip', function(done) { - var index = bitcoind.getBlockIndex(100000); - should.equal(index, null); - done(); - }); - }); - - describe('isMainChain', function() { - [1,2,3,4,5,6,7,8,9].forEach(function(i) { - it('block ' + i + ' is on the main chain', function() { - bitcoind.isMainChain(blockHashes[i]).should.equal(true); - }); - }); - }); - - describe('send transaction functionality', function() { - - it('will not error and return the transaction hash', function() { - - // create and sign the transaction - var tx = bitcore.Transaction(); - tx.from(utxos[0]); - tx.change(privateKey.toAddress()); - tx.to(destKey.toAddress(), utxos[0].amount * 1e8 - 1000); - tx.sign(bitcore.PrivateKey.fromWIF(utxos[0].privateKeyWIF)); - - // test sending the transaction - var hash = bitcoind.sendTransaction(tx.serialize()); - hash.should.equal(tx.hash); - }); - - it('will throw an error if an unsigned transaction is sent', function() { - - var tx = bitcore.Transaction(); - tx.from(utxos[1]); - tx.change(privateKey.toAddress()); - tx.to(destKey.toAddress(), utxos[1].amount * 1e8 - 1000); - (function() { - bitcoind.sendTransaction(tx.uncheckedSerialize()); - }).should.throw('\x10: mandatory-script-verify-flag-failed (Operation not valid with the current stack size)'); - }); - - it('will throw an error for unexpected types', function() { - var garbage = new Buffer('abcdef', 'hex'); - (function() { - bitcoind.sendTransaction(garbage); - }).should.throw('TX decode failed'); - - var num = 23; - (function() { - bitcoind.sendTransaction(num); - }).should.throw('TX decode failed'); - }); - - it('will emit "tx" events', function(done) { - var tx = bitcore.Transaction(); - tx.from(utxos[2]); - tx.change(privateKey.toAddress()); - tx.to(destKey.toAddress(), utxos[2].amount * 1e8 - 1000); - tx.sign(bitcore.PrivateKey.fromWIF(utxos[2].privateKeyWIF)); - - var serialized = tx.serialize(); - - bitcoind.once('tx', function(result) { - result.buffer.toString('hex').should.equal(serialized); - result.hash.should.equal(tx.hash); - result.mempool.should.equal(true); - done(); - }); - bitcoind.sendTransaction(serialized); - }); - - }); - - describe('fee estimation', function() { - it('will estimate fees', function() { - var fees = bitcoind.estimateFee(); - fees.should.equal(-1); - }); - }); - - describe('tip updates', function() { - it('will get an event when the tip is new', function(done) { - this.timeout(4000); - bitcoind.on('tip', function(height) { - if (height == 151) { - height.should.equal(151); - done(); - } - }); - client.generate(1, function(err, response) { - if (err) { - throw err; - } - }); - }); - }); - - describe('transactions leaving the mempool', function() { - it('receive event when transaction leaves', function(done) { - - // add transaction to build a new block - var tx = bitcore.Transaction(); - tx.from(utxos[4]); - tx.change(privateKey.toAddress()); - tx.to(destKey.toAddress(), utxos[4].amount * 1e8 - 1000); - tx.sign(bitcore.PrivateKey.fromWIF(utxos[4].privateKeyWIF)); - bitcoind.sendTransaction(tx.serialize()); - - bitcoind.once('txleave', function(txInfo) { - txInfo.hash.should.equal(tx.hash); - done(); - }); - - client.generate(1, function(err, response) { - if (err) { - throw err; - } - }); - }); - }); - - describe('mempool functionality', function() { - - var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1'; - var utxo1 = { - address: fromAddress, - txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', - outputIndex: 0, - script: bitcore.Script.buildPublicKeyHashOut(fromAddress).toString(), - satoshis: 100000 - }; - var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc'; - var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up'; - var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf'; - var privateKey1 = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY'; - var private1 = '6ce7e97e317d2af16c33db0b9270ec047a91bff3eff8558afb5014afb2bb5976'; - var private2 = 'c9b26b0f771a0d2dad88a44de90f05f416b3b385ff1d989343005546a0032890'; - var tx = new bitcore.Transaction(); - tx.from(utxo1); - tx.to(toAddress, 50000); - tx.change(changeAddress); - tx.sign(privateKey1); - - var tx2; - var tx2Key; - - before(function() { - tx2 = bitcore.Transaction(); - tx2.from(utxos[3]); - tx2.change(privateKey.toAddress()); - tx2.to(destKey.toAddress(), utxos[3].amount * 1e8 - 1000); - tx2Key = bitcore.PrivateKey.fromWIF(utxos[3].privateKeyWIF); - tx2.sign(tx2Key); - }); - - it('will add an unchecked transaction', function() { - var added = bitcoind.addMempoolUncheckedTransaction(tx.serialize()); - added.should.equal(true); - bitcoind.getTransaction(tx.hash, true, function(err, txBuffer) { - if(err) { - throw err; - } - var expected = tx.toBuffer().toString('hex'); - txBuffer.toString('hex').should.equal(expected); - }); - - }); - - it('get one transaction', function() { - var transactions = bitcoind.getMempoolTransactions(); - transactions[0].toString('hex').should.equal(tx.serialize()); - }); - - it('get multiple transactions', function() { - bitcoind.sendTransaction(tx2.serialize()); - var transactions = bitcoind.getMempoolTransactions(); - var expected = [tx.serialize(), tx2.serialize()]; - expected.should.contain(transactions[0].toString('hex')); - expected.should.contain(transactions[1].toString('hex')); - }); - - }); - - describe('get transaction with block info', function() { - it('should include tx buffer, height and timestamp', function(done) { - bitcoind.getTransactionWithBlockInfo(utxos[0].txid, true, function(err, data) { - should.not.exist(err); - should.exist(data.height); - data.height.should.be.a('number'); - should.exist(data.timestamp); - should.exist(data.buffer); - done(); - }); - }); - }); - - describe('get next block hash', function() { - it('will get next block hash', function() { - var nextBlockHash = bitcoind.getNextBlockHash(blockHashes[0]); - nextBlockHash.should.equal(blockHashes[1]); - var nextnextBlockHash = bitcoind.getNextBlockHash(nextBlockHash); - nextnextBlockHash.should.equal(blockHashes[2]); - }); - - it('will get a null response if the tip hash is provided', function() { - var bestBlockHash = bitcoind.getBestBlockHash(); - var nextBlockHash = bitcoind.getNextBlockHash(bestBlockHash); - should.not.exist(nextBlockHash); - }); - }); - - describe('#getInfo', function() { - it('will get information', function() { - var info = bitcoind.getInfo(); - info.network.should.equal('regtest'); - should.exist(info); - should.exist(info.version); - should.exist(info.blocks); - should.exist(info.timeoffset); - should.exist(info.connections); - should.exist(info.difficulty); - should.exist(info.testnet); - should.exist(info.relayfee); - should.exist(info.errors); - }); - }); - -}); diff --git a/lib/bus.js b/lib/bus.js index 720261d05..d4f3bdd5f 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -13,6 +13,7 @@ var util = require('util'); function Bus(params) { events.EventEmitter.call(this); this.node = params.node; + this.remoteAddress = params.remoteAddress; } util.inherits(Bus, events.EventEmitter); diff --git a/lib/cli/bitcore.js b/lib/cli/bitcore.js index 3ea4771d9..bcbaa5ee1 100644 --- a/lib/cli/bitcore.js +++ b/lib/cli/bitcore.js @@ -9,7 +9,7 @@ function main(parentServicesPath, additionalServices) { moduleName: 'bitcore-node', configName: 'bitcore-node', processTitle: 'bitcore' - }).on('require', function (name, module) { + }).on('require', function (name) { console.log('Loading:', name); }).on('requireFail', function (name, err) { console.log('Unable to load:', name, err); diff --git a/lib/cli/bitcored.js b/lib/cli/bitcored.js index 158b2ee71..4462a8ea6 100644 --- a/lib/cli/bitcored.js +++ b/lib/cli/bitcored.js @@ -9,7 +9,7 @@ function main(parentServicesPath, additionalServices) { moduleName: 'bitcore-node', configName: 'bitcore-node', processTitle: 'bitcored' - }).on('require', function (name, module) { + }).on('require', function (name) { console.log('Loading:', name); }).on('requireFail', function (name, err) { console.log('Unable to load:', name, err); diff --git a/lib/errors.js b/lib/errors.js index 0a1b6a422..c534d0a24 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -3,23 +3,10 @@ var createError = require('errno').create; var BitcoreNodeError = createError('BitcoreNodeError'); -var NoOutputs = createError('NoOutputs', BitcoreNodeError); -var NoOutput = createError('NoOutput', BitcoreNodeError); -var Wallet = createError('WalletError', BitcoreNodeError); -Wallet.InsufficientFunds = createError('InsufficientFunds', Wallet); - -var Consensus = createError('Consensus', BitcoreNodeError); -Consensus.BlockExists = createError('BlockExists', Consensus); - -var Transaction = createError('Transaction', BitcoreNodeError); -Transaction.NotFound = createError('NotFound', Transaction); +var RPCError = createError('RPCError', BitcoreNodeError); module.exports = { Error: BitcoreNodeError, - NoOutputs: NoOutputs, - NoOutput: NoOutput, - Wallet: Wallet, - Consensus: Consensus, - Transaction: Transaction + RPCError: RPCError }; diff --git a/lib/logger.js b/lib/logger.js index eb627650b..4084c2ccb 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,14 +1,22 @@ 'use strict'; +var bitcore = require('bitcore-lib'); +var _ = bitcore.deps._; var colors = require('colors/safe'); /** * Wraps console.log with some special magic * @constructor */ -function Logger() { +function Logger(options) { + if (!options) { + options = {}; + } + this.formatting = _.isUndefined(options.formatting) ? Logger.DEFAULT_FORMATTING : options.formatting; } +Logger.DEFAULT_FORMATTING = true; + /** * Prints an info message * #info @@ -45,17 +53,18 @@ Logger.prototype.warn = function() { * Proxies console.log with color and arg parsing magic * #_log */ -Logger.prototype._log = function(color, type) { - if (process.env.NODE_ENV === 'test') { - return; - } - +Logger.prototype._log = function(color) { var args = Array.prototype.slice.call(arguments); args = args.slice(1); - var date = new Date(); - var typeString = colors[color].italic(args.shift() + ':'); - args[0] = '[' + date.toISOString() + ']' + ' ' + typeString + ' ' + args[0]; - console.log.apply(console, args); + var level = args.shift(); + + if (this.formatting) { + var date = new Date(); + var typeString = colors[color].italic(level + ':'); + args[0] = '[' + date.toISOString() + ']' + ' ' + typeString + ' ' + args[0]; + } + var fn = console[level] || console.log; + fn.apply(console, args); }; module.exports = Logger; diff --git a/lib/node.js b/lib/node.js index 737cd307a..c2350b505 100644 --- a/lib/node.js +++ b/lib/node.js @@ -27,8 +27,8 @@ var errors = require('./errors'); * ``` * * @param {Object} config - The configuration of the node + * @param {Array} config.formatLogs - Option to disable formatting of logs * @param {Array} config.services - The array of services - * @param {String} config.datadir - The directory for data (e.g. bitcoind datadir) * @param {Number} config.port - The HTTP port for services * @param {Boolean} config.https - Enable https * @param {Object} config.httpsOptions - Options for https @@ -41,8 +41,14 @@ function Node(config) { if(!(this instanceof Node)) { return new Node(config); } + this.configPath = config.path; this.errors = errors; this.log = log; + + if (!_.isUndefined(config.formatLogs)) { + this.log.formatting = config.formatLogs ? true : false; + } + this.network = null; this.services = {}; this._unloadedServices = []; @@ -52,8 +58,6 @@ function Node(config) { $.checkArgument(Array.isArray(config.services)); this._unloadedServices = config.services; } - $.checkState(config.datadir, 'Node config expects "datadir"'); - this.datadir = config.datadir; this.port = config.port; this.https = config.https; this.httpsOptions = config.httpsOptions; @@ -83,8 +87,11 @@ Node.prototype._setNetwork = function(config) { * Will instantiate a new Bus for this node. * @returns {Bus} */ -Node.prototype.openBus = function() { - return new Bus({node: this}); +Node.prototype.openBus = function(options) { + if (!options) { + options = {}; + } + return new Bus({node: this, remoteAddress: options.remoteAddress}); }; /** @@ -218,6 +225,13 @@ Node.prototype._startService = function(serviceInfo, callback) { }; +Node.prototype._logTitle = function() { + if (this.configPath) { + log.info('Using config:', this.configPath); + } +}; + + /** * Will start all running services in the order based on the dependency chain. * @param {Function} callback - Called when all services are started @@ -226,6 +240,8 @@ Node.prototype.start = function(callback) { var self = this; var servicesOrder = this.getServiceOrder(); + self._logTitle(); + async.eachSeries( servicesOrder, function(service, next) { @@ -241,6 +257,14 @@ Node.prototype.start = function(callback) { ); }; +Node.prototype.getNetworkName = function() { + var network = this.network.name; + if (this.network.regtestEnabled) { + network = 'regtest'; + } + return network; +}; + /** * Will stop all running services in the reverse order that they * were initially started. diff --git a/lib/scaffold/add.js b/lib/scaffold/add.js index 5d0e6eea2..76f24bdbc 100644 --- a/lib/scaffold/add.js +++ b/lib/scaffold/add.js @@ -5,6 +5,7 @@ var fs = require('fs'); var path = require('path'); var spawn = require('child_process').spawn; var bitcore = require('bitcore-lib'); +var utils = require('../utils'); var $ = bitcore.util.preconditions; var _ = bitcore.deps._; @@ -14,7 +15,7 @@ var _ = bitcore.deps._; * @param {Function} done */ function addConfig(configFilePath, service, done) { - $.checkState(path.isAbsolute(configFilePath), 'An absolute path is expected'); + $.checkState(utils.isAbsolutePath(configFilePath), 'An absolute path is expected'); fs.readFile(configFilePath, function(err, data) { if (err) { return done(err); @@ -39,7 +40,7 @@ function addConfig(configFilePath, service, done) { * @param {Function} done */ function addService(configDir, service, done) { - $.checkState(path.isAbsolute(configDir), 'An absolute path is expected'); + $.checkState(utils.isAbsolutePath(configDir), 'An absolute path is expected'); var npm = spawn('npm', ['install', service, '--save'], {cwd: configDir}); npm.stdout.on('data', function(data) { @@ -69,7 +70,7 @@ function add(options, done) { $.checkArgument(_.isObject(options)); $.checkArgument(_.isFunction(done)); $.checkArgument( - _.isString(options.path) && path.isAbsolute(options.path), + _.isString(options.path) && utils.isAbsolutePath(options.path), 'An absolute path is expected' ); $.checkArgument(Array.isArray(options.services)); diff --git a/lib/scaffold/create.js b/lib/scaffold/create.js index aa9a30840..c521602eb 100644 --- a/lib/scaffold/create.js +++ b/lib/scaffold/create.js @@ -11,12 +11,7 @@ var mkdirp = require('mkdirp'); var fs = require('fs'); var defaultBaseConfig = require('./default-base-config'); -var version; -if (packageFile.version.match('-dev')) { - version = '^' + packageFile.lastBuild; -} else { - version = '^' + packageFile.version; -} +var version = '^' + packageFile.version; var BASE_PACKAGE = { description: 'A full Bitcoin node build with Bitcore', diff --git a/lib/scaffold/default-base-config.js b/lib/scaffold/default-base-config.js index 6bb0babc3..1f584b55a 100644 --- a/lib/scaffold/default-base-config.js +++ b/lib/scaffold/default-base-config.js @@ -7,6 +7,7 @@ var path = require('path'); * or default locations. * @param {Object} options * @param {String} options.network - "testnet" or "livenet" + * @param {String} options.datadir - Absolute path to bitcoin database directory */ function getDefaultBaseConfig(options) { if (!options) { @@ -15,10 +16,17 @@ function getDefaultBaseConfig(options) { return { path: process.cwd(), config: { - datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'), network: options.network || 'livenet', port: 3001, - services: ['bitcoind', 'db', 'address', 'web'] + services: ['bitcoind', 'web'], + servicesConfig: { + bitcoind: { + spawn: { + datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'), + exec: path.resolve(__dirname, '../../bin/bitcoind') + } + } + } } }; } diff --git a/lib/scaffold/default-config.js b/lib/scaffold/default-config.js index 83d36f7a2..7075a7fce 100644 --- a/lib/scaffold/default-config.js +++ b/lib/scaffold/default-config.js @@ -24,17 +24,24 @@ function getDefaultConfig(options) { mkdirp.sync(defaultPath); } - var defaultServices = ['bitcoind', 'db', 'address', 'web']; + var defaultServices = ['bitcoind', 'web']; if (options.additionalServices) { defaultServices = defaultServices.concat(options.additionalServices); } if (!fs.existsSync(defaultConfigFile)) { var defaultConfig = { - datadir: path.resolve(defaultPath, './data'), network: 'livenet', port: 3001, - services: defaultServices + services: defaultServices, + servicesConfig: { + bitcoind: { + spawn: { + datadir: path.resolve(defaultPath, './data'), + exec: path.resolve(__dirname, '../../bin/bitcoind') + } + } + } }; fs.writeFileSync(defaultConfigFile, JSON.stringify(defaultConfig, null, 2)); } diff --git a/lib/scaffold/find-config.js b/lib/scaffold/find-config.js index f1c70b531..a4306e8b1 100644 --- a/lib/scaffold/find-config.js +++ b/lib/scaffold/find-config.js @@ -5,6 +5,7 @@ var $ = bitcore.util.preconditions; var _ = bitcore.deps._; var path = require('path'); var fs = require('fs'); +var utils = require('../utils'); /** * Will return the path and bitcore-node configuration @@ -12,7 +13,7 @@ var fs = require('fs'); */ function findConfig(cwd) { $.checkArgument(_.isString(cwd), 'Argument should be a string'); - $.checkArgument(path.isAbsolute(cwd), 'Argument should be an absolute path'); + $.checkArgument(utils.isAbsolutePath(cwd), 'Argument should be an absolute path'); var directory = String(cwd); while (!fs.existsSync(path.resolve(directory, 'bitcore-node.json'))) { directory = path.resolve(directory, '../'); diff --git a/lib/scaffold/remove.js b/lib/scaffold/remove.js index d17dabf44..6d866d6ff 100644 --- a/lib/scaffold/remove.js +++ b/lib/scaffold/remove.js @@ -8,6 +8,7 @@ var spawn = require('child_process').spawn; var bitcore = require('bitcore-lib'); var $ = bitcore.util.preconditions; var _ = bitcore.deps._; +var utils = require('../utils'); /** * Will remove a service from bitcore-node.json @@ -16,7 +17,7 @@ var _ = bitcore.deps._; * @param {Function} done */ function removeConfig(configFilePath, service, done) { - $.checkArgument(path.isAbsolute(configFilePath), 'An absolute path is expected'); + $.checkArgument(utils.isAbsolutePath(configFilePath), 'An absolute path is expected'); fs.readFile(configFilePath, function(err, data) { if (err) { return done(err); @@ -47,7 +48,7 @@ function removeConfig(configFilePath, service, done) { * @param {Function} done */ function uninstallService(configDir, service, done) { - $.checkArgument(path.isAbsolute(configDir), 'An absolute path is expected'); + $.checkArgument(utils.isAbsolutePath(configDir), 'An absolute path is expected'); $.checkArgument(_.isString(service), 'A string is expected for the service argument'); var child = spawn('npm', ['uninstall', service, '--save'], {cwd: configDir}); @@ -76,7 +77,7 @@ function uninstallService(configDir, service, done) { * @param {Function} done */ function removeService(configDir, service, done) { - $.checkArgument(path.isAbsolute(configDir), 'An absolute path is expected'); + $.checkArgument(utils.isAbsolutePath(configDir), 'An absolute path is expected'); $.checkArgument(_.isString(service), 'A string is expected for the service argument'); // check if the service is installed @@ -109,7 +110,7 @@ function remove(options, done) { $.checkArgument(_.isObject(options)); $.checkArgument(_.isFunction(done)); $.checkArgument( - _.isString(options.path) && path.isAbsolute(options.path), + _.isString(options.path) && utils.isAbsolutePath(options.path), 'An absolute path is expected' ); $.checkArgument(Array.isArray(options.services)); diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index f9ac430af..378e96c33 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -5,14 +5,151 @@ var BitcoreNode = require('../node'); var index = require('../'); var bitcore = require('bitcore-lib'); var _ = bitcore.deps._; -var $ = bitcore.util.preconditions; var log = index.log; -var child_process = require('child_process'); -var fs = require('fs'); var shuttingDown = false; log.debug = function() {}; +/** + * Checks for configuration options from version 2. This includes an "address" and + * "db" service, or having "datadir" at the root of the config. + */ +function checkConfigVersion2(fullConfig) { + var datadirUndefined = _.isUndefined(fullConfig.datadir); + var addressDefined = (fullConfig.services.indexOf('address') >= 0); + var dbDefined = (fullConfig.services.indexOf('db') >= 0); + + if (!datadirUndefined || addressDefined || dbDefined) { + + console.warn('\nConfiguration file is not compatible with this version. \n' + + 'A reindex for bitcoind is necessary for this upgrade with the "reindex=1" bitcoin.conf option. \n' + + 'There are changes necessary in both bitcoin.conf and bitcore-node.json. \n\n' + + 'To upgrade please see the details below and documentation at: \n' + + 'https://github.com/bitpay/bitcore-node/blob/bitcoind/docs/upgrade.md \n'); + + if (!datadirUndefined) { + console.warn('Please remove "datadir" and add it to the config at ' + fullConfig.path + ' with:'); + var missingConfig = { + servicesConfig: { + bitcoind: { + spawn: { + datadir: fullConfig.datadir, + exec: path.resolve(__dirname, '../../bin/bitcoind') + } + } + } + }; + console.warn(JSON.stringify(missingConfig, null, 2) + '\n'); + } + + if (addressDefined || dbDefined) { + console.warn('Please remove "address" and/or "db" from "services" in: ' + fullConfig.path + '\n'); + } + + return true; + } + + return false; +} + +/** + * This function will instantiate and start a Node, requiring the necessary service + * modules, and registering event handlers. + * @param {Object} options + * @param {Object} options.servicesPath - The path to the location of service modules + * @param {String} options.path - The absolute path of the configuration file + * @param {Object} options.config - The parsed bitcore-node.json configuration file + * @param {Array} options.config.services - An array of services names. + * @param {Object} options.config.servicesConfig - Parameters to pass to each service + * @param {String} options.config.datadir - A relative (to options.path) or absolute path to the datadir + * @param {String} options.config.network - 'livenet', 'testnet' or 'regtest + * @param {Number} options.config.port - The port to use for the web service + */ +function start(options) { + /* jshint maxstatements: 20 */ + + var fullConfig = _.clone(options.config); + + var servicesPath; + if (options.servicesPath) { + servicesPath = options.servicesPath; // services are in a different directory than the config + } else { + servicesPath = options.path; // defaults to the same directory + } + + fullConfig.path = path.resolve(options.path, './bitcore-node.json'); + + if (checkConfigVersion2(fullConfig)) { + process.exit(1); + } + + fullConfig.services = start.setupServices(require, servicesPath, options.config); + + var node = new BitcoreNode(fullConfig); + + // setup handlers for uncaught exceptions and ctrl+c + start.registerExitHandlers(process, node); + + node.on('ready', function() { + log.info('Bitcore Node ready'); + }); + + node.on('error', function(err) { + log.error(err); + }); + + node.start(function(err) { + if(err) { + log.error('Failed to start services'); + if (err.stack) { + log.error(err.stack); + } + start.cleanShutdown(process, node); + } + }); + + return node; + +} + +/** + * Checks a service for the expected methods + * @param {Object} service + */ +function checkService(service) { + // check that the service supports expected methods + if (!service.module.prototype || + !service.module.dependencies || + !service.module.prototype.start || + !service.module.prototype.stop) { + throw new Error( + 'Could not load service "' + service.name + '" as it does not support necessary methods and properties.' + ); + } +} + +/** + * Will require a module from local services directory first + * and then from available node_modules + * @param {Function} req + * @param {Object} service + */ +function loadModule(req, service) { + try { + // first try in the built-in bitcore-node services directory + service.module = req(path.resolve(__dirname, '../services/' + service.name)); + } catch(e) { + + // check if the package.json specifies a specific file to use + var servicePackage = req(service.name + '/package.json'); + var serviceModule = service.name; + if (servicePackage.bitcoreNode) { + serviceModule = service.name + '/' + servicePackage.bitcoreNode; + } + service.module = req(serviceModule); + } +} + /** * This function will loop over the configuration for services and require the * specified modules, and assemble an array in this format: @@ -42,29 +179,8 @@ function setupServices(req, servicesPath, config) { var hasConfig = config.servicesConfig && config.servicesConfig[service.name]; service.config = hasConfig ? config.servicesConfig[service.name] : {}; - try { - // first try in the built-in bitcore-node services directory - service.module = req(path.resolve(__dirname, '../services/' + service.name)); - } catch(e) { - - // check if the package.json specifies a specific file to use - var servicePackage = req(service.name + '/package.json'); - var serviceModule = service.name; - if (servicePackage.bitcoreNode) { - serviceModule = service.name + '/' + servicePackage.bitcoreNode; - } - service.module = req(serviceModule); - } - - // check that the service supports expected methods - if (!service.module.prototype || - !service.module.dependencies || - !service.module.prototype.start || - !service.module.prototype.stop) { - throw new Error( - 'Could not load service "' + service.name + '" as it does not support necessary methods.' - ); - } + loadModule(req, service); + checkService(service); services.push(service); } @@ -72,50 +188,6 @@ function setupServices(req, servicesPath, config) { return services; } -/** - * Will register event handlers to log the current db sync status. - * @param {Node} node - */ -function registerSyncHandlers(node, delay) { - - delay = delay || 10000; - var interval = false; - var count = 0; - - function logSyncStatus() { - log.info( - 'Database Sync Status: Tip:', node.services.db.tip.hash, - 'Height:', node.services.db.tip.__height, - 'Rate:', count/10, 'blocks per second' - ); - } - - node.on('ready', function() { - - if (node.services.db) { - node.on('synced', function() { - clearInterval(interval); - logSyncStatus(); - }); - node.services.db.on('addblock', function(block) { - count++; - // Initialize logging if not already instantiated - if (!interval) { - interval = setInterval(function() { - logSyncStatus(); - count = 0; - }, delay); - } - }); - } - - }); - - node.on('stopping', function() { - clearInterval(interval); - }); -} - /** * Will shutdown a node and then the process * @param {Object} _process - The Node.js process object @@ -132,20 +204,6 @@ function cleanShutdown(_process, node) { }); } -/** - * Will register event handlers to stop the node for `process` events - * `uncaughtException` and `SIGINT`. - * @param {Object} _process - The Node.js process - * @param {Node} node - */ -function registerExitHandlers(_process, node) { - //catches uncaught exceptions - _process.on('uncaughtException', exitHandler.bind(null, {exit:true}, _process, node)); - - //catches ctrl+c event - _process.on('SIGINT', exitHandler.bind(null, {sigint:true}, _process, node)); -} - /** * Will handle all the shutdown tasks that need to take place to ensure a safe exit * @param {Object} options @@ -177,106 +235,22 @@ function exitHandler(options, _process, node, err) { } /** - * This function will instantiate and start a Node, requiring the necessary service - * modules, and registering event handlers. - * @param {Object} options - * @param {Object} options.servicesPath - The path to the location of service modules - * @param {String} options.path - The absolute path of the configuration file - * @param {Object} options.config - The parsed bitcore-node.json configuration file - * @param {Array} options.config.services - An array of services names. - * @param {Object} options.config.servicesConfig - Parameters to pass to each service - * @param {String} options.config.datadir - A relative (to options.path) or absolute path to the datadir - * @param {String} options.config.network - 'livenet', 'testnet' or 'regtest - * @param {Number} options.config.port - The port to use for the web service - */ -function start(options) { - - var fullConfig = _.clone(options.config); - - var servicesPath; - if (options.servicesPath) { - servicesPath = options.servicesPath; // services are in a different directory than the config - } else { - servicesPath = options.path; // defaults to the same directory - } - - fullConfig.services = start.setupServices(require, servicesPath, options.config); - fullConfig.datadir = path.resolve(options.path, options.config.datadir); - - if (fullConfig.daemon) { - start.spawnChildProcess(fullConfig.datadir, process); - } - - var node = new BitcoreNode(fullConfig); - - // set up the event handlers for logging sync information - start.registerSyncHandlers(node); - - // setup handlers for uncaught exceptions and ctrl+c - start.registerExitHandlers(process, node); - - node.on('ready', function() { - log.info('Bitcore Node ready'); - }); - - node.on('error', function(err) { - log.error(err); - }); - - node.start(function(err) { - if(err) { - log.error('Failed to start services'); - if (err.stack) { - log.error(err.stack); - } - start.cleanShutdown(process, node); - } - }); - - return node; - -} - -/** - * This function will fork the passed in process and exit the parent process - * in order to daemonize the process. If there is already a daemon for this pid (process), - * then the function just returns. Stdout and stderr both append to one file, 'bitcore-node.log' - * located in the datadir. - * @param {String} datadir - The data directory where the bitcoin blockchain and config live. - * @param {Object} _process - The process that needs to fork a child and then, itself, exit. + * Will register event handlers to stop the node for `process` events + * `uncaughtException` and `SIGINT`. + * @param {Object} _process - The Node.js process + * @param {Node} node */ -function spawnChildProcess(datadir, _process) { - - if (_process.env.__bitcore_node) { - return _process.pid; - } +function registerExitHandlers(_process, node) { + //catches uncaught exceptions + _process.on('uncaughtException', exitHandler.bind(null, {exit:true}, _process, node)); - var args = [].concat(_process.argv); - args.shift(); - var script = args.shift(); - var env = _process.env; - var cwd = _process.cwd(); - env.__bitcore_node = true; - - var stderr = fs.openSync(datadir + '/bitcore-node.log', 'a+'); - var stdout = stderr; - - var cp_opt = { - stdio: ['ignore', stdout, stderr], - env: env, - cwd: cwd, - detached: true - }; - - var child = child_process.spawn(_process.execPath, [script].concat(args), cp_opt); - child.unref(); - return _process.exit(); + //catches ctrl+c event + _process.on('SIGINT', exitHandler.bind(null, {sigint:true}, _process, node)); } module.exports = start; module.exports.registerExitHandlers = registerExitHandlers; module.exports.exitHandler = exitHandler; -module.exports.registerSyncHandlers = registerSyncHandlers; module.exports.setupServices = setupServices; -module.exports.spawnChildProcess = spawnChildProcess; module.exports.cleanShutdown = cleanShutdown; +module.exports.checkConfigVersion2 = checkConfigVersion2; diff --git a/lib/service.js b/lib/service.js index cc47ce1af..08d0ef5a2 100644 --- a/lib/service.js +++ b/lib/service.js @@ -78,7 +78,7 @@ Service.prototype.stop = function(done) { * Setup express routes * @param {Express} app */ -Service.prototype.setupRoutes = function(app) { +Service.prototype.setupRoutes = function() { // Setup express routes here }; @@ -86,6 +86,4 @@ Service.prototype.getRoutePrefix = function() { return this.name; }; - - module.exports = Service; diff --git a/lib/services/address/constants.js b/lib/services/address/constants.js deleted file mode 100644 index 3653e9cb7..000000000 --- a/lib/services/address/constants.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -var exports = {}; - -exports.PREFIXES = { - OUTPUTS: new Buffer('02', 'hex'), // Query outputs by address and/or height - SPENTS: new Buffer('03', 'hex'), // Query inputs by address and/or height - SPENTSMAP: new Buffer('05', 'hex') // Get the input that spends an output -}; - -exports.MEMPREFIXES = { - OUTPUTS: new Buffer('01', 'hex'), // Query mempool outputs by address - SPENTS: new Buffer('02', 'hex'), // Query mempool inputs by address - SPENTSMAP: new Buffer('03', 'hex') // Query mempool for the input that spends an output -}; - -// To save space, we're only storing the PubKeyHash or ScriptHash in our index. -// To avoid intentional unspendable collisions, which have been seen on the blockchain, -// we must store the hash type (PK or Script) as well. -exports.HASH_TYPES = { - PUBKEY: new Buffer('01', 'hex'), - REDEEMSCRIPT: new Buffer('02', 'hex') -}; - -// Translates from our enum type back into the hash types returned by -// bitcore-lib/address. -exports.HASH_TYPES_READABLE = { - '01': 'pubkeyhash', - '02': 'scripthash' -}; - -exports.HASH_TYPES_MAP = { - 'pubkeyhash': exports.HASH_TYPES.PUBKEY, - 'scripthash': exports.HASH_TYPES.REDEEMSCRIPT -}; - -exports.SPACER_MIN = new Buffer('00', 'hex'); -exports.SPACER_MAX = new Buffer('ff', 'hex'); -exports.SPACER_HEIGHT_MIN = new Buffer('0000000000', 'hex'); -exports.SPACER_HEIGHT_MAX = new Buffer('ffffffffff', 'hex'); -exports.TIMESTAMP_MIN = new Buffer('0000000000000000', 'hex'); -exports.TIMESTAMP_MAX = new Buffer('ffffffffffffffff', 'hex'); - -// The maximum number of inputs that can be queried at once -exports.MAX_INPUTS_QUERY_LENGTH = 50000; -// The maximum number of outputs that can be queried at once -exports.MAX_OUTPUTS_QUERY_LENGTH = 50000; -// The maximum number of transactions that can be queried at once -exports.MAX_HISTORY_QUERY_LENGTH = 100; -// The maximum number of addresses that can be queried at once -exports.MAX_ADDRESSES_QUERY = 10000; -// The maximum number of simultaneous requests -exports.MAX_ADDRESSES_LIMIT = 5; - -module.exports = exports; - diff --git a/lib/services/address/encoding.js b/lib/services/address/encoding.js deleted file mode 100644 index 8ebce51c9..000000000 --- a/lib/services/address/encoding.js +++ /dev/null @@ -1,307 +0,0 @@ -'use strict'; - -var bitcore = require('bitcore-lib'); -var BufferReader = bitcore.encoding.BufferReader; -var Address = bitcore.Address; -var PublicKey = bitcore.PublicKey; -var constants = require('./constants'); -var $ = bitcore.util.preconditions; - -var exports = {}; - -exports.encodeSpentIndexSyncKey = function(txidBuffer, outputIndex) { - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var key = Buffer.concat([ - txidBuffer, - outputIndexBuffer - ]); - return key.toString('binary'); -}; - -exports.encodeMempoolAddressIndexKey = function(hashBuffer, hashTypeBuffer) { - var key = Buffer.concat([ - hashBuffer, - hashTypeBuffer, - ]); - return key.toString('binary'); -}; - - -exports.encodeOutputKey = function(hashBuffer, hashTypeBuffer, height, txidBuffer, outputIndex) { - var heightBuffer = new Buffer(4); - heightBuffer.writeUInt32BE(height); - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var key = Buffer.concat([ - constants.PREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - heightBuffer, - txidBuffer, - outputIndexBuffer - ]); - return key; -}; - -exports.decodeOutputKey = function(buffer) { - var reader = new BufferReader(buffer); - var prefix = reader.read(1); - var hashBuffer = reader.read(20); - var hashTypeBuffer = reader.read(1); - var spacer = reader.read(1); - var height = reader.readUInt32BE(); - var txid = reader.read(32); - var outputIndex = reader.readUInt32BE(); - return { - prefix: prefix, - hashBuffer: hashBuffer, - hashTypeBuffer: hashTypeBuffer, - height: height, - txid: txid, - outputIndex: outputIndex - }; -}; - -exports.encodeOutputValue = function(satoshis, scriptBuffer) { - var satoshisBuffer = new Buffer(8); - satoshisBuffer.writeDoubleBE(satoshis); - return Buffer.concat([satoshisBuffer, scriptBuffer]); -}; - -exports.encodeOutputMempoolValue = function(satoshis, timestampBuffer, scriptBuffer) { - var satoshisBuffer = new Buffer(8); - satoshisBuffer.writeDoubleBE(satoshis); - return Buffer.concat([satoshisBuffer, timestampBuffer, scriptBuffer]); -}; - -exports.decodeOutputValue = function(buffer) { - var satoshis = buffer.readDoubleBE(0); - var scriptBuffer = buffer.slice(8, buffer.length); - return { - satoshis: satoshis, - scriptBuffer: scriptBuffer - }; -}; - -exports.decodeOutputMempoolValue = function(buffer) { - var satoshis = buffer.readDoubleBE(0); - var timestamp = buffer.readDoubleBE(8); - var scriptBuffer = buffer.slice(16, buffer.length); - return { - satoshis: satoshis, - timestamp: timestamp, - scriptBuffer: scriptBuffer - }; -}; - -exports.encodeInputKey = function(hashBuffer, hashTypeBuffer, height, prevTxIdBuffer, outputIndex) { - var heightBuffer = new Buffer(4); - heightBuffer.writeUInt32BE(height); - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - return Buffer.concat([ - constants.PREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - heightBuffer, - prevTxIdBuffer, - outputIndexBuffer - ]); -}; - -exports.decodeInputKey = function(buffer) { - var reader = new BufferReader(buffer); - var prefix = reader.read(1); - var hashBuffer = reader.read(20); - var hashTypeBuffer = reader.read(1); - var spacer = reader.read(1); - var height = reader.readUInt32BE(); - var prevTxId = reader.read(32); - var outputIndex = reader.readUInt32BE(); - return { - prefix: prefix, - hashBuffer: hashBuffer, - hashTypeBuffer: hashTypeBuffer, - height: height, - prevTxId: prevTxId, - outputIndex: outputIndex - }; -}; - -exports.encodeInputValue = function(txidBuffer, inputIndex) { - var inputIndexBuffer = new Buffer(4); - inputIndexBuffer.writeUInt32BE(inputIndex); - return Buffer.concat([ - txidBuffer, - inputIndexBuffer - ]); -}; - -exports.decodeInputValue = function(buffer) { - var txid = buffer.slice(0, 32); - var inputIndex = buffer.readUInt32BE(32); - return { - txid: txid, - inputIndex: inputIndex - }; -}; - -exports.encodeInputKeyMap = function(outputTxIdBuffer, outputIndex) { - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - return Buffer.concat([ - constants.PREFIXES.SPENTSMAP, - outputTxIdBuffer, - outputIndexBuffer - ]); -}; - -exports.decodeInputKeyMap = function(buffer) { - var txid = buffer.slice(1, 33); - var outputIndex = buffer.readUInt32BE(33); - return { - outputTxId: txid, - outputIndex: outputIndex - }; -}; - -exports.encodeInputValueMap = function(inputTxIdBuffer, inputIndex) { - var inputIndexBuffer = new Buffer(4); - inputIndexBuffer.writeUInt32BE(inputIndex); - return Buffer.concat([ - inputTxIdBuffer, - inputIndexBuffer - ]); -}; - -exports.decodeInputValueMap = function(buffer) { - var txid = buffer.slice(0, 32); - var inputIndex = buffer.readUInt32BE(32); - return { - inputTxId: txid, - inputIndex: inputIndex - }; -}; - -exports.encodeSummaryCacheKey = function(address) { - return Buffer.concat([address.hashBuffer, constants.HASH_TYPES_MAP[address.type]]); -}; - -exports.decodeSummaryCacheKey = function(buffer, network) { - var hashBuffer = buffer.read(20); - var type = constants.HASH_TYPES_READABLE[buffer.read(20, 2).toString('hex')]; - var address = new Address({ - hashBuffer: hashBuffer, - type: type, - network: network - }); - return address; -}; - -exports.encodeSummaryCacheValue = function(cache, tipHeight, tipHash) { - var tipHashBuffer = new Buffer(tipHash, 'hex'); - var buffer = new Buffer(new Array(20)); - buffer.writeUInt32BE(tipHeight); - buffer.writeDoubleBE(cache.result.totalReceived, 4); - buffer.writeDoubleBE(cache.result.balance, 12); - var txidBuffers = []; - for (var i = 0; i < cache.result.txids.length; i++) { - var buf = new Buffer(new Array(36)); - var txid = cache.result.txids[i]; - buf.write(txid, 'hex'); - buf.writeUInt32BE(cache.result.appearanceIds[txid], 32); - txidBuffers.push(buf); - } - var txidsBuffer = Buffer.concat(txidBuffers); - var value = Buffer.concat([tipHashBuffer, buffer, txidsBuffer]); - - return value; -}; - -exports.decodeSummaryCacheValue = function(buffer) { - - var hash = buffer.slice(0, 32).toString('hex'); - var height = buffer.readUInt32BE(32); - var totalReceived = buffer.readDoubleBE(36); - var balance = buffer.readDoubleBE(44); - - // read 32 byte chunks until exhausted - var appearanceIds = {}; - var txids = []; - var pos = 52; - while(pos < buffer.length) { - var txid = buffer.slice(pos, pos + 32).toString('hex'); - var txidHeight = buffer.readUInt32BE(pos + 32); - txids.push(txid); - appearanceIds[txid] = txidHeight; - pos += 36; - } - - var cache = { - height: height, - hash: hash, - result: { - appearanceIds: appearanceIds, - txids: txids, - totalReceived: totalReceived, - balance: balance, - unconfirmedAppearanceIds: {}, // unconfirmed values are never stored in cache - unconfirmedBalance: 0 - } - }; - - return cache; -}; - -exports.getAddressInfo = function(addressStr) { - var addrObj = bitcore.Address(addressStr); - var hashTypeBuffer = constants.HASH_TYPES_MAP[addrObj.type]; - - return { - hashBuffer: addrObj.hashBuffer, - hashTypeBuffer: hashTypeBuffer, - hashTypeReadable: addrObj.type - }; -}; - -/** - * This function is optimized to return address information about an output script - * without constructing a Bitcore Address instance. - * @param {Script} - An instance of a Bitcore Script - * @param {Network|String} - The network for the address - */ -exports.extractAddressInfoFromScript = function(script, network) { - $.checkArgument(network, 'Second argument is expected to be a network'); - var hashBuffer; - var addressType; - var hashTypeBuffer; - if (script.isPublicKeyHashOut()) { - hashBuffer = script.chunks[2].buf; - hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - addressType = Address.PayToPublicKeyHash; - } else if (script.isScriptHashOut()) { - hashBuffer = script.chunks[1].buf; - hashTypeBuffer = constants.HASH_TYPES.REDEEMSCRIPT; - addressType = Address.PayToScriptHash; - } else if (script.isPublicKeyOut()) { - var pubkey = script.chunks[0].buf; - var address = Address.fromPublicKey(new PublicKey(pubkey), network); - hashBuffer = address.hashBuffer; - hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - // pay-to-publickey doesn't have an address, however for compatibility - // purposes, we can create an address - addressType = Address.PayToPublicKeyHash; - } else { - return false; - } - return { - hashBuffer: hashBuffer, - hashTypeBuffer: hashTypeBuffer, - addressType: addressType - }; -}; - -module.exports = exports; diff --git a/lib/services/address/history.js b/lib/services/address/history.js deleted file mode 100644 index 88b5c96b7..000000000 --- a/lib/services/address/history.js +++ /dev/null @@ -1,266 +0,0 @@ -'use strict'; - -var bitcore = require('bitcore-lib'); -var async = require('async'); -var _ = bitcore.deps._; - -var constants = require('./constants'); - -/** - * This represents an instance that keeps track of data over a series of - * asynchronous I/O calls to get the transaction history for a group of - * addresses. History can be queried by start and end block heights to limit large sets - * of results (uses leveldb key streaming). - */ -function AddressHistory(args) { - this.node = args.node; - this.options = args.options; - - if(Array.isArray(args.addresses)) { - this.addresses = args.addresses; - } else { - this.addresses = [args.addresses]; - } - - this.maxHistoryQueryLength = args.options.maxHistoryQueryLength || constants.MAX_HISTORY_QUERY_LENGTH; - this.maxAddressesQuery = args.options.maxAddressesQuery || constants.MAX_ADDRESSES_QUERY; - this.maxAddressesLimit = args.options.maxAddressesLimit || constants.MAX_ADDRESSES_LIMIT; - - this.addressStrings = []; - for (var i = 0; i < this.addresses.length; i++) { - var address = this.addresses[i]; - if (address instanceof bitcore.Address) { - this.addressStrings.push(address.toString()); - } else if (_.isString(address)) { - this.addressStrings.push(address); - } else { - throw new TypeError('Addresses are expected to be strings'); - } - } - - this.detailedArray = []; -} - -AddressHistory.prototype._mergeAndSortTxids = function(summaries) { - var appearanceIds = {}; - var unconfirmedAppearanceIds = {}; - - for (var i = 0; i < summaries.length; i++) { - var summary = summaries[i]; - for (var key in summary.appearanceIds) { - appearanceIds[key] = summary.appearanceIds[key]; - delete summary.appearanceIds[key]; - } - for (var unconfirmedKey in summary.unconfirmedAppearanceIds) { - unconfirmedAppearanceIds[unconfirmedKey] = summary.unconfirmedAppearanceIds[unconfirmedKey]; - delete summary.unconfirmedAppearanceIds[key]; - } - } - var confirmedTxids = Object.keys(appearanceIds); - confirmedTxids.sort(function(a, b) { - // Confirmed are sorted by height - return appearanceIds[a] - appearanceIds[b]; - }); - var unconfirmedTxids = Object.keys(unconfirmedAppearanceIds); - unconfirmedTxids.sort(function(a, b) { - // Unconfirmed are sorted by timestamp - return unconfirmedAppearanceIds[a] - unconfirmedAppearanceIds[b]; - }); - return confirmedTxids.concat(unconfirmedTxids); -}; - -/** - * This function will give detailed history for the configured - * addresses. See AddressService.prototype.getAddressHistory - * for complete documentation about options and response format. - */ -AddressHistory.prototype.get = function(callback) { - var self = this; - if (this.addresses.length > this.maxAddressesQuery) { - return callback(new TypeError('Maximum number of addresses (' + this.maxAddressesQuery + ') exceeded')); - } - - var opts = _.clone(this.options); - opts.noBalance = true; - - if (this.addresses.length === 1) { - var address = this.addresses[0]; - self.node.services.address.getAddressSummary(address, opts, function(err, summary) { - if (err) { - return callback(err); - } - return self._paginateWithDetails.call(self, summary.txids, callback); - }); - } else { - - opts.fullTxList = true; - async.mapLimit( - self.addresses, - self.maxAddressesLimit, - function(address, next) { - self.node.services.address.getAddressSummary(address, opts, next); - }, - function(err, summaries) { - if (err) { - return callback(err); - } - var txids = self._mergeAndSortTxids(summaries); - return self._paginateWithDetails.call(self, txids, callback); - } - ); - } - -}; - -AddressHistory.prototype._paginateWithDetails = function(allTxids, callback) { - var self = this; - var totalCount = allTxids.length; - - // Slice the page starting with the most recent - var txids; - if (self.options.from >= 0 && self.options.to >= 0) { - var fromOffset = Math.max(0, totalCount - self.options.from); - var toOffset = Math.max(0, totalCount - self.options.to); - txids = allTxids.slice(toOffset, fromOffset); - } else { - txids = allTxids; - } - - // Verify that this query isn't too long - if (txids.length > self.maxHistoryQueryLength) { - return callback(new Error( - 'Maximum length query (' + self.maxHistoryQueryLength + ') exceeded for address(es): ' + - self.addresses.join(',') - )); - } - - // Reverse to include most recent at the top - txids.reverse(); - - async.eachSeries( - txids, - function(txid, next) { - self.getDetailedInfo(txid, next); - }, - function(err) { - if (err) { - return callback(err); - } - callback(null, { - totalCount: totalCount, - items: self.detailedArray - }); - } - ); - -}; - -/** - * This function will transform items from the combinedArray into - * the detailedArray with the full transaction, satoshis and confirmation. - * @param {Object} txInfo - An item from the `combinedArray` - * @param {Function} next - */ -AddressHistory.prototype.getDetailedInfo = function(txid, next) { - var self = this; - var queryMempool = _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool; - - self.node.services.db.getTransactionWithBlockInfo( - txid, - queryMempool, - function(err, transaction) { - if (err) { - return next(err); - } - - transaction.populateInputs(self.node.services.db, [], function(err) { - if (err) { - return next(err); - } - - var addressDetails = self.getAddressDetailsForTransaction(transaction); - - self.detailedArray.push({ - addresses: addressDetails.addresses, - satoshis: addressDetails.satoshis, - height: transaction.__height, - confirmations: self.getConfirmationsDetail(transaction), - timestamp: transaction.__timestamp, - // TODO bitcore-lib should return null instead of throwing error on coinbase - fees: !transaction.isCoinbase() ? transaction.getFee() : null, - tx: transaction - }); - - next(); - }); - } - ); -}; - -/** - * A helper function for `getDetailedInfo` for getting the confirmations. - * @param {Transaction} transaction - A transaction with a populated __height value. - */ -AddressHistory.prototype.getConfirmationsDetail = function(transaction) { - var confirmations = 0; - if (transaction.__height >= 0) { - confirmations = this.node.services.db.tip.__height - transaction.__height + 1; - } - return confirmations; -}; - -AddressHistory.prototype.getAddressDetailsForTransaction = function(transaction) { - var result = { - addresses: {}, - satoshis: 0 - }; - - for (var inputIndex = 0; inputIndex < transaction.inputs.length; inputIndex++) { - var input = transaction.inputs[inputIndex]; - if (!input.script) { - continue; - } - var inputAddress = input.script.toAddress(this.node.network); - if (inputAddress) { - var inputAddressString = inputAddress.toString(); - if (this.addressStrings.indexOf(inputAddressString) >= 0) { - if (!result.addresses[inputAddressString]) { - result.addresses[inputAddressString] = { - inputIndexes: [inputIndex], - outputIndexes: [] - }; - } else { - result.addresses[inputAddressString].inputIndexes.push(inputIndex); - } - result.satoshis -= input.output.satoshis; - } - } - } - - for (var outputIndex = 0; outputIndex < transaction.outputs.length; outputIndex++) { - var output = transaction.outputs[outputIndex]; - if (!output.script) { - continue; - } - var outputAddress = output.script.toAddress(this.node.network); - if (outputAddress) { - var outputAddressString = outputAddress.toString(); - if (this.addressStrings.indexOf(outputAddressString) >= 0) { - if (!result.addresses[outputAddressString]) { - result.addresses[outputAddressString] = { - inputIndexes: [], - outputIndexes: [outputIndex] - }; - } else { - result.addresses[outputAddressString].outputIndexes.push(outputIndex); - } - result.satoshis += output.satoshis; - } - } - } - - return result; - -}; - -module.exports = AddressHistory; diff --git a/lib/services/address/index.js b/lib/services/address/index.js deleted file mode 100644 index eef4ed072..000000000 --- a/lib/services/address/index.js +++ /dev/null @@ -1,1611 +0,0 @@ -'use strict'; - -var fs = require('fs'); -var BaseService = require('../../service'); -var inherits = require('util').inherits; -var async = require('async'); -var mkdirp = require('mkdirp'); -var index = require('../../'); -var log = index.log; -var errors = index.errors; -var bitcore = require('bitcore-lib'); -var Networks = bitcore.Networks; -var levelup = require('levelup'); -var leveldown = require('leveldown'); -var memdown = require('memdown'); -var $ = bitcore.util.preconditions; -var _ = bitcore.deps._; -var Hash = bitcore.crypto.Hash; -var EventEmitter = require('events').EventEmitter; -var Address = bitcore.Address; -var AddressHistory = require('./history'); -var constants = require('./constants'); -var encoding = require('./encoding'); -var InputsTransformStream = require('./streams/inputs-transform'); -var OutputsTransformStream = require('./streams/outputs-transform'); - - -/** - * The Address Service builds upon the Database Service and the Bitcoin Service to add additional - * functionality for getting information by base58check encoded addresses. This includes getting the - * balance for an address, the history for a collection of addresses, and unspent outputs for - * constructing transactions. This is typically the core functionality for building a wallet. - * @param {Object} options - * @param {Node} options.node - An instance of the node - * @param {String} options.name - An optional name of the service - */ -var AddressService = function(options) { - BaseService.call(this, options); - - this.subscriptions = {}; - this.subscriptions['address/transaction'] = {}; - this.subscriptions['address/balance'] = {}; - - this._bitcoindTransactionListener = this.transactionHandler.bind(this); - this._bitcoindTransactionLeaveListener = this.transactionLeaveHandler.bind(this); - this.node.services.bitcoind.on('tx', this._bitcoindTransactionListener); - this.node.services.bitcoind.on('txleave', this._bitcoindTransactionLeaveListener); - - this.maxInputsQueryLength = options.maxInputsQueryLength || constants.MAX_INPUTS_QUERY_LENGTH; - this.maxOutputsQueryLength = options.maxOutputsQueryLength || constants.MAX_OUTPUTS_QUERY_LENGTH; - - this._setMempoolIndexPath(); - if (options.mempoolMemoryIndex) { - this.levelupStore = memdown; - } else { - this.levelupStore = leveldown; - } - this.mempoolIndex = null; // Used for larger mempool indexes - this.mempoolSpentIndex = {}; // Used for small quick synchronous lookups - this.mempoolAddressIndex = {}; // Used to check if an address is on the spend pool -}; - -inherits(AddressService, BaseService); - -AddressService.dependencies = [ - 'bitcoind', - 'db' -]; - -AddressService.prototype.start = function(callback) { - var self = this; - - async.series([ - - function(next) { - // Flush any existing mempool index - if (fs.existsSync(self.mempoolIndexPath)) { - leveldown.destroy(self.mempoolIndexPath, next); - } else { - setImmediate(next); - } - }, - function(next) { - // Setup new mempool index - if (!fs.existsSync(self.mempoolIndexPath)) { - mkdirp(self.mempoolIndexPath, next); - } else { - setImmediate(next); - } - }, - function(next) { - self.mempoolIndex = levelup( - self.mempoolIndexPath, - { - db: self.levelupStore, - keyEncoding: 'binary', - valueEncoding: 'binary', - fillCache: false, - maxOpenFiles: 200 - }, - next - ); - } - ], callback); - -}; - -AddressService.prototype.stop = function(callback) { - // TODO Keep track of ongoing db requests before shutting down - this.node.services.bitcoind.removeListener('tx', this._bitcoindTransactionListener); - this.node.services.bitcoind.removeListener('txleave', this._bitcoindTransactionLeaveListener); - this.mempoolIndex.close(callback); -}; - -/** - * This function will set `this.mempoolIndexPath` based on `this.node.network`. - * @private - */ -AddressService.prototype._setMempoolIndexPath = function() { - this.mempoolIndexPath = this._getDBPathFor('bitcore-addressmempool.db'); -}; - -AddressService.prototype._getDBPathFor = function(dbname) { - $.checkState(this.node.datadir, 'Node is expected to have a "datadir" property'); - var path; - if (this.node.network === Networks.livenet) { - path = this.node.datadir + '/' + dbname; - } else if (this.node.network === Networks.testnet) { - if (this.node.network.regtestEnabled) { - path = this.node.datadir + '/regtest/' + dbname; - } else { - path = this.node.datadir + '/testnet3/' + dbname; - } - } else { - throw new Error('Unknown network: ' + this.network); - } - return path; -}; - -/** - * Called by the Node to get the available API methods for this service, - * that can be exposed over the JSON-RPC interface. - */ -AddressService.prototype.getAPIMethods = function() { - return [ - ['getBalance', this, this.getBalance, 2], - ['getOutputs', this, this.getOutputs, 2], - ['getUnspentOutputs', this, this.getUnspentOutputs, 2], - ['getInputForOutput', this, this.getInputForOutput, 2], - ['isSpent', this, this.isSpent, 2], - ['getAddressHistory', this, this.getAddressHistory, 2], - ['getAddressSummary', this, this.getAddressSummary, 1] - ]; -}; - -/** - * Called by the Bus to get the available events for this service. - */ -AddressService.prototype.getPublishEvents = function() { - return [ - { - name: 'address/transaction', - scope: this, - subscribe: this.subscribe.bind(this, 'address/transaction'), - unsubscribe: this.unsubscribe.bind(this, 'address/transaction') - }, - { - name: 'address/balance', - scope: this, - subscribe: this.subscribe.bind(this, 'address/balance'), - unsubscribe: this.unsubscribe.bind(this, 'address/balance') - } - ]; -}; - -/** - * Will process each output of a transaction from the daemon "tx" event, and construct - * an object with the data for the message to be relayed to any subscribers for an address. - * - * @param {Object} messages - An object to collect messages - * @param {Transaction} tx - Instance of the transaction - * @param {Number} outputIndex - The index of the output in the transaction - * @param {Boolean} rejected - If the transaction was rejected by the mempool - */ -AddressService.prototype.transactionOutputHandler = function(messages, tx, outputIndex, rejected) { - var script = tx.outputs[outputIndex].script; - - // If the script is invalid skip - if (!script) { - return; - } - - var addressInfo = encoding.extractAddressInfoFromScript(script, this.node.network); - if (!addressInfo) { - return; - } - - addressInfo.hashHex = addressInfo.hashBuffer.toString('hex'); - - // Collect data to publish to address subscribers - if (messages[addressInfo.hashHex]) { - messages[addressInfo.hashHex].outputIndexes.push(outputIndex); - } else { - messages[addressInfo.hashHex] = { - tx: tx, - outputIndexes: [outputIndex], - addressInfo: addressInfo, - rejected: rejected - }; - } -}; - -/** - * This will handle data from the daemon "txleave" that a transaction has left the mempool. - * @param {Object} txInfo - The data from the daemon.on('txleave') event - * @param {Buffer} txInfo.buffer - The transaction buffer - * @param {String} txInfo.hash - The hash of the transaction - */ -AddressService.prototype.transactionLeaveHandler = function(txInfo) { - var tx = bitcore.Transaction().fromBuffer(txInfo.buffer); - this.updateMempoolIndex(tx, false); -}; - -/** - * This will handle data from the daemon "tx" event, go through each of the outputs - * and send messages by calling `transactionEventHandler` to any subscribers for a - * particular address. - * @param {Object} txInfo - The data from the daemon.on('tx') event - * @param {Buffer} txInfo.buffer - The transaction buffer - * @param {Boolean} txInfo.mempool - If the transaction was accepted in the mempool - * @param {String} txInfo.hash - The hash of the transaction - * @param {Function} [callback] - Optional callback - */ -AddressService.prototype.transactionHandler = function(txInfo, callback) { - var self = this; - - if (!callback) { - callback = function(err) { - if (err) { - return log.error(err); - } - }; - } - - if (this.node.stopping) { - return callback(); - } - - // Basic transaction format is handled by the daemon - // and we can safely assume the buffer is properly formatted. - var tx = bitcore.Transaction().fromBuffer(txInfo.buffer); - - var messages = {}; - - var outputsLength = tx.outputs.length; - for (var i = 0; i < outputsLength; i++) { - this.transactionOutputHandler(messages, tx, i, !txInfo.mempool); - } - - function finish(err) { - if (err) { - return callback(err); - } - for (var key in messages) { - self.transactionEventHandler(messages[key]); - self.balanceEventHandler(null, messages[key].addressInfo); - } - callback(); - } - - if (txInfo.mempool) { - self.updateMempoolIndex(tx, true, finish); - } else { - setImmediate(finish); - } - -}; - -AddressService.prototype._updateAddressIndex = function(key, add) { - var currentValue = this.mempoolAddressIndex[key] || 0; - - if(add) { - if (currentValue > 0) { - this.mempoolAddressIndex[key] = currentValue + 1; - } else { - this.mempoolAddressIndex[key] = 1; - } - } else { - if (currentValue <= 1) { - delete this.mempoolAddressIndex[key]; - } else { - this.mempoolAddressIndex[key]--; - } - } -}; - - -/** - * This function will update the mempool address index with the necessary - * information for further lookups. - * @param {Transaction} - An instance of a Bitcore Transaction - * @param {Boolean} - Add/remove from the index - */ -AddressService.prototype.updateMempoolIndex = function(tx, add, callback) { - /* jshint maxstatements: 100 */ - - var operations = []; - var timestampBuffer = new Buffer(new Array(8)); - timestampBuffer.writeDoubleBE(new Date().getTime()); - - var action = 'put'; - if (!add) { - action = 'del'; - } - - var txid = tx.hash; - var txidBuffer = new Buffer(txid, 'hex'); - - var outputLength = tx.outputs.length; - for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) { - var output = tx.outputs[outputIndex]; - if (!output.script) { - continue; - } - var addressInfo = encoding.extractAddressInfoFromScript(output.script, this.node.network); - if (!addressInfo) { - continue; - } - - var addressIndexKey = encoding.encodeMempoolAddressIndexKey(addressInfo.hashBuffer, addressInfo.hashTypeBuffer); - - this._updateAddressIndex(addressIndexKey, add); - - // Update output index - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - - var outKey = Buffer.concat([ - constants.MEMPREFIXES.OUTPUTS, - addressInfo.hashBuffer, - addressInfo.hashTypeBuffer, - txidBuffer, - outputIndexBuffer - ]); - - var outValue = encoding.encodeOutputMempoolValue( - output.satoshis, - timestampBuffer, - output._scriptBuffer - ); - - operations.push({ - type: action, - key: outKey, - value: outValue - }); - - } - var inputLength = tx.inputs.length; - for (var inputIndex = 0; inputIndex < inputLength; inputIndex++) { - - var input = tx.inputs[inputIndex]; - - var inputOutputIndexBuffer = new Buffer(4); - inputOutputIndexBuffer.writeUInt32BE(input.outputIndex); - - // Add an additional small spent index for fast synchronous lookups - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey( - input.prevTxId, - input.outputIndex - ); - if (add) { - this.mempoolSpentIndex[spentIndexSyncKey] = true; - } else { - delete this.mempoolSpentIndex[spentIndexSyncKey]; - } - - // Add a more detailed spent index with values - var spentIndexKey = Buffer.concat([ - constants.MEMPREFIXES.SPENTSMAP, - input.prevTxId, - inputOutputIndexBuffer - ]); - var inputIndexBuffer = new Buffer(4); - inputIndexBuffer.writeUInt32BE(inputIndex); - var inputIndexValue = Buffer.concat([ - txidBuffer, - inputIndexBuffer - ]); - operations.push({ - type: action, - key: spentIndexKey, - value: inputIndexValue - }); - - // Update input index - var inputHashBuffer; - var inputHashType; - if (input.script.isPublicKeyHashIn()) { - inputHashBuffer = Hash.sha256ripemd160(input.script.chunks[1].buf); - inputHashType = constants.HASH_TYPES.PUBKEY; - } else if (input.script.isScriptHashIn()) { - inputHashBuffer = Hash.sha256ripemd160(input.script.chunks[input.script.chunks.length - 1].buf); - inputHashType = constants.HASH_TYPES.REDEEMSCRIPT; - } else { - continue; - } - var inputKey = Buffer.concat([ - constants.MEMPREFIXES.SPENTS, - inputHashBuffer, - inputHashType, - input.prevTxId, - inputOutputIndexBuffer - ]); - var inputValue = Buffer.concat([ - txidBuffer, - inputIndexBuffer, - timestampBuffer - ]); - operations.push({ - type: action, - key: inputKey, - value: inputValue - }); - - var addressIndexKey = encoding.encodeMempoolAddressIndexKey(inputHashBuffer, inputHashType); - - this._updateAddressIndex(addressIndexKey, add); - } - - if (!callback) { - callback = function(err) { - if (err) { - return log.error(err); - } - }; - } - - this.mempoolIndex.batch(operations, callback); -}; - -/** - * The Database Service will run this function when blocks are connected and - * disconnected to the chain during syncing and reorganizations. - * @param {Block} block - An instance of a Bitcore Block - * @param {Boolean} addOutput - If the block is being removed or added to the chain - * @param {Function} callback - */ -AddressService.prototype.blockHandler = function(block, addOutput, callback) { - var txs = block.transactions; - var height = block.__height; - - var action = 'put'; - if (!addOutput) { - action = 'del'; - } - - var operations = []; - - var transactionLength = txs.length; - for (var i = 0; i < transactionLength; i++) { - - var tx = txs[i]; - var txid = tx.id; - var txidBuffer = new Buffer(txid, 'hex'); - var inputs = tx.inputs; - var outputs = tx.outputs; - - // Subscription messages - var txmessages = {}; - - var outputLength = outputs.length; - for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) { - var output = outputs[outputIndex]; - - var script = output.script; - - if(!script) { - log.debug('Invalid script'); - continue; - } - - var addressInfo = encoding.extractAddressInfoFromScript(script, this.node.network); - if (!addressInfo) { - continue; - } - - // We need to use the height for indexes (and not the timestamp) because the - // the timestamp has unreliable sequential ordering. The next block - // can have a time that is previous to the previous block (however not - // less than the mean of the 11 previous blocks) and not greater than 2 - // hours in the future. - var key = encoding.encodeOutputKey(addressInfo.hashBuffer, addressInfo.hashTypeBuffer, - height, txidBuffer, outputIndex); - var value = encoding.encodeOutputValue(output.satoshis, output._scriptBuffer); - operations.push({ - type: action, - key: key, - value: value - }); - - addressInfo.hashHex = addressInfo.hashBuffer.toString('hex'); - - // Collect data for subscribers - if (txmessages[addressInfo.hashHex]) { - txmessages[addressInfo.hashHex].outputIndexes.push(outputIndex); - } else { - txmessages[addressInfo.hashHex] = { - tx: tx, - height: height, - outputIndexes: [outputIndex], - addressInfo: addressInfo, - timestamp: block.header.timestamp - }; - } - - this.balanceEventHandler(block, addressInfo); - - } - - // Publish events to any subscribers for this transaction - for (var addressKey in txmessages) { - this.transactionEventHandler(txmessages[addressKey]); - } - - if(tx.isCoinbase()) { - continue; - } - - for(var inputIndex = 0; inputIndex < inputs.length; inputIndex++) { - - var input = inputs[inputIndex]; - var inputHash; - var inputHashType; - - if (input.script.isPublicKeyHashIn()) { - inputHash = Hash.sha256ripemd160(input.script.chunks[1].buf); - inputHashType = constants.HASH_TYPES.PUBKEY; - } else if (input.script.isScriptHashIn()) { - inputHash = Hash.sha256ripemd160(input.script.chunks[input.script.chunks.length - 1].buf); - inputHashType = constants.HASH_TYPES.REDEEMSCRIPT; - } else { - continue; - } - - var prevTxIdBuffer = new Buffer(input.prevTxId, 'hex'); - - // To be able to query inputs by address and spent height - var inputKey = encoding.encodeInputKey(inputHash, inputHashType, height, prevTxIdBuffer, input.outputIndex); - var inputValue = encoding.encodeInputValue(txidBuffer, inputIndex); - - operations.push({ - type: action, - key: inputKey, - value: inputValue - }); - - // To be able to search for an input spending an output - var inputKeyMap = encoding.encodeInputKeyMap(prevTxIdBuffer, input.outputIndex); - var inputValueMap = encoding.encodeInputValueMap(txidBuffer, inputIndex); - - operations.push({ - type: action, - key: inputKeyMap, - value: inputValueMap - }); - - } - } - - setImmediate(function() { - callback(null, operations); - }); -}; - -/** - * This function is responsible for emitting events to any subscribers to the - * `address/transaction` event. - * @param {Object} obj - * @param {Transaction} obj.tx - The transaction - * @param {Object} obj.addressInfo - * @param {String} obj.addressInfo.hashHex - The hex string of address hash for the subscription - * @param {String} obj.addressInfo.hashBuffer - The address hash buffer - * @param {String} obj.addressInfo.addressType - The address type - * @param {Array} obj.outputIndexes - Indexes of the inputs that includes the address - * @param {Array} obj.inputIndexes - Indexes of the outputs that includes the address - * @param {Date} obj.timestamp - The time of the block the transaction was included - * @param {Number} obj.height - The height of the block the transaction was included - * @param {Boolean} obj.rejected - If the transaction was not accepted in the mempool - */ -AddressService.prototype.transactionEventHandler = function(obj) { - if(this.subscriptions['address/transaction'][obj.addressInfo.hashHex]) { - var emitters = this.subscriptions['address/transaction'][obj.addressInfo.hashHex]; - var address = new Address({ - hashBuffer: obj.addressInfo.hashBuffer, - network: this.node.network, - type: obj.addressInfo.addressType - }); - for(var i = 0; i < emitters.length; i++) { - emitters[i].emit('address/transaction', { - rejected: obj.rejected, - height: obj.height, - timestamp: obj.timestamp, - inputIndexes: obj.inputIndexes, - outputIndexes: obj.outputIndexes, - address: address, - tx: obj.tx - }); - } - } -}; - -/** - * The function is responsible for emitting events to any subscribers for the - * `address/balance` event. - * @param {Block} block - * @param {Object} obj - * @param {String} obj.hashHex - * @param {Buffer} obj.hashBuffer - * @param {String} obj.addressType - */ -AddressService.prototype.balanceEventHandler = function(block, obj) { - if(this.subscriptions['address/balance'][obj.hashHex]) { - var emitters = this.subscriptions['address/balance'][obj.hashHex]; - var address = new Address({ - hashBuffer: obj.hashBuffer, - network: this.node.network, - type: obj.addressType - }); - this.getBalance(address, true, function(err, balance) { - if(err) { - return this.emit(err); - } - for(var i = 0; i < emitters.length; i++) { - emitters[i].emit('address/balance', address, balance, block); - } - }); - } -}; - -/** - * The Bus will use this function to subscribe to the available - * events for this service. For information about the available events - * please see `getPublishEvents`. - * @param {String} name - The name of the event - * @param {EventEmitter} emitter - An event emitter instance - * @param {Array} addresses - An array of addresses to subscribe - */ -AddressService.prototype.subscribe = function(name, emitter, addresses) { - $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); - $.checkArgument(Array.isArray(addresses), 'Second argument is expected to be an Array of addresses'); - - for(var i = 0; i < addresses.length; i++) { - var hashHex = bitcore.Address(addresses[i]).hashBuffer.toString('hex'); - if(!this.subscriptions[name][hashHex]) { - this.subscriptions[name][hashHex] = []; - } - this.subscriptions[name][hashHex].push(emitter); - } -}; - -/** - * The Bus will use this function to unsubscribe to the available - * events for this service. - * @param {String} name - The name of the event - * @param {EventEmitter} emitter - An event emitter instance - * @param {Array} addresses - An array of addresses to subscribe - */ -AddressService.prototype.unsubscribe = function(name, emitter, addresses) { - $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); - $.checkArgument(Array.isArray(addresses) || _.isUndefined(addresses), 'Second argument is expected to be an Array of addresses or undefined'); - - if(!addresses) { - return this.unsubscribeAll(name, emitter); - } - - for(var i = 0; i < addresses.length; i++) { - var hashHex = bitcore.Address(addresses[i]).hashBuffer.toString('hex'); - if(this.subscriptions[name][hashHex]) { - var emitters = this.subscriptions[name][hashHex]; - var index = emitters.indexOf(emitter); - if(index > -1) { - emitters.splice(index, 1); - } - } - } -}; - -/** - * A helper function for the `unsubscribe` method to unsubscribe from all addresses. - * @param {String} name - The name of the event - * @param {EventEmitter} emitter - An instance of an event emitter - */ -AddressService.prototype.unsubscribeAll = function(name, emitter) { - $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); - - for(var hashHex in this.subscriptions[name]) { - var emitters = this.subscriptions[name][hashHex]; - var index = emitters.indexOf(emitter); - if(index > -1) { - emitters.splice(index, 1); - } - } -}; - -/** - * Will sum the total of all unspent outputs to calculate the balance - * for an address. - * @param {String} address - The base58check encoded address - * @param {Boolean} queryMempool - Include mempool in the results - * @param {Function} callback - */ -AddressService.prototype.getBalance = function(address, queryMempool, callback) { - this.getUnspentOutputs(address, queryMempool, function(err, outputs) { - if(err) { - return callback(err); - } - - var satoshis = outputs.map(function(output) { - return output.satoshis; - }); - - var sum = satoshis.reduce(function(a, b) { - return a + b; - }, 0); - - return callback(null, sum); - }); -}; - -/** - * Will give the input that spends an output if it exists with: - * inputTxId - The input txid hex string - * inputIndex - A number with the spending input index - * @param {String|Buffer} txid - The transaction hash with the output - * @param {Number} outputIndex - The output index in the transaction - * @param {Object} options - * @param {Object} options.queryMempool - Include mempool in results - * @param {Function} callback - */ -AddressService.prototype.getInputForOutput = function(txid, outputIndex, options, callback) { - $.checkArgument(_.isNumber(outputIndex)); - $.checkArgument(_.isObject(options)); - $.checkArgument(_.isFunction(callback)); - var self = this; - var txidBuffer; - if (Buffer.isBuffer(txid)) { - txidBuffer = txid; - } else { - txidBuffer = new Buffer(txid, 'hex'); - } - if (options.queryMempool) { - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(txidBuffer, outputIndex); - if (this.mempoolSpentIndex[spentIndexSyncKey]) { - return this._getSpentMempool(txidBuffer, outputIndex, callback); - } - } - var key = encoding.encodeInputKeyMap(txidBuffer, outputIndex); - var dbOptions = { - valueEncoding: 'binary', - keyEncoding: 'binary' - }; - this.node.services.db.store.get(key, dbOptions, function(err, buffer) { - if (err instanceof levelup.errors.NotFoundError) { - return callback(null, false); - } else if (err) { - return callback(err); - } - var value = encoding.decodeInputValueMap(buffer); - callback(null, { - inputTxId: value.inputTxId.toString('hex'), - inputIndex: value.inputIndex - }); - }); -}; - -/** - * A streaming equivalent to `getInputs`, and returns a transform stream with data - * emitted in the same format as `getInputs`. - * - * @param {String} addressStr - The relevant address - * @param {Object} options - Additional options for query the outputs - * @param {Number} [options.start] - The relevant start block height - * @param {Number} [options.end] - The relevant end block height - * @param {Function} callback - */ -AddressService.prototype.createInputsStream = function(addressStr, options) { - var inputStream = new InputsTransformStream({ - address: new Address(addressStr, this.node.network), - tipHeight: this.node.services.db.tip.__height - }); - - var stream = this.createInputsDBStream(addressStr, options) - .on('error', function(err) { - // Forward the error - inputStream.emit('error', err); - inputStream.end(); - }).pipe(inputStream); - - return stream; - -}; - -AddressService.prototype.createInputsDBStream = function(addressStr, options) { - var stream; - var addrObj = encoding.getAddressInfo(addressStr); - var hashBuffer = addrObj.hashBuffer; - var hashTypeBuffer = addrObj.hashTypeBuffer; - - if (options.start >= 0 && options.end >= 0) { - - var endBuffer = new Buffer(4); - endBuffer.writeUInt32BE(options.end, 0); - - var startBuffer = new Buffer(4); - // Because the key has additional data following it, we don't have an ability - // to use "gte" or "lte" we can only use "gt" and "lt", we therefore need to adjust the number - // to be one value larger to include it. - var adjustedStart = options.start + 1; - startBuffer.writeUInt32BE(adjustedStart, 0); - - stream = this.node.services.db.store.createReadStream({ - gt: Buffer.concat([ - constants.PREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - endBuffer - ]), - lt: Buffer.concat([ - constants.PREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - startBuffer - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - } else { - var allKey = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer]); - stream = this.node.services.db.store.createReadStream({ - gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]), - lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - } - - return stream; -}; - -/** - * Will give inputs that spend previous outputs for an address as an object with: - * address - The base58check encoded address - * hashtype - The type of the address, e.g. 'pubkeyhash' or 'scripthash' - * txid - A string of the transaction hash - * outputIndex - A number of corresponding transaction input - * height - The height of the block the transaction was included, will be -1 for mempool transactions - * confirmations - The number of confirmations, will equal 0 for mempool transactions - * - * @param {String} addressStr - The relevant address - * @param {Object} options - Additional options for query the outputs - * @param {Number} [options.start] - The relevant start block height - * @param {Number} [options.end] - The relevant end block height - * @param {Boolean} [options.queryMempool] - Include the mempool in the results - * @param {Function} callback - */ -AddressService.prototype.getInputs = function(addressStr, options, callback) { - - var self = this; - - var inputs = []; - - var addrObj = encoding.getAddressInfo(addressStr); - var hashBuffer = addrObj.hashBuffer; - var hashTypeBuffer = addrObj.hashTypeBuffer; - - var stream = this.createInputsStream(addressStr, options); - - stream.on('data', function(input) { - inputs.push(input); - if (inputs.length > self.maxInputsQueryLength) { - log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for address '+ addressStr); - error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached'); - stream.end(); - } - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('finish', function() { - if (error) { - return callback(error); - } - - if(options.queryMempool) { - self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) { - if (err) { - return callback(err); - } - inputs = inputs.concat(mempoolInputs); - callback(null, inputs); - }); - } else { - callback(null, inputs); - } - - }); - - return stream; - -}; - -AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) { - var self = this; - var mempoolInputs = []; - - var stream = self.mempoolIndex.createReadStream({ - gte: Buffer.concat([ - constants.MEMPREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN - ]), - lte: Buffer.concat([ - constants.MEMPREFIXES.SPENTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MAX - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - - stream.on('data', function(data) { - var txid = data.value.slice(0, 32); - var inputIndex = data.value.readUInt32BE(32); - var timestamp = data.value.readDoubleBE(36); - var input = { - address: addressStr, - hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], - txid: txid.toString('hex'), //TODO use a buffer - inputIndex: inputIndex, - timestamp: timestamp, - height: -1, - confirmations: 0 - }; - mempoolInputs.push(input); - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('close', function() { - if (error) { - return callback(error); - } - callback(null, mempoolInputs); - }); - -}; - -AddressService.prototype._getSpentMempool = function(txidBuffer, outputIndex, callback) { - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var spentIndexKey = Buffer.concat([ - constants.MEMPREFIXES.SPENTSMAP, - txidBuffer, - outputIndexBuffer - ]); - - this.mempoolIndex.get( - spentIndexKey, - function(err, mempoolValue) { - if (err) { - return callback(err); - } - var inputTxId = mempoolValue.slice(0, 32); - var inputIndex = mempoolValue.readUInt32BE(32); - callback(null, { - inputTxId: inputTxId.toString('hex'), - inputIndex: inputIndex - }); - } - ); -}; - -AddressService.prototype.createOutputsStream = function(addressStr, options) { - var outputStream = new OutputsTransformStream({ - address: new Address(addressStr, this.node.network), - tipHeight: this.node.services.db.tip.__height - }); - - var stream = this.createOutputsDBStream(addressStr, options) - .on('error', function(err) { - // Forward the error - outputStream.emit('error', err); - outputStream.end(); - }) - .pipe(outputStream); - - return stream; - -}; - -AddressService.prototype.createOutputsDBStream = function(addressStr, options) { - - var addrObj = encoding.getAddressInfo(addressStr); - var hashBuffer = addrObj.hashBuffer; - var hashTypeBuffer = addrObj.hashTypeBuffer; - var stream; - - if (options.start >= 0 && options.end >= 0) { - - var endBuffer = new Buffer(4); - endBuffer.writeUInt32BE(options.end, 0); - - var startBuffer = new Buffer(4); - // Because the key has additional data following it, we don't have an ability - // to use "gte" or "lte" we can only use "gt" and "lt", we therefore need to adjust the number - // to be one value larger to include it. - var startAdjusted = options.start + 1; - startBuffer.writeUInt32BE(startAdjusted, 0); - - stream = this.node.services.db.store.createReadStream({ - gt: Buffer.concat([ - constants.PREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - endBuffer - ]), - lt: Buffer.concat([ - constants.PREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN, - startBuffer - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - } else { - var allKey = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer]); - stream = this.node.services.db.store.createReadStream({ - gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]), - lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - } - - return stream; - -}; - -/** - * Will give outputs for an address as an object with: - * address - The base58check encoded address - * hashtype - The type of the address, e.g. 'pubkeyhash' or 'scripthash' - * txid - A string of the transaction hash - * outputIndex - A number of corresponding transaction output - * height - The height of the block the transaction was included, will be -1 for mempool transactions - * satoshis - The satoshis value of the output - * script - The script of the output as a hex string - * confirmations - The number of confirmations, will equal 0 for mempool transactions - * - * @param {String} addressStr - The relevant address - * @param {Object} options - Additional options for query the outputs - * @param {Number} [options.start] - The relevant start block height - * @param {Number} [options.end] - The relevant end block height - * @param {Boolean} [options.queryMempool] - Include the mempool in the results - * @param {Function} callback - */ -AddressService.prototype.getOutputs = function(addressStr, options, callback) { - var self = this; - $.checkArgument(_.isObject(options), 'Second argument is expected to be an options object.'); - $.checkArgument(_.isFunction(callback), 'Third argument is expected to be a callback function.'); - - var addrObj = encoding.getAddressInfo(addressStr); - var hashBuffer = addrObj.hashBuffer; - var hashTypeBuffer = addrObj.hashTypeBuffer; - if (!hashTypeBuffer) { - return callback(new Error('Unknown address type: ' + addrObj.hashTypeReadable + ' for address: ' + addressStr)); - } - - var outputs = []; - var stream = this.createOutputsStream(addressStr, options); - - stream.on('data', function(data) { - outputs.push(data); - if (outputs.length > self.maxOutputsQueryLength) { - log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for address ' + addressStr); - error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached'); - stream.end(); - } - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('finish', function() { - if (error) { - return callback(error); - } - - if(options.queryMempool) { - self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { - if (err) { - return callback(err); - } - outputs = outputs.concat(mempoolOutputs); - callback(null, outputs); - }); - } else { - callback(null, outputs); - } - }); - - return stream; - -}; - -AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) { - var self = this; - var mempoolOutputs = []; - - var stream = self.mempoolIndex.createReadStream({ - gte: Buffer.concat([ - constants.MEMPREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MIN - ]), - lte: Buffer.concat([ - constants.MEMPREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - constants.SPACER_MAX - ]), - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - - stream.on('data', function(data) { - // Format of data: - // prefix: 1, hashBuffer: 20, hashTypeBuffer: 1, txid: 32, outputIndex: 4 - var txid = data.key.slice(22, 54); - var outputIndex = data.key.readUInt32BE(54); - var value = encoding.decodeOutputMempoolValue(data.value); - var output = { - address: addressStr, - hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], - txid: txid.toString('hex'), //TODO use a buffer - outputIndex: outputIndex, - height: -1, - timestamp: value.timestamp, - satoshis: value.satoshis, - script: value.scriptBuffer.toString('hex'), //TODO use a buffer - confirmations: 0 - }; - mempoolOutputs.push(output); - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('close', function() { - if (error) { - return callback(error); - } - callback(null, mempoolOutputs); - }); - -}; - -/** - * Will give unspent outputs for an address or an array of addresses. - * @param {Array|String} addresses - An array of addresses - * @param {Boolean} queryMempool - Include or exclude the mempool - * @param {Function} callback - */ -AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) { - var self = this; - - if(!Array.isArray(addresses)) { - addresses = [addresses]; - } - - var utxos = []; - - async.eachSeries(addresses, function(address, next) { - self.getUnspentOutputsForAddress(address, queryMempool, function(err, unspents) { - if(err && err instanceof errors.NoOutputs) { - return next(); - } else if(err) { - return next(err); - } - - utxos = utxos.concat(unspents); - next(); - }); - }, function(err) { - callback(err, utxos); - }); -}; - -/** - * Will give unspent outputs for an address. - * @param {String} address - An address in base58check encoding - * @param {Boolean} queryMempool - Include or exclude the mempool - * @param {Function} callback - */ -AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) { - - var self = this; - - this.getOutputs(address, {queryMempool: queryMempool}, function(err, outputs) { - if (err) { - return callback(err); - } else if(!outputs.length) { - return callback(new errors.NoOutputs('Address ' + address + ' has no outputs'), []); - } - - var opts = { - queryMempool: queryMempool - }; - - var isUnspent = function(output, callback) { - self.isUnspent(output, opts, callback); - }; - - async.filter(outputs, isUnspent, function(results) { - callback(null, results); - }); - }); -}; - -/** - * Will give the inverse of isSpent - * @param {Object} output - * @param {Object} options - * @param {Boolean} options.queryMempool - Include mempool in results - * @param {Function} callback - */ -AddressService.prototype.isUnspent = function(output, options, callback) { - $.checkArgument(_.isFunction(callback)); - this.isSpent(output, options, function(spent) { - callback(!spent); - }); -}; - -/** - * Will determine if an output is spent. - * @param {Object} output - An output as returned from getOutputs - * @param {Object} options - * @param {Boolean} options.queryMempool - Include mempool in results - * @param {Function} callback - */ -AddressService.prototype.isSpent = function(output, options, callback) { - $.checkArgument(_.isFunction(callback)); - var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool; - var self = this; - var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; - var spent = self.node.services.bitcoind.isSpent(txid, output.outputIndex); - if (!spent && queryMempool) { - var txidBuffer = new Buffer(txid, 'hex'); - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(txidBuffer, output.outputIndex); - spent = self.mempoolSpentIndex[spentIndexSyncKey] ? true : false; - } - setImmediate(function() { - // TODO error should be the first argument? - callback(spent); - }); -}; - - -/** - * This will give the history for many addresses limited by a range of block heights (to limit - * the database lookup times) and/or paginated to limit the results length. - * - * The response format will be: - * { - * totalCount: 12 // the total number of items there are between the two heights - * items: [ - * { - * addresses: { - * '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX': { - * inputIndexes: [], - * outputIndexes: [0] - * } - * }, - * satoshis: 100, - * height: 300000, - * confirmations: 1, - * timestamp: 1442337090 // in seconds - * fees: 1000 // in satoshis - * tx: - * } - * ] - * } - * @param {Array} addresses - An array of addresses - * @param {Object} options - The options to limit the query - * @param {Number} [options.from] - The pagination "from" index - * @param {Number} [options.to] - The pagination "to" index - * @param {Number} [options.start] - The beginning block height (e.g. 1500 the most recent block height). - * @param {Number} [options.end] - The ending block height (e.g. 0 the older block height, results are inclusive). - * @param {Boolean} [options.queryMempool] - Include the mempool in the query - * @param {Function} callback - */ -AddressService.prototype.getAddressHistory = function(addresses, options, callback) { - var history = new AddressHistory({ - node: this.node, - options: options, - addresses: addresses - }); - history.get(callback); -}; - -/** - * This will give an object with: - * balance - confirmed balance - * unconfirmedBalance - unconfirmed balance - * totalReceived - satoshis received - * totalSpent - satoshis spent - * appearances - number of transactions - * unconfirmedAppearances - number of unconfirmed transactions - * txids - list of txids (unless noTxList is set) - * - * @param {String} address - * @param {Object} options - * @param {Boolean} [options.noTxList] - if set, txid array will not be included - * @param {Function} callback - */ -AddressService.prototype.getAddressSummary = function(addressArg, options, callback) { - var self = this; - - var startTime = new Date(); - var address = new Address(addressArg); - - if (_.isUndefined(options.queryMempool)) { - options.queryMempool = true; - } - - async.waterfall([ - function(next) { - self._getAddressConfirmedSummary(address, options, next); - }, - function(result, next) { - self._getAddressMempoolSummary(address, options, result, next); - }, - function(result, next) { - self._setAndSortTxidsFromAppearanceIds(result, next); - } - ], function(err, result) { - if (err) { - return callback(err); - } - - var summary = self._transformAddressSummaryFromResult(result, options); - - var timeDelta = new Date() - startTime; - if (timeDelta > 5000) { - var seconds = Math.round(timeDelta / 1000); - log.warn('Slow (' + seconds + 's) getAddressSummary request for address: ' + address.toString()); - } - - callback(null, summary); - - }); - -}; - -AddressService.prototype._getAddressConfirmedSummary = function(address, options, callback) { - var self = this; - var baseResult = { - appearanceIds: {}, - totalReceived: 0, - balance: 0, - unconfirmedAppearanceIds: {}, - unconfirmedBalance: 0 - }; - - async.waterfall([ - function(next) { - self._getAddressConfirmedInputsSummary(address, baseResult, options, next); - }, - function(result, next) { - self._getAddressConfirmedOutputsSummary(address, result, options, next); - } - ], callback); - -}; - -AddressService.prototype._getAddressConfirmedInputsSummary = function(address, result, options, callback) { - $.checkArgument(address instanceof Address); - var self = this; - var error = null; - var count = 0; - - var inputsStream = self.createInputsStream(address, options); - inputsStream.on('data', function(input) { - var txid = input.txid; - result.appearanceIds[txid] = input.height; - - count++; - - if (count > self.maxInputsQueryLength) { - log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for summary of address ' + address.toString()); - error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached'); - inputsStream.end(); - } - - }); - - inputsStream.on('error', function(err) { - error = err; - }); - - inputsStream.on('end', function() { - if (error) { - return callback(error); - } - callback(null, result); - }); -}; - -AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, result, options, callback) { - $.checkArgument(address instanceof Address); - $.checkArgument(!_.isUndefined(result) && - !_.isUndefined(result.appearanceIds) && - !_.isUndefined(result.unconfirmedAppearanceIds)); - - var self = this; - var count = 0; - - var outputStream = self.createOutputsStream(address, options); - - outputStream.on('data', function(output) { - - var txid = output.txid; - var outputIndex = output.outputIndex; - result.totalReceived += output.satoshis; - result.appearanceIds[txid] = output.height; - - if(!options.noBalance) { - - // Bitcoind's isSpent only works for confirmed transactions - var spentDB = self.node.services.bitcoind.isSpent(txid, outputIndex); - - if(!spentDB) { - result.balance += output.satoshis; - } - - if(options.queryMempool) { - // Check to see if this output is spent in the mempool and if so - // we will subtract it from the unconfirmedBalance (a.k.a unconfirmedDelta) - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey( - new Buffer(txid, 'hex'), // TODO: get buffer directly - outputIndex - ); - var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; - if(spentMempool) { - result.unconfirmedBalance -= output.satoshis; - } - } - } - - count++; - - if (count > self.maxOutputsQueryLength) { - log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for summary of address ' + address.toString()); - error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached'); - outputStream.end(); - } - - }); - - var error = null; - - outputStream.on('error', function(err) { - error = err; - }); - - outputStream.on('end', function() { - if (error) { - return callback(error); - } - callback(null, result); - }); - -}; - -AddressService.prototype._setAndSortTxidsFromAppearanceIds = function(result, callback) { - result.txids = Object.keys(result.appearanceIds); - result.txids.sort(function(a, b) { - return result.appearanceIds[a] - result.appearanceIds[b]; - }); - result.unconfirmedTxids = Object.keys(result.unconfirmedAppearanceIds); - result.unconfirmedTxids.sort(function(a, b) { - return result.unconfirmedAppearanceIds[a] - result.unconfirmedAppearanceIds[b]; - }); - callback(null, result); -}; - -AddressService.prototype._getAddressMempoolSummary = function(address, options, result, callback) { - var self = this; - - // Skip if the options do not want to include the mempool - if (!options.queryMempool) { - return callback(null, result); - } - - var addressStr = address.toString(); - var hashBuffer = address.hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; - var addressIndexKey = encoding.encodeMempoolAddressIndexKey(hashBuffer, hashTypeBuffer); - - if(!this.mempoolAddressIndex[addressIndexKey]) { - return callback(null, result); - } - - async.waterfall([ - function(next) { - self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) { - if (err) { - return next(err); - } - for(var i = 0; i < mempoolInputs.length; i++) { - var input = mempoolInputs[i]; - result.unconfirmedAppearanceIds[input.txid] = input.timestamp; - } - next(null, result); - }); - - }, function(result, next) { - self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { - if (err) { - return next(err); - } - for(var i = 0; i < mempoolOutputs.length; i++) { - var output = mempoolOutputs[i]; - - result.unconfirmedAppearanceIds[output.txid] = output.timestamp; - - if(!options.noBalance) { - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey( - new Buffer(output.txid, 'hex'), // TODO: get buffer directly - output.outputIndex - ); - var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; - // Only add this to the balance if it's not spent in the mempool already - if(!spentMempool) { - result.unconfirmedBalance += output.satoshis; - } - } - } - next(null, result); - }); - } - ], callback); -}; - -AddressService.prototype._transformAddressSummaryFromResult = function(result, options) { - - var confirmedTxids = result.txids; - var unconfirmedTxids = result.unconfirmedTxids; - - var summary = { - totalReceived: result.totalReceived, - totalSpent: result.totalReceived - result.balance, - balance: result.balance, - appearances: confirmedTxids.length, - unconfirmedBalance: result.unconfirmedBalance, - unconfirmedAppearances: unconfirmedTxids.length - }; - - if (options.fullTxList) { - summary.appearanceIds = result.appearanceIds; - summary.unconfirmedAppearanceIds = result.unconfirmedAppearanceIds; - } else if (!options.noTxList) { - summary.txids = confirmedTxids.concat(unconfirmedTxids); - } - - return summary; - -}; - -module.exports = AddressService; diff --git a/lib/services/address/streams/inputs-transform.js b/lib/services/address/streams/inputs-transform.js deleted file mode 100644 index 8b8f71d3d..000000000 --- a/lib/services/address/streams/inputs-transform.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -var Transform = require('stream').Transform; -var inherits = require('util').inherits; -var bitcore = require('bitcore-lib'); -var encodingUtil = require('../encoding'); -var $ = bitcore.util.preconditions; - -function InputsTransformStream(options) { - $.checkArgument(options.address instanceof bitcore.Address); - Transform.call(this, { - objectMode: true - }); - this._address = options.address; - this._addressStr = this._address.toString(); - this._tipHeight = options.tipHeight; -} -inherits(InputsTransformStream, Transform); - -InputsTransformStream.prototype._transform = function(chunk, encoding, callback) { - var self = this; - - var key = encodingUtil.decodeInputKey(chunk.key); - var value = encodingUtil.decodeInputValue(chunk.value); - - var input = { - address: this._addressStr, - hashType: this._address.type, - txid: value.txid.toString('hex'), - inputIndex: value.inputIndex, - height: key.height, - confirmations: this._tipHeight - key.height + 1 - }; - - self.push(input); - callback(); - -}; - -module.exports = InputsTransformStream; diff --git a/lib/services/address/streams/outputs-transform.js b/lib/services/address/streams/outputs-transform.js deleted file mode 100644 index b9c8e8d36..000000000 --- a/lib/services/address/streams/outputs-transform.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -var Transform = require('stream').Transform; -var inherits = require('util').inherits; -var bitcore = require('bitcore-lib'); -var encodingUtil = require('../encoding'); -var $ = bitcore.util.preconditions; - -function OutputsTransformStream(options) { - Transform.call(this, { - objectMode: true - }); - $.checkArgument(options.address instanceof bitcore.Address); - this._address = options.address; - this._addressStr = this._address.toString(); - this._tipHeight = options.tipHeight; -} -inherits(OutputsTransformStream, Transform); - -OutputsTransformStream.prototype._transform = function(chunk, encoding, callback) { - var self = this; - - var key = encodingUtil.decodeOutputKey(chunk.key); - var value = encodingUtil.decodeOutputValue(chunk.value); - - var output = { - address: this._addressStr, - hashType: this._address.type, - txid: key.txid.toString('hex'), //TODO use a buffer - outputIndex: key.outputIndex, - height: key.height, - satoshis: value.satoshis, - script: value.scriptBuffer.toString('hex'), //TODO use a buffer - confirmations: this._tipHeight - key.height + 1 - }; - - self.push(output); - callback(); - -}; - -module.exports = OutputsTransformStream; diff --git a/lib/services/bitcoind.js b/lib/services/bitcoind.js index edc2dad6c..4e79fcd62 100644 --- a/lib/services/bitcoind.js +++ b/lib/services/bitcoind.js @@ -1,18 +1,29 @@ 'use strict'; var fs = require('fs'); +var spawn = require('child_process').spawn; var util = require('util'); -var bindings = require('bindings')('bitcoind.node'); var mkdirp = require('mkdirp'); var bitcore = require('bitcore-lib'); +var zmq = require('zmq'); +var async = require('async'); +var LRU = require('lru-cache'); +var BitcoinRPC = require('bitcoind-rpc'); var $ = bitcore.util.preconditions; +var _ = bitcore.deps._; +var Transaction = bitcore.Transaction; + var index = require('../'); +var errors = index.errors; var log = index.log; var Service = require('../service'); /** - * Provides an interface to native bindings to [Bitcoin Core](https://github.com/bitcoin/bitcoin) - * compiled as a static library. The C++ bindings can be found at `src/libbitcoind.cc` + * Provides a friendly event driven API to bitcoind in Node.js. Manages starting and + * stopping bitcoind as a child process for application support, as well as connecting + * to multiple bitcoind processes for server infrastructure. Results are cached in an + * LRU cache for improved performance and methods added for common queries. + * * @param {Object} options * @param {Node} options.node - A reference to the node */ @@ -21,328 +32,2006 @@ function Bitcoin(options) { return new Bitcoin(options); } - this._reindex = false; - this._reindexWait = 1000; - Service.call(this, options); - $.checkState(this.node.datadir, 'Node is missing datadir property'); -} + Service.call(this, options); + this.options = options; + + this._initCaches(); + + // bitcoind child process + this.spawn = false; + + // event subscribers + this.subscriptions = {}; + this.subscriptions.rawtransaction = []; + this.subscriptions.hashblock = []; + this.subscriptions.address = {}; + + // set initial settings + this._initDefaults(options); + + // available bitcoind nodes + this._initClients(); + + // for testing purposes + this._process = options.process || process; + + this.on('error', function(err) { + log.error(err.stack); + }); +} +util.inherits(Bitcoin, Service); + +Bitcoin.dependencies = []; + +Bitcoin.DEFAULT_MAX_TXIDS = 1000; +Bitcoin.DEFAULT_MAX_HISTORY = 50; +Bitcoin.DEFAULT_SHUTDOWN_TIMEOUT = 15000; +Bitcoin.DEFAULT_ZMQ_SUBSCRIBE_PROGRESS = 0.9999; +Bitcoin.DEFAULT_MAX_ADDRESSES_QUERY = 10000; +Bitcoin.DEFAULT_SPAWN_RESTART_TIME = 5000; +Bitcoin.DEFAULT_SPAWN_STOP_TIME = 10000; +Bitcoin.DEFAULT_TRY_ALL_INTERVAL = 1000; +Bitcoin.DEFAULT_REINDEX_INTERVAL = 10000; +Bitcoin.DEFAULT_START_RETRY_INTERVAL = 5000; +Bitcoin.DEFAULT_TIP_UPDATE_INTERVAL = 15000; +Bitcoin.DEFAULT_CONFIG_SETTINGS = { + server: 1, + whitelist: '127.0.0.1', + txindex: 1, + addressindex: 1, + timestampindex: 1, + spentindex: 1, + zmqpubrawtx: 'tcp://127.0.0.1:28332', + zmqpubhashblock: 'tcp://127.0.0.1:28332', + rpcallowip: '127.0.0.1', + rpcuser: 'bitcoin', + rpcpassword: 'local321', + uacomment: 'bitcore' +}; + +Bitcoin.prototype._initDefaults = function(options) { + // limits + this.maxTxids = options.maxTxids || Bitcoin.DEFAULT_MAX_TXIDS; + this.maxTransactionHistory = options.maxTransactionHistory || Bitcoin.DEFAULT_MAX_HISTORY; + this.maxAddressesQuery = options.maxAddressesQuery || Bitcoin.DEFAULT_MAX_ADDRESSES_QUERY; + this.shutdownTimeout = options.shutdownTimeout || Bitcoin.DEFAULT_SHUTDOWN_TIMEOUT; + + // spawn restart setting + this.spawnRestartTime = options.spawnRestartTime || Bitcoin.DEFAULT_SPAWN_RESTART_TIME; + this.spawnStopTime = options.spawnStopTime || Bitcoin.DEFAULT_SPAWN_STOP_TIME; + + // try all interval + this.tryAllInterval = options.tryAllInterval || Bitcoin.DEFAULT_TRY_ALL_INTERVAL; + this.startRetryInterval = options.startRetryInterval || Bitcoin.DEFAULT_START_RETRY_INTERVAL; + + // sync progress level when zmq subscribes to events + this.zmqSubscribeProgress = options.zmqSubscribeProgress || Bitcoin.DEFAULT_ZMQ_SUBSCRIBE_PROGRESS; +}; + +Bitcoin.prototype._initCaches = function() { + // caches valid until there is a new block + this.utxosCache = LRU(50000); + this.txidsCache = LRU(50000); + this.balanceCache = LRU(50000); + this.summaryCache = LRU(50000); + this.blockOverviewCache = LRU(144); + this.transactionDetailedCache = LRU(100000); + + // caches valid indefinitely + this.transactionCache = LRU(100000); + this.rawTransactionCache = LRU(50000); + this.blockCache = LRU(144); + this.rawBlockCache = LRU(72); + this.blockHeaderCache = LRU(288); + this.zmqKnownTransactions = LRU(5000); + this.zmqKnownBlocks = LRU(50); + this.lastTip = 0; + this.lastTipTimeout = false; +}; + +Bitcoin.prototype._initClients = function() { + var self = this; + this.nodes = []; + this.nodesIndex = 0; + Object.defineProperty(this, 'client', { + get: function() { + var client = self.nodes[self.nodesIndex].client; + self.nodesIndex = (self.nodesIndex + 1) % self.nodes.length; + return client; + }, + enumerable: true, + configurable: false + }); +}; + +/** + * Called by Node to determine the available API methods. + */ +Bitcoin.prototype.getAPIMethods = function() { + var methods = [ + ['getBlock', this, this.getBlock, 1], + ['getRawBlock', this, this.getRawBlock, 1], + ['getBlockHeader', this, this.getBlockHeader, 1], + ['getBlockOverview', this, this.getBlockOverview, 1], + ['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2], + ['getBestBlockHash', this, this.getBestBlockHash, 0], + ['getSpentInfo', this, this.getSpentInfo, 1], + ['getInfo', this, this.getInfo, 0], + ['syncPercentage', this, this.syncPercentage, 0], + ['isSynced', this, this.isSynced, 0], + ['getRawTransaction', this, this.getRawTransaction, 1], + ['getTransaction', this, this.getTransaction, 1], + ['getDetailedTransaction', this, this.getDetailedTransaction, 1], + ['sendTransaction', this, this.sendTransaction, 1], + ['estimateFee', this, this.estimateFee, 1], + ['getAddressTxids', this, this.getAddressTxids, 2], + ['getAddressBalance', this, this.getAddressBalance, 2], + ['getAddressUnspentOutputs', this, this.getAddressUnspentOutputs, 2], + ['getAddressHistory', this, this.getAddressHistory, 2], + ['getAddressSummary', this, this.getAddressSummary, 1], + ['generateBlock', this, this.generateBlock, 1] + ]; + return methods; +}; + +/** + * Called by the Bus to determine the available events. + */ +Bitcoin.prototype.getPublishEvents = function() { + return [ + { + name: 'bitcoind/rawtransaction', + scope: this, + subscribe: this.subscribe.bind(this, 'rawtransaction'), + unsubscribe: this.unsubscribe.bind(this, 'rawtransaction') + }, + { + name: 'bitcoind/hashblock', + scope: this, + subscribe: this.subscribe.bind(this, 'hashblock'), + unsubscribe: this.unsubscribe.bind(this, 'hashblock') + }, + { + name: 'bitcoind/addresstxid', + scope: this, + subscribe: this.subscribeAddress.bind(this), + unsubscribe: this.unsubscribeAddress.bind(this) + } + ]; +}; + +Bitcoin.prototype.subscribe = function(name, emitter) { + this.subscriptions[name].push(emitter); + log.info(emitter.remoteAddress, 'subscribe:', 'bitcoind/' + name, 'total:', this.subscriptions[name].length); +}; + +Bitcoin.prototype.unsubscribe = function(name, emitter) { + var index = this.subscriptions[name].indexOf(emitter); + if (index > -1) { + this.subscriptions[name].splice(index, 1); + } + log.info(emitter.remoteAddress, 'unsubscribe:', 'bitcoind/' + name, 'total:', this.subscriptions[name].length); +}; + +Bitcoin.prototype.subscribeAddress = function(emitter, addresses) { + var self = this; + + function addAddress(addressStr) { + if(self.subscriptions.address[addressStr]) { + var emitters = self.subscriptions.address[addressStr]; + var index = emitters.indexOf(emitter); + if (index === -1) { + self.subscriptions.address[addressStr].push(emitter); + } + } else { + self.subscriptions.address[addressStr] = [emitter]; + } + } + + for(var i = 0; i < addresses.length; i++) { + if (bitcore.Address.isValid(addresses[i], this.node.network)) { + addAddress(addresses[i]); + } + } + + log.info(emitter.remoteAddress, 'subscribe:', 'bitcoind/addresstxid', 'total:', _.size(this.subscriptions.address)); +}; + +Bitcoin.prototype.unsubscribeAddress = function(emitter, addresses) { + var self = this; + if(!addresses) { + return this.unsubscribeAddressAll(emitter); + } + + function removeAddress(addressStr) { + var emitters = self.subscriptions.address[addressStr]; + var index = emitters.indexOf(emitter); + if(index > -1) { + emitters.splice(index, 1); + if (emitters.length === 0) { + delete self.subscriptions.address[addressStr]; + } + } + } + + for(var i = 0; i < addresses.length; i++) { + if(this.subscriptions.address[addresses[i]]) { + removeAddress(addresses[i]); + } + } + + log.info(emitter.remoteAddress, 'unsubscribe:', 'bitcoind/addresstxid', 'total:', _.size(this.subscriptions.address)); +}; + +/** + * A helper function for the `unsubscribe` method to unsubscribe from all addresses. + * @param {String} name - The name of the event + * @param {EventEmitter} emitter - An instance of an event emitter + */ +Bitcoin.prototype.unsubscribeAddressAll = function(emitter) { + for(var hashHex in this.subscriptions.address) { + var emitters = this.subscriptions.address[hashHex]; + var index = emitters.indexOf(emitter); + if(index > -1) { + emitters.splice(index, 1); + } + if (emitters.length === 0) { + delete this.subscriptions.address[hashHex]; + } + } + log.info(emitter.remoteAddress, 'unsubscribe:', 'bitcoind/addresstxid', 'total:', _.size(this.subscriptions.address)); +}; + +Bitcoin.prototype._getDefaultConfig = function() { + var config = ''; + var defaults = Bitcoin.DEFAULT_CONFIG_SETTINGS; + for(var key in defaults) { + config += key + '=' + defaults[key] + '\n'; + } + return config; +}; + +Bitcoin.prototype._parseBitcoinConf = function(configPath) { + var options = {}; + var file = fs.readFileSync(configPath); + var unparsed = file.toString().split('\n'); + for(var i = 0; i < unparsed.length; i++) { + var line = unparsed[i]; + if (!line.match(/^\#/) && line.match(/\=/)) { + var option = line.split('='); + var value; + if (!Number.isNaN(Number(option[1]))) { + value = Number(option[1]); + } else { + value = option[1]; + } + options[option[0]] = value; + } + } + return options; +}; + +Bitcoin.prototype._loadSpawnConfiguration = function(node) { + /* jshint maxstatements: 25 */ + + $.checkArgument(this.options.spawn, 'Please specify "spawn" in bitcoind config options'); + $.checkArgument(this.options.spawn.datadir, 'Please specify "spawn.datadir" in bitcoind config options'); + $.checkArgument(this.options.spawn.exec, 'Please specify "spawn.exec" in bitcoind config options'); + + var spawnOptions = this.options.spawn; + var configPath = spawnOptions.datadir + '/bitcoin.conf'; + + this.spawn = {}; + this.spawn.datadir = this.options.spawn.datadir; + this.spawn.exec = this.options.spawn.exec; + this.spawn.configPath = configPath; + this.spawn.config = {}; + + if (!fs.existsSync(spawnOptions.datadir)) { + mkdirp.sync(spawnOptions.datadir); + } + + if (!fs.existsSync(configPath)) { + var defaultConfig = this._getDefaultConfig(); + fs.writeFileSync(configPath, defaultConfig); + } + + _.extend(this.spawn.config, this._getDefaultConf()); + _.extend(this.spawn.config, this._parseBitcoinConf(configPath)); + + var networkConfigPath = this._getNetworkConfigPath(); + if (networkConfigPath && fs.existsSync(networkConfigPath)) { + _.extend(this.spawn.config, this._parseBitcoinConf(networkConfigPath)); + } + + var spawnConfig = this.spawn.config; + + this._checkConfigIndexes(spawnConfig, node); + +}; + +Bitcoin.prototype._checkConfigIndexes = function(spawnConfig, node) { + $.checkState( + spawnConfig.txindex && spawnConfig.txindex === 1, + '"txindex" option is required in order to use transaction query features of bitcore-node. ' + + 'Please add "txindex=1" to your configuration and reindex an existing database if ' + + 'necessary with reindex=1' + ); + + $.checkState( + spawnConfig.addressindex && spawnConfig.addressindex === 1, + '"addressindex" option is required in order to use address query features of bitcore-node. ' + + 'Please add "addressindex=1" to your configuration and reindex an existing database if ' + + 'necessary with reindex=1' + ); + + $.checkState( + spawnConfig.spentindex && spawnConfig.spentindex === 1, + '"spentindex" option is required in order to use spent info query features of bitcore-node. ' + + 'Please add "spentindex=1" to your configuration and reindex an existing database if ' + + 'necessary with reindex=1' + ); + + $.checkState( + spawnConfig.server && spawnConfig.server === 1, + '"server" option is required to communicate to bitcoind from bitcore. ' + + 'Please add "server=1" to your configuration and restart' + ); + + $.checkState( + spawnConfig.zmqpubrawtx, + '"zmqpubrawtx" option is required to get event updates from bitcoind. ' + + 'Please add "zmqpubrawtx=tcp://127.0.0.1:" to your configuration and restart' + ); + + $.checkState( + spawnConfig.zmqpubhashblock, + '"zmqpubhashblock" option is required to get event updates from bitcoind. ' + + 'Please add "zmqpubhashblock=tcp://127.0.0.1:" to your configuration and restart' + ); + + if (spawnConfig.reindex && spawnConfig.reindex === 1) { + log.warn('Reindex option is currently enabled. This means that bitcoind is undergoing a reindex. ' + + 'The reindex flag will start the index from beginning every time the node is started, so it ' + + 'should be removed after the reindex has been initiated. Once the reindex is complete, the rest ' + + 'of bitcore-node services will start.'); + node._reindex = true; + } +}; + +Bitcoin.prototype._resetCaches = function() { + this.transactionDetailedCache.reset(); + this.utxosCache.reset(); + this.txidsCache.reset(); + this.balanceCache.reset(); + this.summaryCache.reset(); + this.blockOverviewCache.reset(); +}; + +Bitcoin.prototype._tryAll = function(func, callback) { + async.retry({times: this.nodes.length, interval: this.tryAllInterval || 1000}, func, callback); +}; + +Bitcoin.prototype._wrapRPCError = function(errObj) { + var err = new errors.RPCError(errObj.message); + err.code = errObj.code; + return err; +}; + +Bitcoin.prototype._initChain = function(callback) { + var self = this; + + self.client.getBestBlockHash(function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + + self.client.getBlock(response.result, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + + self.height = response.result.height; + + self.client.getBlockHash(0, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + var blockhash = response.result; + self.getRawBlock(blockhash, function(err, blockBuffer) { + if (err) { + return callback(err); + } + self.genesisBuffer = blockBuffer; + self.emit('ready'); + log.info('Bitcoin Daemon Ready'); + callback(); + }); + }); + + }); + }); +}; + +Bitcoin.prototype._getDefaultConf = function() { + var networkOptions = { + rpcport: 8332 + }; + if (this.node.network === bitcore.Networks.testnet) { + networkOptions.rpcport = 18332; + } + return networkOptions; +}; + +Bitcoin.prototype._getNetworkConfigPath = function() { + var networkPath; + if (this.node.network === bitcore.Networks.testnet) { + networkPath = 'testnet3/bitcoin.conf'; + if (this.node.network.regtestEnabled) { + networkPath = 'regtest/bitcoin.conf'; + } + } + return networkPath; +}; + +Bitcoin.prototype._getNetworkOption = function() { + var networkOption; + if (this.node.network === bitcore.Networks.testnet) { + networkOption = '--testnet'; + if (this.node.network.regtestEnabled) { + networkOption = '--regtest'; + } + } + return networkOption; +}; + +Bitcoin.prototype._zmqBlockHandler = function(node, message) { + var self = this; + + // Update the current chain tip + self._rapidProtectedUpdateTip(node, message); + + // Notify block subscribers + var id = message.toString('binary'); + if (!self.zmqKnownBlocks.get(id)) { + self.zmqKnownBlocks.set(id, true); + self.emit('block', message); + + for (var i = 0; i < this.subscriptions.hashblock.length; i++) { + this.subscriptions.hashblock[i].emit('bitcoind/hashblock', message.toString('hex')); + } + } + +}; + +Bitcoin.prototype._rapidProtectedUpdateTip = function(node, message) { + var self = this; + + // Prevent a rapid succession of tip updates + if (new Date() - self.lastTip > 1000) { + self.lastTip = new Date(); + self._updateTip(node, message); + } else { + clearTimeout(self.lastTipTimeout); + self.lastTipTimeout = setTimeout(function() { + self._updateTip(node, message); + }, 1000); + } +}; + +Bitcoin.prototype._updateTip = function(node, message) { + var self = this; + + var hex = message.toString('hex'); + if (hex !== self.tiphash) { + self.tiphash = message.toString('hex'); + + // reset block valid caches + self._resetCaches(); + + node.client.getBlock(self.tiphash, function(err, response) { + if (err) { + var error = self._wrapRPCError(err); + self.emit('error', error); + } else { + self.height = response.result.height; + $.checkState(self.height >= 0); + self.emit('tip', self.height); + } + }); + + if(!self.node.stopping) { + self.syncPercentage(function(err, percentage) { + if (err) { + self.emit('error', err); + } else { + if (Math.round(percentage) >= 100) { + self.emit('synced', self.height); + } + log.info('Bitcoin Height:', self.height, 'Percentage:', percentage.toFixed(2)); + } + }); + } + } +}; + +Bitcoin.prototype._getAddressesFromTransaction = function(transaction) { + var addresses = []; + + for (var i = 0; i < transaction.inputs.length; i++) { + var input = transaction.inputs[i]; + if (input.script) { + var inputAddress = input.script.toAddress(this.node.network); + if (inputAddress) { + addresses.push(inputAddress.toString()); + } + } + } + + for (var j = 0; j < transaction.outputs.length; j++) { + var output = transaction.outputs[j]; + if (output.script) { + var outputAddress = output.script.toAddress(this.node.network); + if (outputAddress) { + addresses.push(outputAddress.toString()); + } + } + } + + return _.uniq(addresses); +}; + +Bitcoin.prototype._notifyAddressTxidSubscribers = function(txid, transaction) { + var addresses = this._getAddressesFromTransaction(transaction); + for (var i = 0; i < addresses.length; i++) { + var address = addresses[i]; + if(this.subscriptions.address[address]) { + var emitters = this.subscriptions.address[address]; + for(var j = 0; j < emitters.length; j++) { + emitters[j].emit('bitcoind/addresstxid', { + address: address, + txid: txid + }); + } + } + } +}; + +Bitcoin.prototype._zmqTransactionHandler = function(node, message) { + var self = this; + var hash = bitcore.crypto.Hash.sha256sha256(message); + var id = hash.toString('binary'); + if (!self.zmqKnownTransactions.get(id)) { + self.zmqKnownTransactions.set(id, true); + self.emit('tx', message); + + // Notify transaction subscribers + for (var i = 0; i < this.subscriptions.rawtransaction.length; i++) { + this.subscriptions.rawtransaction[i].emit('bitcoind/rawtransaction', message.toString('hex')); + } + + var tx = bitcore.Transaction(); + tx.fromString(message); + var txid = bitcore.util.buffer.reverse(hash).toString('hex'); + self._notifyAddressTxidSubscribers(txid, tx); + + } +}; + +Bitcoin.prototype._checkSyncedAndSubscribeZmqEvents = function(node) { + var self = this; + var interval; + + function checkAndSubscribe(callback) { + // update tip + node.client.getBestBlockHash(function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + var blockhash = new Buffer(response.result, 'hex'); + self.emit('block', blockhash); + self._updateTip(node, blockhash); + + // check if synced + node.client.getBlockchainInfo(function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + var progress = response.result.verificationprogress; + if (progress >= self.zmqSubscribeProgress) { + // subscribe to events for further updates + self._subscribeZmqEvents(node); + clearInterval(interval); + callback(null, true); + } else { + callback(null, false); + } + }); + }); + } + + checkAndSubscribe(function(err, synced) { + if (err) { + log.error(err); + } + if (!synced) { + interval = setInterval(function() { + if (self.node.stopping) { + return clearInterval(interval); + } + checkAndSubscribe(function(err) { + if (err) { + log.error(err); + } + }); + }, node._tipUpdateInterval || Bitcoin.DEFAULT_TIP_UPDATE_INTERVAL); + } + }); + +}; + +Bitcoin.prototype._subscribeZmqEvents = function(node) { + var self = this; + node.zmqSubSocket.subscribe('hashblock'); + node.zmqSubSocket.subscribe('rawtx'); + node.zmqSubSocket.on('message', function(topic, message) { + var topicString = topic.toString('utf8'); + if (topicString === 'rawtx') { + self._zmqTransactionHandler(node, message); + } else if (topicString === 'hashblock') { + self._zmqBlockHandler(node, message); + } + }); +}; + +Bitcoin.prototype._initZmqSubSocket = function(node, zmqUrl) { + var self = this; + node.zmqSubSocket = zmq.socket('sub'); + + node.zmqSubSocket.on('connect', function(fd, endPoint) { + log.info('ZMQ connected to:', endPoint); + }); + + node.zmqSubSocket.on('connect_delay', function(fd, endPoint) { + log.warn('ZMQ connection delay:', endPoint); + }); + + node.zmqSubSocket.on('disconnect', function(fd, endPoint) { + log.warn('ZMQ disconnect:', endPoint); + }); + + node.zmqSubSocket.on('monitor_error', function(err) { + log.error('Error in monitoring: %s, will restart monitoring in 5 seconds', err); + setTimeout(function() { + self.zmqSubSocket.monitor(500, 0); + }, 5000); + }); + + node.zmqSubSocket.monitor(500, 0); + node.zmqSubSocket.connect(zmqUrl); +}; + +Bitcoin.prototype._checkReindex = function(node, callback) { + var self = this; + var interval; + function finish(err) { + clearInterval(interval); + callback(err); + } + if (node._reindex) { + interval = setInterval(function() { + node.client.getBlockchainInfo(function(err, response) { + if (err) { + return finish(self._wrapRPCError(err)); + } + var percentSynced = response.result.verificationprogress * 100; + + log.info('Bitcoin Core Daemon Reindex Percentage: ' + percentSynced.toFixed(2)); + + if (Math.round(percentSynced) >= 100) { + node._reindex = false; + finish(); + } + }); + }, node._reindexWait || Bitcoin.DEFAULT_REINDEX_INTERVAL); + } else { + callback(); + } +}; + +Bitcoin.prototype._loadTipFromNode = function(node, callback) { + var self = this; + node.client.getBestBlockHash(function(err, response) { + if (err && err.code === -28) { + log.warn(err.message); + return callback(self._wrapRPCError(err)); + } else if (err) { + return callback(self._wrapRPCError(err)); + } + node.client.getBlock(response.result, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + self.height = response.result.height; + $.checkState(self.height >= 0); + self.emit('tip', self.height); + callback(); + }); + }); +}; + +Bitcoin.prototype._stopSpawnedBitcoin = function(callback) { + var self = this; + var spawnOptions = this.options.spawn; + var pidPath = spawnOptions.datadir + '/bitcoind.pid'; + + function stopProcess() { + fs.readFile(pidPath, 'utf8', function(err, pid) { + if (err && err.code === 'ENOENT') { + // pid file doesn't exist we can continue + return callback(null); + } else if (err) { + return callback(err); + } + pid = parseInt(pid); + if (!Number.isFinite(pid)) { + // pid doesn't exist we can continue + return callback(null); + } + try { + log.warn('Stopping existing spawned bitcoin process with pid: ' + pid); + self._process.kill(pid, 'SIGINT'); + } catch(err) { + if (err && err.code === 'ESRCH') { + log.warn('Unclean bitcoin process shutdown, process not found with pid: ' + pid); + return callback(null); + } else if(err) { + return callback(err); + } + } + setTimeout(function() { + stopProcess(); + }, self.spawnStopTime); + }); + } + + stopProcess(); +}; + +Bitcoin.prototype._spawnChildProcess = function(callback) { + var self = this; + + var node = {}; + node._reindex = false; + node._reindexWait = 10000; + + try { + self._loadSpawnConfiguration(node); + } catch(e) { + return callback(e); + } + + var options = [ + '--conf=' + this.spawn.configPath, + '--datadir=' + this.spawn.datadir, + ]; + + if (self._getNetworkOption()) { + options.push(self._getNetworkOption()); + } + + self._stopSpawnedBitcoin(function(err) { + if (err) { + return callback(err); + } + + log.info('Starting bitcoin process'); + self.spawn.process = spawn(self.spawn.exec, options, {stdio: 'inherit'}); + + self.spawn.process.on('error', function(err) { + self.emit('error', err); + }); + + self.spawn.process.once('exit', function(code) { + if (!self.node.stopping) { + log.warn('Bitcoin process unexpectedly exited with code:', code); + log.warn('Restarting bitcoin child process in ' + self.spawnRestartTime + 'ms'); + setTimeout(function() { + self._spawnChildProcess(function(err) { + if (err) { + return self.emit('error', err); + } + log.warn('Bitcoin process restarted'); + }); + }, self.spawnRestartTime); + } + }); + + var exitShutdown = false; + + async.retry({times: 60, interval: self.startRetryInterval}, function(done) { + if (self.node.stopping) { + exitShutdown = true; + return done(); + } + + node.client = new BitcoinRPC({ + protocol: 'http', + host: '127.0.0.1', + port: self.spawn.config.rpcport, + user: self.spawn.config.rpcuser, + pass: self.spawn.config.rpcpassword + }); + + self._loadTipFromNode(node, done); + + }, function(err) { + if (err) { + return callback(err); + } + if (exitShutdown) { + return callback(new Error('Stopping while trying to spawn bitcoind.')); + } + + self._initZmqSubSocket(node, self.spawn.config.zmqpubrawtx); + + self._checkReindex(node, function(err) { + if (err) { + return callback(err); + } + self._checkSyncedAndSubscribeZmqEvents(node); + callback(null, node); + }); + + }); + + }); + +}; + +Bitcoin.prototype._connectProcess = function(config, callback) { + var self = this; + var node = {}; + var exitShutdown = false; + + async.retry({times: 60, interval: self.startRetryInterval}, function(done) { + if (self.node.stopping) { + exitShutdown = true; + return done(); + } + + node.client = new BitcoinRPC({ + protocol: config.rpcprotocol || 'http', + host: config.rpchost || '127.0.0.1', + port: config.rpcport, + user: config.rpcuser, + pass: config.rpcpassword + }); + + self._loadTipFromNode(node, done); + + }, function(err) { + if (err) { + return callback(err); + } + if (exitShutdown) { + return callback(new Error('Stopping while trying to connect to bitcoind.')); + } + + self._initZmqSubSocket(node, config.zmqpubrawtx); + self._subscribeZmqEvents(node); + + callback(null, node); + }); +}; + +/** + * Called by Node to start the service + * @param {Function} callback + */ +Bitcoin.prototype.start = function(callback) { + var self = this; + + async.series([ + function(next) { + if (self.options.spawn) { + self._spawnChildProcess(function(err, node) { + if (err) { + return next(err); + } + self.nodes.push(node); + next(); + }); + } else { + next(); + } + }, + function(next) { + if (self.options.connect) { + async.map(self.options.connect, self._connectProcess.bind(self), function(err, nodes) { + if (err) { + return callback(err); + } + for(var i = 0; i < nodes.length; i++) { + self.nodes.push(nodes[i]); + } + next(); + }); + } else { + next(); + } + } + ], function(err) { + if (err) { + return callback(err); + } + if (self.nodes.length === 0) { + return callback(new Error('Bitcoin configuration options "spawn" or "connect" are expected')); + } + self._initChain(callback); + }); + +}; + +/** + * Helper to determine the state of the database. + * @param {Function} callback + */ +Bitcoin.prototype.isSynced = function(callback) { + this.syncPercentage(function(err, percentage) { + if (err) { + return callback(err); + } + if (Math.round(percentage) >= 100) { + callback(null, true); + } else { + callback(null, false); + } + }); +}; + +/** + * Helper to determine the progress of the database. + * @param {Function} callback + */ +Bitcoin.prototype.syncPercentage = function(callback) { + var self = this; + this.client.getBlockchainInfo(function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + var percentSynced = response.result.verificationprogress * 100; + callback(null, percentSynced); + }); +}; + +Bitcoin.prototype._normalizeAddressArg = function(addressArg) { + var addresses = [addressArg]; + if (Array.isArray(addressArg)) { + addresses = addressArg; + } + return addresses; +}; + +/** + * Will get the balance for an address or multiple addresses + * @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses + * @param {Object} options + * @param {Function} callback + */ +Bitcoin.prototype.getAddressBalance = function(addressArg, options, callback) { + var self = this; + var addresses = self._normalizeAddressArg(addressArg); + var cacheKey = addresses.join(''); + var balance = self.balanceCache.get(cacheKey); + if (balance) { + return setImmediate(function() { + callback(null, balance); + }); + } else { + this.client.getAddressBalance({addresses: addresses}, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + self.balanceCache.set(cacheKey, response.result); + callback(null, response.result); + }); + } +}; + +/** + * Will get the unspent outputs for an address or multiple addresses + * @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses + * @param {Object} options + * @param {Function} callback + */ +Bitcoin.prototype.getAddressUnspentOutputs = function(addressArg, options, callback) { + var self = this; + var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool; + var addresses = self._normalizeAddressArg(addressArg); + var cacheKey = addresses.join(''); + var utxos = self.utxosCache.get(cacheKey); + + function transformUnspentOutput(delta) { + var script = bitcore.Script.fromAddress(delta.address); + return { + address: delta.address, + txid: delta.txid, + outputIndex: delta.index, + script: script.toHex(), + satoshis: delta.satoshis, + timestamp: delta.timestamp + }; + } + + function updateWithMempool(confirmedUtxos, mempoolDeltas) { + /* jshint maxstatements: 20 */ + if (!mempoolDeltas || !mempoolDeltas.length) { + return confirmedUtxos; + } + var isSpentOutputs = false; + var mempoolUnspentOutputs = []; + var spentOutputs = []; + + for (var i = 0; i < mempoolDeltas.length; i++) { + var delta = mempoolDeltas[i]; + if (delta.prevtxid && delta.satoshis <= 0) { + if (!spentOutputs[delta.prevtxid]) { + spentOutputs[delta.prevtxid] = [delta.prevout]; + } else { + spentOutputs[delta.prevtxid].push(delta.prevout); + } + isSpentOutputs = true; + } else { + mempoolUnspentOutputs.push(transformUnspentOutput(delta)); + } + } + + var utxos = mempoolUnspentOutputs.reverse().concat(confirmedUtxos); -util.inherits(Bitcoin, Service); + if (isSpentOutputs) { + return utxos.filter(function(utxo) { + if (!spentOutputs[utxo.txid]) { + return true; + } else { + return (spentOutputs[utxo.txid].indexOf(utxo.outputIndex) === -1); + } + }); + } -Bitcoin.dependencies = []; + return utxos; + } -Bitcoin.DEFAULT_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n'; + function finish(mempoolDeltas) { + if (utxos) { + return setImmediate(function() { + callback(null, updateWithMempool(utxos, mempoolDeltas)); + }); + } else { + self.client.getAddressUtxos({addresses: addresses}, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + var utxos = response.result.reverse(); + self.utxosCache.set(cacheKey, utxos); + callback(null, updateWithMempool(utxos, mempoolDeltas)); + }); + } + } -Bitcoin.prototype._loadConfiguration = function() { - /* jshint maxstatements: 25 */ + if (queryMempool) { + self.client.getAddressMempool({addresses: addresses}, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + finish(response.result); + }); + } else { + finish(); + } - $.checkArgument(this.node.datadir, 'Please specify "datadir" in configuration options'); - var configPath = this.node.datadir + '/bitcoin.conf'; - this.configuration = {}; +}; - if (!fs.existsSync(this.node.datadir)) { - mkdirp.sync(this.node.datadir); +Bitcoin.prototype._getBalanceFromMempool = function(deltas) { + var satoshis = 0; + for (var i = 0; i < deltas.length; i++) { + satoshis += deltas[i].satoshis; } + return satoshis; +}; - if (!fs.existsSync(configPath)) { - var defaultConfig = Bitcoin.DEFAULT_CONFIG; - if(this.node.https && this.node.httpsOptions) { - defaultConfig += 'rpcssl=1\n'; - defaultConfig += 'rpcsslprivatekeyfile=' + this.node.httpsOptions.key + '\n'; - defaultConfig += 'rpcsslcertificatechainfile=' + this.node.httpsOptions.cert + '\n'; +Bitcoin.prototype._getTxidsFromMempool = function(deltas) { + var mempoolTxids = []; + var mempoolTxidsKnown = {}; + for (var i = 0; i < deltas.length; i++) { + var txid = deltas[i].txid; + if (!mempoolTxidsKnown[txid]) { + mempoolTxids.push(txid); + mempoolTxidsKnown[txid] = true; } - fs.writeFileSync(configPath, defaultConfig); } + return mempoolTxids; +}; - var file = fs.readFileSync(configPath); - var unparsed = file.toString().split('\n'); - for(var i = 0; i < unparsed.length; i++) { - var line = unparsed[i]; - if (!line.match(/^\#/) && line.match(/\=/)) { - var option = line.split('='); - var value; - if (!Number.isNaN(Number(option[1]))) { - value = Number(option[1]); - } else { - value = option[1]; - } - this.configuration[option[0]] = value; +Bitcoin.prototype._getHeightRangeQuery = function(options, clone) { + if (options.start >= 0 && options.end >= 0) { + if (options.end > options.start) { + throw new TypeError('"end" is expected to be less than or equal to "start"'); } + if (clone) { + // reverse start and end as the order in bitcore is most recent to less recent + clone.start = options.end; + clone.end = options.start; + } + return true; } + return false; +}; - $.checkState( - this.configuration.txindex && this.configuration.txindex === 1, - 'Txindex option is required in order to use most of the features of bitcore-node. ' + - 'Please add "txindex=1" to your configuration and reindex an existing database if ' + - 'necessary with reindex=1' - ); +/** + * Will get the txids for an address or multiple addresses + * @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses + * @param {Object} options + * @param {Function} callback + */ +Bitcoin.prototype.getAddressTxids = function(addressArg, options, callback) { + /* jshint maxstatements: 16 */ + var self = this; + var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool; + var rangeQuery = false; + try { + rangeQuery = self._getHeightRangeQuery(options); + } catch(err) { + return callback(err); + } + if (rangeQuery) { + queryMempool = false; + } + var addresses = self._normalizeAddressArg(addressArg); + var cacheKey = addresses.join(''); + var mempoolTxids = []; + var txids = self.txidsCache.get(cacheKey); - if (this.configuration.reindex && this.configuration.reindex === 1) { - log.warn('Reindex option is currently enabled. This means that bitcoind is undergoing a reindex. ' + - 'The reindex flag will start the index from beginning every time the node is started, so it ' + - 'should be removed after the reindex has been initiated. Once the reindex is complete, the rest ' + - 'of bitcore-node services will start.'); - this._reindex = true; + function finish() { + if (txids && !rangeQuery) { + var allTxids = mempoolTxids.reverse().concat(txids); + return setImmediate(function() { + callback(null, allTxids); + }); + } else { + var txidOpts = { + addresses: addresses + }; + if (rangeQuery) { + self._getHeightRangeQuery(options, txidOpts); + } + self.client.getAddressTxids(txidOpts, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + response.result.reverse(); + if (!rangeQuery) { + self.txidsCache.set(cacheKey, response.result); + } + var allTxids = mempoolTxids.reverse().concat(response.result); + return callback(null, allTxids); + }); + } + } + + if (queryMempool) { + self.client.getAddressMempool({addresses: addresses}, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + mempoolTxids = self._getTxidsFromMempool(response.result); + finish(); + }); + } else { + finish(); } }; -Bitcoin.prototype._onTipUpdate = function(result) { - if (result) { - // Emit and event that the tip was updated - this.height = result; - this.emit('tip', result); +Bitcoin.prototype._getConfirmationsDetail = function(transaction) { + $.checkState(this.height > 0, 'current height is unknown'); + var confirmations = 0; + if (transaction.height >= 0) { + confirmations = this.height - transaction.height + 1; + } + if (confirmations < 0) { + log.warn('Negative confirmations calculated for transaction:', transaction.hash); + } + return Math.max(0, confirmations); +}; + +Bitcoin.prototype._getAddressDetailsForInput = function(input, inputIndex, result, addressStrings) { + if (!input.address) { + return; + } + var address = input.address; + if (addressStrings.indexOf(address) >= 0) { + if (!result.addresses[address]) { + result.addresses[address] = { + inputIndexes: [inputIndex], + outputIndexes: [] + }; + } else { + result.addresses[address].inputIndexes.push(inputIndex); + } + result.satoshis -= input.satoshis; + } +}; - // TODO stopping status - if(!this.node.stopping) { - var percentage = this.syncPercentage(); - log.info('Bitcoin Height:', this.height, 'Percentage:', percentage); +Bitcoin.prototype._getAddressDetailsForOutput = function(output, outputIndex, result, addressStrings) { + if (!output.address) { + return; + } + var address = output.address; + if (addressStrings.indexOf(address) >= 0) { + if (!result.addresses[address]) { + result.addresses[address] = { + inputIndexes: [], + outputIndexes: [outputIndex] + }; + } else { + result.addresses[address].outputIndexes.push(outputIndex); } + result.satoshis += output.satoshis; + } +}; + +Bitcoin.prototype._getAddressDetailsForTransaction = function(transaction, addressStrings) { + var result = { + addresses: {}, + satoshis: 0 + }; + + for (var inputIndex = 0; inputIndex < transaction.inputs.length; inputIndex++) { + var input = transaction.inputs[inputIndex]; + this._getAddressDetailsForInput(input, inputIndex, result, addressStrings); + } - // Recursively wait until the next update - bindings.onTipUpdate(this._onTipUpdate.bind(this)); + for (var outputIndex = 0; outputIndex < transaction.outputs.length; outputIndex++) { + var output = transaction.outputs[outputIndex]; + this._getAddressDetailsForOutput(output, outputIndex, result, addressStrings); } + + $.checkState(Number.isFinite(result.satoshis)); + + return result; }; -Bitcoin.prototype._registerEventHandlers = function() { +/** + * Will expand into a detailed transaction from a txid + * @param {Object} txid - A bitcoin transaction id + * @param {Function} callback + */ +Bitcoin.prototype._getAddressDetailedTransaction = function(txid, options, next) { var self = this; - // Set the height and emit a new tip - bindings.onTipUpdate(self._onTipUpdate.bind(this)); + self.getDetailedTransaction( + txid, + function(err, transaction) { + if (err) { + return next(err); + } + + var addressDetails = self._getAddressDetailsForTransaction(transaction, options.addressStrings); - // Register callback function to handle transactions entering the mempool - bindings.startTxMon(function(txs) { - for(var i = 0; i < txs.length; i++) { - self.emit('tx', txs[i]); + var details = { + addresses: addressDetails.addresses, + satoshis: addressDetails.satoshis, + confirmations: self._getConfirmationsDetail(transaction), + tx: transaction + }; + next(null, details); } - }); + ); +}; - // Register callback function to handle transactions leaving the mempool - bindings.startTxMonLeave(function(txs) { - for(var i = 0; i < txs.length; i++) { - self.emit('txleave', txs[i]); +Bitcoin.prototype._getAddressStrings = function(addresses) { + var addressStrings = []; + for (var i = 0; i < addresses.length; i++) { + var address = addresses[i]; + if (address instanceof bitcore.Address) { + addressStrings.push(address.toString()); + } else if (_.isString(address)) { + addressStrings.push(address); + } else { + throw new TypeError('Addresses are expected to be strings'); } - }); + } + return addressStrings; +}; + +Bitcoin.prototype._paginateTxids = function(fullTxids, fromArg, toArg) { + var txids; + var from = parseInt(fromArg); + var to = parseInt(toArg); + $.checkState(from < to, '"from" (' + from + ') is expected to be less than "to" (' + to + ')'); + txids = fullTxids.slice(from, to); + return txids; }; -Bitcoin.prototype._onReady = function(result, callback) { +/** + * Will detailed transaction history for an address or multiple addresses + * @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses + * @param {Object} options + * @param {Function} callback + */ +Bitcoin.prototype.getAddressHistory = function(addressArg, options, callback) { var self = this; + var addresses = self._normalizeAddressArg(addressArg); + if (addresses.length > this.maxAddressesQuery) { + return callback(new TypeError('Maximum number of addresses (' + this.maxAddressesQuery + ') exceeded')); + } + + var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool; + var addressStrings = this._getAddressStrings(addresses); - self._registerEventHandlers(); + var fromArg = parseInt(options.from || 0); + var toArg = parseInt(options.to || self.maxTransactionHistory); - var info = self.getInfo(); - self.height = info.blocks; + if ((toArg - fromArg) > self.maxTransactionHistory) { + return callback(new Error( + '"from" (' + options.from + ') and "to" (' + options.to + ') range should be less than or equal to ' + + self.maxTransactionHistory + )); + } - self.getBlock(0, function(err, block) { + self.getAddressTxids(addresses, options, function(err, txids) { if (err) { return callback(err); } - self.genesisBuffer = block; - self.emit('ready', result); - log.info('Bitcoin Daemon Ready'); - callback(); - }); + var totalCount = txids.length; + try { + txids = self._paginateTxids(txids, fromArg, toArg); + } catch(e) { + return callback(e); + } + + async.mapSeries( + txids, + function(txid, next) { + self._getAddressDetailedTransaction(txid, { + queryMempool: queryMempool, + addressStrings: addressStrings + }, next); + }, + function(err, transactions) { + if (err) { + return callback(err); + } + callback(null, { + totalCount: totalCount, + items: transactions + }); + } + ); + }); }; /** - * Called by Node to start the service + * Will get the summary including txids and balance for an address or multiple addresses + * @param {String|Address|Array} addressArg - An address string, bitcore address, or array of addresses + * @param {Object} options * @param {Function} callback */ -Bitcoin.prototype.start = function(callback) { +Bitcoin.prototype.getAddressSummary = function(addressArg, options, callback) { var self = this; + var summary = {}; + var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool; + var summaryTxids = []; + var mempoolTxids = []; + var addresses = self._normalizeAddressArg(addressArg); + var cacheKey = addresses.join(''); - this._loadConfiguration(); + function finishWithTxids() { + if (!options.noTxList) { + var allTxids = mempoolTxids.reverse().concat(summaryTxids); + var fromArg = parseInt(options.from || 0); + var toArg = parseInt(options.to || self.maxTxids); - var networkName = this.node.network.name; - if (this.node.network.regtestEnabled) { - networkName = 'regtest'; - } + if ((toArg - fromArg) > self.maxTxids) { + return callback(new Error( + '"from" (' + fromArg + ') and "to" (' + toArg + ') range should be less than or equal to ' + + self.maxTxids + )); + } + var paginatedTxids; + try { + paginatedTxids = self._paginateTxids(allTxids, fromArg, toArg); + } catch(e) { + return callback(e); + } - bindings.start({ - datadir: this.node.datadir, - network: networkName - }, function(err) { - if(err) { - return callback(err); + var allSummary = _.clone(summary); + allSummary.txids = paginatedTxids; + callback(null, allSummary); + } else { + callback(null, summary); } - // Wait until the block chain is ready - bindings.onBlocksReady(function(err, result) { + } + + function querySummary() { + async.parallel([ + function getTxList(done) { + self.getAddressTxids(addresses, {queryMempool: false}, function(err, txids) { + if (err) { + return done(err); + } + summaryTxids = txids; + summary.appearances = txids.length; + done(); + }); + }, + function getBalance(done) { + self.getAddressBalance(addresses, options, function(err, data) { + if (err) { + return done(err); + } + summary.totalReceived = data.received; + summary.totalSpent = data.received - data.balance; + summary.balance = data.balance; + done(); + }); + }, + function getMempool(done) { + if (!queryMempool) { + return done(); + } + self.client.getAddressMempool({'addresses': addresses}, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + mempoolTxids = self._getTxidsFromMempool(response.result); + summary.unconfirmedAppearances = mempoolTxids.length; + summary.unconfirmedBalance = self._getBalanceFromMempool(response.result); + done(); + }); + }, + ], function(err) { if (err) { return callback(err); } - if (self._reindex) { - var interval = setInterval(function() { - var percentSynced = bindings.syncPercentage(); - log.info("Bitcoin Core Daemon Reindex Percentage: " + percentSynced); - if (percentSynced >= 100) { - self._reindex = false; - self._onReady(result, callback); - clearInterval(interval); - } - }, self._reindexWait); - - } - else { - self._onReady(result, callback); - } + self.summaryCache.set(cacheKey, summary); + finishWithTxids(); }); - }); + } + + if (options.noTxList) { + var summaryCache = self.summaryCache.get(cacheKey); + if (summaryCache) { + callback(null, summaryCache); + } else { + querySummary(); + } + } else { + querySummary(); + } + +}; + +Bitcoin.prototype._maybeGetBlockHash = function(blockArg, callback) { + var self = this; + if (_.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg))) { + self._tryAll(function(done) { + self.client.getBlockHash(blockArg, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + done(null, response.result); + }); + }, callback); + } else { + callback(null, blockArg); + } }; /** - * Helper to determine the state of the database. - * @returns {Boolean} If the database is fully synced + * Will retrieve a block as a Node.js Buffer + * @param {String|Number} block - A block hash or block height number + * @param {Function} callback */ -Bitcoin.prototype.isSynced = function() { - return bindings.isSynced(); +Bitcoin.prototype.getRawBlock = function(blockArg, callback) { + // TODO apply performance patch to the RPC method for raw data + var self = this; + + function queryBlock(err, blockhash) { + if (err) { + return callback(err); + } + self._tryAll(function(done) { + self.client.getBlock(blockhash, false, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + var buffer = new Buffer(response.result, 'hex'); + self.rawBlockCache.set(blockhash, buffer); + done(null, buffer); + }); + }, callback); + } + + var cachedBlock = self.rawBlockCache.get(blockArg); + if (cachedBlock) { + return setImmediate(function() { + callback(null, cachedBlock); + }); + } else { + self._maybeGetBlockHash(blockArg, queryBlock); + } }; /** - * Helper to determine the progress of the database. - * @returns {Number} An estimated percentage of the syncronization status + * Similar to getBlockHeader but will include a list of txids + * @param {String|Number} block - A block hash or block height number + * @param {Function} callback */ -Bitcoin.prototype.syncPercentage = function() { - return bindings.syncPercentage(); +Bitcoin.prototype.getBlockOverview = function(blockArg, callback) { + var self = this; + + function queryBlock(err, blockhash) { + if (err) { + return callback(err); + } + var cachedBlock = self.blockOverviewCache.get(blockhash); + if (cachedBlock) { + return setImmediate(function() { + callback(null, cachedBlock); + }); + } else { + self._tryAll(function(done) { + self.client.getBlock(blockhash, true, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + var result = response.result; + var blockOverview = { + hash: result.hash, + version: result.version, + confirmations: result.confirmations, + height: result.height, + chainWork: result.chainwork, + prevHash: result.previousblockhash, + nextHash: result.nextblockhash, + merkleRoot: result.merkleroot, + time: result.time, + medianTime: result.mediantime, + nonce: result.nonce, + bits: result.bits, + difficulty: result.difficulty, + txids: result.tx + }; + self.blockOverviewCache.set(blockhash, blockOverview); + done(null, blockOverview); + }); + }, callback); + } + } + + self._maybeGetBlockHash(blockArg, queryBlock); }; /** - * Will retrieve a block as a Node.js Buffer from disk. + * Will retrieve a block as a Bitcore object * @param {String|Number} block - A block hash or block height number + * @param {Function} callback */ -Bitcoin.prototype.getBlock = function(block, callback) { - return bindings.getBlock(block, callback); +Bitcoin.prototype.getBlock = function(blockArg, callback) { + // TODO apply performance patch to the RPC method for raw data + var self = this; + + function queryBlock(err, blockhash) { + if (err) { + return callback(err); + } + var cachedBlock = self.blockCache.get(blockhash); + if (cachedBlock) { + return setImmediate(function() { + callback(null, cachedBlock); + }); + } else { + self._tryAll(function(done) { + self.client.getBlock(blockhash, false, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + var blockObj = bitcore.Block.fromString(response.result); + self.blockCache.set(blockhash, blockObj); + done(null, blockObj); + }); + }, callback); + } + } + + self._maybeGetBlockHash(blockArg, queryBlock); }; /** - * Will return the spent status of an output (not including the mempool) - * @param {String} txid - The transaction hash - * @param {Number} outputIndex - The output index in the transaction - * @returns {Boolean} If the output has been spent + * Will retrieve an array of block hashes within a range of timestamps + * @param {Number} high - The more recent timestamp in seconds + * @param {Number} low - The older timestamp in seconds + * @param {Function} callback */ -Bitcoin.prototype.isSpent = function(txid, outputIndex) { - return bindings.isSpent(txid, outputIndex); +Bitcoin.prototype.getBlockHashesByTimestamp = function(high, low, callback) { + var self = this; + self.client.getBlockHashes(high, low, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + callback(null, response.result); + }); }; /** * Will return the block index information, the output will have the format: * { - * prevHash: '7194fcf33f58c96720f88f21ab28c34ebc5638c5f88d7838517deb27313b59de', - * hash: '7c5caf0af1bf16e3467b275a3b408bc1d251bff3c25be20cb727c47b66a7b216', - * chainWork: '0000000000000000000000000000000000000000000000000000000000000016', - * height: 10 + * hash: '0000000000000a817cd3a74aec2f2246b59eb2cbb1ad730213e6c4a1d68ec2f6', + * confirmations: 5, + * height: 828781, + * chainWork: '00000000000000000000000000000000000000000000000ad467352c93bc6a3b', + * prevHash: '0000000000000504235b2aff578a48470dbf6b94dafa9b3703bbf0ed554c9dd9', + * nextHash: '00000000000000eedd967ec155f237f033686f0924d574b946caf1b0e89551b8' + * version: 536870912, + * merkleRoot: '124e0f3fb5aa268f102b0447002dd9700988fc570efcb3e0b5b396ac7db437a9', + * time: 1462979126, + * medianTime: 1462976771, + * nonce: 2981820714, + * bits: '1a13ca10', + * difficulty: 847779.0710240941, * } * @param {String|Number} block - A block hash or block height - * @returns {Object} + * @param {Function} callback */ -Bitcoin.prototype.getBlockIndex = function(block) { - return bindings.getBlockIndex(block); -}; +Bitcoin.prototype.getBlockHeader = function(blockArg, callback) { + var self = this; -/** - * Will return if the block is a part of the main chain. - * @param {String} blockHash - * @returns {Boolean} - */ -Bitcoin.prototype.isMainChain = function(blockHash) { - return bindings.isMainChain(blockHash); + function queryHeader(err, blockhash) { + if (err) { + return callback(err); + } + self._tryAll(function(done) { + self.client.getBlockHeader(blockhash, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + var result = response.result; + var header = { + hash: result.hash, + version: result.version, + confirmations: result.confirmations, + height: result.height, + chainWork: result.chainwork, + prevHash: result.previousblockhash, + nextHash: result.nextblockhash, + merkleRoot: result.merkleroot, + time: result.time, + medianTime: result.mediantime, + nonce: result.nonce, + bits: result.bits, + difficulty: result.difficulty + }; + done(null, header); + }); + }, callback); + } + + self._maybeGetBlockHash(blockArg, queryHeader); }; /** * Will estimate the fee per kilobyte. * @param {Number} blocks - The number of blocks for the transaction to be confirmed. - * @returns {Number} + * @param {Function} callback */ -Bitcoin.prototype.estimateFee = function(blocks) { - return bindings.estimateFee(blocks); +Bitcoin.prototype.estimateFee = function(blocks, callback) { + var self = this; + this.client.estimateFee(blocks, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + callback(null, response.result); + }); }; /** - * Will add a transaction to the mempool and relay to connected peers, the function - * will throw an error if there were validation problems. - * @param {String} transaction - The hex string of the transaction - * @param {Boolean} allowAbsurdFees - Enable large fees + * Will add a transaction to the mempool and relay to connected peers + * @param {String|Transaction} transaction - The hex string of the transaction + * @param {Object=} options + * @param {Boolean=} options.allowAbsurdFees - Enable large fees + * @param {Function} callback */ -Bitcoin.prototype.sendTransaction = function(transaction, allowAbsurdFees) { - return bindings.sendTransaction(transaction, allowAbsurdFees); +Bitcoin.prototype.sendTransaction = function(tx, options, callback) { + var self = this; + var allowAbsurdFees = false; + if (_.isFunction(options) && _.isUndefined(callback)) { + callback = options; + } else if (_.isObject(options)) { + allowAbsurdFees = options.allowAbsurdFees; + } + + this.client.sendRawTransaction(tx, allowAbsurdFees, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + callback(null, response.result); + }); + }; /** - * Will get a transaction as a Node.js Buffer from disk and the mempool. + * Will get a transaction as a Node.js Buffer. Results include the mempool. * @param {String} txid - The transaction hash - * @param {Boolean} queryMempool - Include the mempool * @param {Function} callback */ -Bitcoin.prototype.getTransaction = function(txid, queryMempool, callback) { - return bindings.getTransaction(txid, queryMempool, callback); +Bitcoin.prototype.getRawTransaction = function(txid, callback) { + var self = this; + var tx = self.rawTransactionCache.get(txid); + if (tx) { + return setImmediate(function() { + callback(null, tx); + }); + } else { + self._tryAll(function(done) { + self.client.getRawTransaction(txid, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + var buffer = new Buffer(response.result, 'hex'); + self.rawTransactionCache.set(txid, buffer); + done(null, buffer); + }); + }, callback); + } }; /** - * Will get a transaction with additional information about the block, in the format: - * { - * blockHash: '2725743288feae6bdaa976590af7cb12d7b535b5a242787de6d2789c73682ed1', - * height: 48, - * timestamp: 1442951110, // in seconds - * buffer: // transaction buffer - * } + * Will get a transaction as a Bitcore Transaction. Results include the mempool. * @param {String} txid - The transaction hash * @param {Boolean} queryMempool - Include the mempool * @param {Function} callback */ -Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { - return bindings.getTransactionWithBlockInfo(txid, queryMempool, callback); +Bitcoin.prototype.getTransaction = function(txid, callback) { + var self = this; + var tx = self.transactionCache.get(txid); + if (tx) { + return setImmediate(function() { + callback(null, tx); + }); + } else { + self._tryAll(function(done) { + self.client.getRawTransaction(txid, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + var tx = Transaction(); + tx.fromString(response.result); + self.transactionCache.set(txid, tx); + done(null, tx); + }); + }, callback); + } }; /** - * Will return the entire mempool as an Array of transaction Buffers. - * @returns {Array} + * Will get a detailed view of a transaction including addresses, amounts and fees. + * + * Example result: + * { + * blockHash: '000000000000000002cd0ba6e8fae058747d2344929ed857a18d3484156c9250', + * height: 411462, + * blockTimestamp: 1463070382, + * version: 1, + * hash: 'de184cc227f6d1dc0316c7484aa68b58186a18f89d853bb2428b02040c394479', + * locktime: 411451, + * coinbase: true, + * inputs: [ + * { + * prevTxId: '3d003413c13eec3fa8ea1fe8bbff6f40718c66facffe2544d7516c9e2900cac2', + * outputIndex: 0, + * sequence: 123456789, + * script: [hexString], + * scriptAsm: [asmString], + * address: '1LCTmj15p7sSXv3jmrPfA6KGs6iuepBiiG', + * satoshis: 771146 + * } + * ], + * outputs: [ + * { + * satoshis: 811146, + * script: '76a914d2955017f4e3d6510c57b427cf45ae29c372c99088ac', + * scriptAsm: 'OP_DUP OP_HASH160 d2955017f4e3d6510c57b427cf45ae29c372c990 OP_EQUALVERIFY OP_CHECKSIG', + * address: '1LCTmj15p7sSXv3jmrPfA6KGs6iuepBiiG', + * spentTxId: '4316b98e7504073acd19308b4b8c9f4eeb5e811455c54c0ebfe276c0b1eb6315', + * spentIndex: 1, + * spentHeight: 100 + * } + * ], + * inputSatoshis: 771146, + * outputSatoshis: 811146, + * feeSatoshis: 40000 + * }; + * + * @param {String} txid - The hex string of the transaction + * @param {Function} callback */ -Bitcoin.prototype.getMempoolTransactions = function() { - return bindings.getMempoolTransactions(); -}; +Bitcoin.prototype.getDetailedTransaction = function(txid, callback) { + var self = this; + var tx = self.transactionDetailedCache.get(txid); -/** - * Will add a transaction to the mempool without any validation. This is used - * exclusively for testing purposes. - * @param {String} transaction - The hex string for the transaction - */ -Bitcoin.prototype.addMempoolUncheckedTransaction = function(transaction) { - return bindings.addMempoolUncheckedTransaction(transaction); + function addInputsToTx(tx, result) { + tx.inputs = []; + tx.inputSatoshis = 0; + for(var inputIndex = 0; inputIndex < result.vin.length; inputIndex++) { + var input = result.vin[inputIndex]; + if (!tx.coinbase) { + tx.inputSatoshis += input.valueSat; + } + var script = null; + var scriptAsm = null; + if (input.scriptSig) { + script = input.scriptSig.hex; + scriptAsm = input.scriptSig.asm; + } else if (input.coinbase) { + script = input.coinbase; + } + tx.inputs.push({ + prevTxId: input.txid || null, + outputIndex: _.isUndefined(input.vout) ? null : input.vout, + script: script, + scriptAsm: scriptAsm || null, + sequence: input.sequence, + address: input.address || null, + satoshis: _.isUndefined(input.valueSat) ? null : input.valueSat + }); + } + } + + function addOutputsToTx(tx, result) { + tx.outputs = []; + tx.outputSatoshis = 0; + for(var outputIndex = 0; outputIndex < result.vout.length; outputIndex++) { + var out = result.vout[outputIndex]; + tx.outputSatoshis += out.valueSat; + var address = null; + if (out.scriptPubKey && out.scriptPubKey.addresses && out.scriptPubKey.addresses.length === 1) { + address = out.scriptPubKey.addresses[0]; + } + tx.outputs.push({ + satoshis: out.valueSat, + script: out.scriptPubKey.hex, + scriptAsm: out.scriptPubKey.asm, + spentTxId: out.spentTxId, + spentIndex: out.spentIndex, + spentHeight: out.spentHeight, + address: address + }); + } + } + + if (tx) { + return setImmediate(function() { + callback(null, tx); + }); + } else { + self._tryAll(function(done) { + self.client.getRawTransaction(txid, 1, function(err, response) { + if (err) { + return done(self._wrapRPCError(err)); + } + var result = response.result; + var tx = { + hex: result.hex, + blockHash: result.blockhash, + height: result.height ? result.height : -1, + blockTimestamp: result.time, + version: result.version, + hash: txid, + locktime: result.locktime, + }; + + if (result.vin[0] && result.vin[0].coinbase) { + tx.coinbase = true; + } + + addInputsToTx(tx, result); + addOutputsToTx(tx, result); + + if (!tx.coinbase) { + tx.feeSatoshis = tx.inputSatoshis - tx.outputSatoshis; + } else { + tx.feeSatoshis = 0; + } + + self.transactionDetailedCache.set(txid, tx); + + done(null, tx); + }); + }, callback); + } }; /** * Will get the best block hash for the chain. - * @returns {String} + * @param {Function} callback */ -Bitcoin.prototype.getBestBlockHash = function() { - return bindings.getBestBlockHash(); +Bitcoin.prototype.getBestBlockHash = function(callback) { + var self = this; + this.client.getBestBlockHash(function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + callback(null, response.result); + }); }; /** - * Will get the next block hash for a block hash. - * @param {String} hash - The starting block hash - * @returns {String} + * Will give the txid and inputIndex that spent an output + * @param {Function} callback */ -Bitcoin.prototype.getNextBlockHash = function(hash) { - return bindings.getNextBlockHash(hash); +Bitcoin.prototype.getSpentInfo = function(options, callback) { + var self = this; + this.client.getSpentInfo(options, function(err, response) { + if (err && err.code === -5) { + return callback(null, {}); + } else if (err) { + return callback(self._wrapRPCError(err)); + } + callback(null, response.result); + }); }; /** * This will return information about the database in the format: * { * version: 110000, - * protocolversion: 70002, + * protocolVersion: 70002, * blocks: 151, - * timeoffset: 0, + * timeOffset: 0, * connections: 0, * difficulty: 4.6565423739069247e-10, * testnet: false, - * relayfee: 1000, + * network: 'testnet' + * relayFee: 1000, * errors: '' * } + * @param {Function} callback */ -Bitcoin.prototype.getInfo = function() { - return bindings.getInfo(); +Bitcoin.prototype.getInfo = function(callback) { + var self = this; + this.client.getInfo(function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + var result = response.result; + var info = { + version: result.version, + protocolVersion: result.protocolversion, + blocks: result.blocks, + timeOffset: result.timeoffset, + connections: result.connections, + proxy: result.proxy, + difficulty: result.difficulty, + testnet: result.testnet, + relayFee: result.relayfee, + errors: result.errors, + network: self.node.getNetworkName() + }; + callback(null, info); + }); +}; + +Bitcoin.prototype.generateBlock = function(num, callback) { + var self = this; + this.client.generate(num, function(err, response) { + if (err) { + return callback(self._wrapRPCError(err)); + } + callback(null, response.result); + }); }; /** @@ -350,14 +2039,30 @@ Bitcoin.prototype.getInfo = function() { * @param {Function} callback */ Bitcoin.prototype.stop = function(callback) { - return bindings.stop(function(err, status) { - if (err) { - return callback(err); - } else { - log.info(status); - return callback(); - } - }); + if (this.spawn && this.spawn.process) { + var exited = false; + this.spawn.process.once('exit', function(code) { + if (!exited) { + exited = true; + if (code !== 0) { + var error = new Error('bitcoind spawned process exited with status code: ' + code); + error.code = code; + return callback(error); + } else { + return callback(); + } + } + }); + this.spawn.process.kill('SIGINT'); + setTimeout(function() { + if (!exited) { + exited = true; + return callback(new Error('bitcoind process did not exit')); + } + }, this.shutdownTimeout).unref(); + } else { + callback(); + } }; module.exports = Bitcoin; diff --git a/lib/services/db.js b/lib/services/db.js deleted file mode 100644 index 679935ca9..000000000 --- a/lib/services/db.js +++ /dev/null @@ -1,812 +0,0 @@ -'use strict'; - -var util = require('util'); -var fs = require('fs'); -var async = require('async'); -var levelup = require('levelup'); -var leveldown = require('leveldown'); -var mkdirp = require('mkdirp'); -var bitcore = require('bitcore-lib'); -var BufferUtil = bitcore.util.buffer; -var Networks = bitcore.Networks; -var Block = bitcore.Block; -var $ = bitcore.util.preconditions; -var index = require('../'); -var errors = index.errors; -var log = index.log; -var Transaction = require('../transaction'); -var Service = require('../service'); - -/** - * This service synchronizes a leveldb database with bitcoin block chain by connecting and - * disconnecting blocks to build new indexes that can be queried. Other services can extend - * the data that is indexed by implementing a `blockHandler` method. - * - * @param {Object} options - * @param {Node} options.node - A reference to the node - * @param {Node} options.store - A levelup backend store - */ -function DB(options) { - /* jshint maxstatements: 20 */ - - if (!(this instanceof DB)) { - return new DB(options); - } - if (!options) { - options = {}; - } - - Service.call(this, options); - - // Used to keep track of the version of the indexes - // to determine during an upgrade if a reindex is required - this.version = 2; - - this.tip = null; - this.genesis = null; - - $.checkState(this.node.network, 'Node is expected to have a "network" property'); - this.network = this.node.network; - - this._setDataPath(); - - this.maxOpenFiles = options.maxOpenFiles || DB.DEFAULT_MAX_OPEN_FILES; - this.maxTransactionLimit = options.maxTransactionLimit || DB.MAX_TRANSACTION_LIMIT; - - this.levelupStore = leveldown; - if (options.store) { - this.levelupStore = options.store; - } - - this.retryInterval = 60000; - - this.subscriptions = { - transaction: [], - block: [] - }; -} - -util.inherits(DB, Service); - -DB.dependencies = ['bitcoind']; - -DB.PREFIXES = { - VERSION: new Buffer('ff', 'hex'), - BLOCKS: new Buffer('01', 'hex'), - TIP: new Buffer('04', 'hex') -}; - -// The maximum number of transactions to query at once -// Used for populating previous inputs -DB.MAX_TRANSACTION_LIMIT = 5; - -// The default maxiumum number of files open for leveldb -DB.DEFAULT_MAX_OPEN_FILES = 200; - -/** - * This function will set `this.dataPath` based on `this.node.network`. - * @private - */ -DB.prototype._setDataPath = function() { - $.checkState(this.node.datadir, 'Node is expected to have a "datadir" property'); - if (this.node.network === Networks.livenet) { - this.dataPath = this.node.datadir + '/bitcore-node.db'; - } else if (this.node.network === Networks.testnet) { - if (this.node.network.regtestEnabled) { - this.dataPath = this.node.datadir + '/regtest/bitcore-node.db'; - } else { - this.dataPath = this.node.datadir + '/testnet3/bitcore-node.db'; - } - } else { - throw new Error('Unknown network: ' + this.network); - } -}; - -DB.prototype._checkVersion = function(callback) { - var self = this; - var options = { - keyEncoding: 'binary', - valueEncoding: 'binary' - }; - self.store.get(DB.PREFIXES.TIP, options, function(err) { - if (err instanceof levelup.errors.NotFoundError) { - // The database is brand new and doesn't have a tip stored - // we can skip version checking - return callback(); - } else if (err) { - return callback(err); - } - self.store.get(DB.PREFIXES.VERSION, options, function(err, buffer) { - var version; - if (err instanceof levelup.errors.NotFoundError) { - // The initial version (1) of the database didn't store the version number - version = 1; - } else if (err) { - return callback(err); - } else { - version = buffer.readUInt32BE(); - } - if (self.version !== version) { - var helpUrl = 'https://github.com/bitpay/bitcore-node/blob/master/docs/services/db.md#how-to-reindex'; - return callback(new Error( - 'The version of the database "' + version + '" does not match the expected version "' + - self.version + '". A recreation of "' + self.dataPath + '" (can take several hours) is ' + - 'required or to switch versions of software to match. Please see ' + helpUrl + - ' for more information.' - )); - } - callback(); - }); - }); -}; - -DB.prototype._setVersion = function(callback) { - var versionBuffer = new Buffer(new Array(4)); - versionBuffer.writeUInt32BE(this.version); - this.store.put(DB.PREFIXES.VERSION, versionBuffer, callback); -}; - -/** - * Called by Node to start the service. - * @param {Function} callback - */ -DB.prototype.start = function(callback) { - - var self = this; - if (!fs.existsSync(this.dataPath)) { - mkdirp.sync(this.dataPath); - } - - this.genesis = Block.fromBuffer(this.node.services.bitcoind.genesisBuffer); - this.store = levelup(this.dataPath, { db: this.levelupStore, maxOpenFiles: this.maxOpenFiles }); - this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this)); - - this.once('ready', function() { - log.info('Bitcoin Database Ready'); - - // Notify that there is a new tip - self.node.services.bitcoind.on('tip', function(height) { - if(!self.node.stopping) { - self.sync(); - } - }); - }); - - async.series([ - function(next) { - self._checkVersion(next); - }, - function(next) { - self._setVersion(next); - } - ], function(err) { - if (err) { - return callback(err); - } - self.loadTip(function(err) { - if (err) { - return callback(err); - } - - self.sync(); - self.emit('ready'); - setImmediate(callback); - }); - }); -}; - -/** - * Called by Node to stop the service - * @param {Function} callback - */ -DB.prototype.stop = function(callback) { - var self = this; - - // Wait until syncing stops and all db operations are completed before closing leveldb - async.whilst(function() { - return self.bitcoindSyncing; - }, function(next) { - setTimeout(next, 10); - }, function() { - self.store.close(callback); - }); -}; - -/** - * Will give information about the database from bitcoin. - * @param {Function} callback - */ -DB.prototype.getInfo = function(callback) { - var self = this; - setImmediate(function() { - var info = self.node.bitcoind.getInfo(); - callback(null, info); - }); -}; - -/** - * Closes the underlying store database - * @param {Function} callback - */ -DB.prototype.close = function(callback) { - this.store.close(callback); -}; - -/** - * This function is responsible for emitting `db/transaction` events. - * @param {Object} txInfo - The data from the bitcoind.on('tx') event - * @param {Buffer} txInfo.buffer - The transaction buffer - * @param {Boolean} txInfo.mempool - If the transaction was accepted in the mempool - * @param {String} txInfo.hash - The hash of the transaction - */ -DB.prototype.transactionHandler = function(txInfo) { - var tx = Transaction().fromBuffer(txInfo.buffer); - for (var i = 0; i < this.subscriptions.transaction.length; i++) { - this.subscriptions.transaction[i].emit('db/transaction', { - rejected: !txInfo.mempool, - tx: tx - }); - } -}; - -/** - * Called by Node to determine the available API methods. - */ -DB.prototype.getAPIMethods = function() { - var methods = [ - ['getBlock', this, this.getBlock, 1], - ['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2], - ['getTransaction', this, this.getTransaction, 2], - ['getTransactionWithBlockInfo', this, this.getTransactionWithBlockInfo, 2], - ['sendTransaction', this, this.sendTransaction, 1], - ['estimateFee', this, this.estimateFee, 1] - ]; - return methods; -}; - -DB.prototype.loadTip = function(callback) { - var self = this; - - var options = { - keyEncoding: 'binary', - valueEncoding: 'binary' - }; - - self.store.get(DB.PREFIXES.TIP, options, function(err, tipData) { - if(err && err instanceof levelup.errors.NotFoundError) { - self.tip = self.genesis; - self.tip.__height = 0; - self.connectBlock(self.genesis, function(err) { - if(err) { - return callback(err); - } - - self.emit('addblock', self.genesis); - callback(); - }); - return; - } else if(err) { - return callback(err); - } - - var hash = tipData.toString('hex'); - - var times = 0; - async.retry({times: 3, interval: self.retryInterval}, function(done) { - self.getBlock(hash, function(err, tip) { - if(err) { - times++; - log.warn('Bitcoind does not have our tip (' + hash + '). Bitcoind may have crashed and needs to catch up.'); - if(times < 3) { - log.warn('Retrying in ' + (self.retryInterval / 1000) + ' seconds.'); - } - return done(err); - } - - done(null, tip); - }); - }, function(err, tip) { - if(err) { - log.warn('Giving up after 3 tries. Please report this bug to https://github.com/bitpay/bitcore-node/issues'); - log.warn('Please reindex your database.'); - return callback(err); - } - - self.tip = tip; - var blockIndex = self.node.services.bitcoind.getBlockIndex(self.tip.hash); - if(!blockIndex) { - return callback(new Error('Could not get height for tip.')); - } - self.tip.__height = blockIndex.height; - callback(); - }); - }); -}; - -/** - * Will get a block from bitcoind and give a Bitcore Block - * @param {String|Number} hash - A block hash or block height - */ -DB.prototype.getBlock = function(hash, callback) { - this.node.services.bitcoind.getBlock(hash, function(err, blockBuffer) { - if (err) { - return callback(err); - } - callback(null, Block.fromBuffer(blockBuffer)); - }); -}; - -/** - * Get block hashes between two timestamps - * @param {Number} high - high timestamp, in seconds, inclusive - * @param {Number} low - low timestamp, in seconds, inclusive - * @param {Function} callback - */ -DB.prototype.getBlockHashesByTimestamp = function(high, low, callback) { - var self = this; - var hashes = []; - var lowKey; - var highKey; - - try { - lowKey = this._encodeBlockIndexKey(low); - highKey = this._encodeBlockIndexKey(high); - } catch(e) { - return callback(e); - } - - var stream = this.store.createReadStream({ - gte: lowKey, - lte: highKey, - reverse: true, - valueEncoding: 'binary', - keyEncoding: 'binary' - }); - - stream.on('data', function(data) { - hashes.push(self._decodeBlockIndexValue(data.value)); - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('close', function() { - if (error) { - return callback(error); - } - callback(null, hashes); - }); - - return stream; -}; - -/** - * Will give a Bitcore Transaction from bitcoind by txid - * @param {String} txid - A transaction hash - * @param {Boolean} queryMempool - Include the mempool - * @param {Function} callback - */ -DB.prototype.getTransaction = function(txid, queryMempool, callback) { - this.node.services.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) { - if (err) { - return callback(err); - } - if (!txBuffer) { - return callback(new errors.Transaction.NotFound()); - } - - callback(null, Transaction().fromBuffer(txBuffer)); - }); -}; - -/** - * Will give a Bitcore Transaction and populated information about the block included. - * @param {String} txid - A transaction hash - * @param {Boolean} queryMempool - Include the mempool - * @param {Function} callback - */ -DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { - this.node.services.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { - if (err) { - return callback(err); - } - - var tx = Transaction().fromBuffer(obj.buffer); - tx.__blockHash = obj.blockHash; - tx.__height = obj.height; - tx.__timestamp = obj.timestamp; - - callback(null, tx); - }); -}; - -/** - * Will send a transaction to the Bitcoin network. - * @param {Transaction} tx - An instance of a Bitcore Transaction - * @param {Function} callback - */ -DB.prototype.sendTransaction = function(tx, callback) { - var txString; - if (tx instanceof Transaction) { - txString = tx.serialize(); - } else { - txString = tx; - } - - try { - var txid = this.node.services.bitcoind.sendTransaction(txString); - return callback(null, txid); - } catch(err) { - return callback(err); - } -}; - -/** - * Will estimate fees for a transaction and give a result in - * satoshis per kilobyte. Similar to the bitcoind estimateFee method. - * @param {Number} blocks - The number of blocks for the transaction to be included. - * @param {Function} callback - */ -DB.prototype.estimateFee = function(blocks, callback) { - var self = this; - setImmediate(function() { - callback(null, self.node.services.bitcoind.estimateFee(blocks)); - }); -}; - -/** - * Called by the Bus to determine the available events. - */ -DB.prototype.getPublishEvents = function() { - return [ - { - name: 'db/transaction', - scope: this, - subscribe: this.subscribe.bind(this, 'transaction'), - unsubscribe: this.unsubscribe.bind(this, 'transaction') - }, - { - name: 'db/block', - scope: this, - subscribe: this.subscribe.bind(this, 'block'), - unsubscribe: this.unsubscribe.bind(this, 'block') - } - ]; -}; - -DB.prototype.subscribe = function(name, emitter) { - this.subscriptions[name].push(emitter); -}; - -DB.prototype.unsubscribe = function(name, emitter) { - var index = this.subscriptions[name].indexOf(emitter); - if (index > -1) { - this.subscriptions[name].splice(index, 1); - } -}; - -/** - * Will give the previous hash for a block. - * @param {String} blockHash - * @param {Function} callback - */ -DB.prototype.getPrevHash = function(blockHash, callback) { - var blockIndex = this.node.services.bitcoind.getBlockIndex(blockHash); - setImmediate(function() { - if (blockIndex) { - callback(null, blockIndex.prevHash); - } else { - callback(new Error('Could not get prevHash, block not found')); - } - }); -}; - -/** - * Connects a block to the database and add indexes - * @param {Block} block - The bitcore block - * @param {Function} callback - */ -DB.prototype.connectBlock = function(block, callback) { - log.debug('DB handling new chain block'); - this.runAllBlockHandlers(block, true, callback); -}; - -/** - * Disconnects a block from the database and removes indexes - * @param {Block} block - The bitcore block - * @param {Function} callback - */ -DB.prototype.disconnectBlock = function(block, callback) { - log.debug('DB removing chain block'); - this.runAllBlockHandlers(block, false, callback); -}; - -/** - * Will collect all database operations for a block from other services that implement - * `blockHandler` methods and then save operations to the database. - * @param {Block} block - The bitcore block - * @param {Boolean} add - If the block is being added/connected or removed/disconnected - * @param {Function} callback - */ -DB.prototype.runAllBlockHandlers = function(block, add, callback) { - var self = this; - var operations = []; - - // Notify block subscribers - for (var i = 0; i < this.subscriptions.block.length; i++) { - this.subscriptions.block[i].emit('db/block', block.hash); - } - - // Update tip - var tipHash = add ? new Buffer(block.hash, 'hex') : BufferUtil.reverse(block.header.prevHash); - operations.push({ - type: 'put', - key: DB.PREFIXES.TIP, - value: tipHash - }); - - // Update block index - operations.push({ - type: add ? 'put' : 'del', - key: this._encodeBlockIndexKey(block.header.timestamp), - value: this._encodeBlockIndexValue(block.hash) - }); - - async.eachSeries( - this.node.services, - function(mod, next) { - if(mod.blockHandler) { - $.checkArgument(typeof mod.blockHandler === 'function', 'blockHandler must be a function'); - - mod.blockHandler.call(mod, block, add, function(err, ops) { - if (err) { - return next(err); - } - if (ops) { - $.checkArgument(Array.isArray(ops), 'blockHandler for ' + mod.name + ' returned non-array'); - operations = operations.concat(ops); - } - next(); - }); - } else { - setImmediate(next); - } - }, - function(err) { - if (err) { - return callback(err); - } - - log.debug('Updating the database with operations', operations); - self.store.batch(operations, callback); - } - ); -}; - -DB.prototype._encodeBlockIndexKey = function(timestamp) { - $.checkArgument(timestamp >= 0 && timestamp <= 4294967295, 'timestamp out of bounds'); - var timestampBuffer = new Buffer(4); - timestampBuffer.writeUInt32BE(timestamp); - return Buffer.concat([DB.PREFIXES.BLOCKS, timestampBuffer]); -}; - -DB.prototype._encodeBlockIndexValue = function(hash) { - return new Buffer(hash, 'hex'); -}; - -DB.prototype._decodeBlockIndexValue = function(value) { - return value.toString('hex'); -}; - -/** - * This function will find the common ancestor between the current chain and a forked block, - * by moving backwards on both chains until there is a meeting point. - * @param {Block} block - The new tip that forks the current chain. - * @param {Function} done - A callback function that is called when complete. - */ -DB.prototype.findCommonAncestor = function(block, done) { - - var self = this; - - var mainPosition = self.tip.hash; - var forkPosition = block.hash; - - var mainHashesMap = {}; - var forkHashesMap = {}; - - mainHashesMap[mainPosition] = true; - forkHashesMap[forkPosition] = true; - - var commonAncestor = null; - - async.whilst( - function() { - return !commonAncestor; - }, - function(next) { - - if(mainPosition) { - var mainBlockIndex = self.node.services.bitcoind.getBlockIndex(mainPosition); - if(mainBlockIndex && mainBlockIndex.prevHash) { - mainHashesMap[mainBlockIndex.prevHash] = true; - mainPosition = mainBlockIndex.prevHash; - } else { - mainPosition = null; - } - } - - if(forkPosition) { - var forkBlockIndex = self.node.services.bitcoind.getBlockIndex(forkPosition); - if(forkBlockIndex && forkBlockIndex.prevHash) { - forkHashesMap[forkBlockIndex.prevHash] = true; - forkPosition = forkBlockIndex.prevHash; - } else { - forkPosition = null; - } - } - - if(forkPosition && mainHashesMap[forkPosition]) { - commonAncestor = forkPosition; - } - - if(mainPosition && forkHashesMap[mainPosition]) { - commonAncestor = mainPosition; - } - - if(!mainPosition && !forkPosition) { - return next(new Error('Unknown common ancestor')); - } - - setImmediate(next); - }, - function(err) { - done(err, commonAncestor); - } - ); -}; - -/** - * This function will attempt to rewind the chain to the common ancestor - * between the current chain and a forked block. - * @param {Block} block - The new tip that forks the current chain. - * @param {Function} done - A callback function that is called when complete. - */ -DB.prototype.syncRewind = function(block, done) { - - var self = this; - - self.findCommonAncestor(block, function(err, ancestorHash) { - if (err) { - return done(err); - } - log.warn('Reorg common ancestor found:', ancestorHash); - // Rewind the chain to the common ancestor - async.whilst( - function() { - // Wait until the tip equals the ancestor hash - return self.tip.hash !== ancestorHash; - }, - function(removeDone) { - - var tip = self.tip; - - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(tip.header.prevHash).toString('hex'); - - self.getBlock(prevHash, function(err, previousTip) { - if (err) { - removeDone(err); - } - - // Undo the related indexes for this block - self.disconnectBlock(tip, function(err) { - if (err) { - return removeDone(err); - } - - // Set the new tip - previousTip.__height = self.tip.__height - 1; - self.tip = previousTip; - self.emit('removeblock', tip); - removeDone(); - }); - - }); - - }, done - ); - }); -}; - -/** - * This function will synchronize additional indexes for the chain based on - * the current active chain in the bitcoin daemon. In the event that there is - * a reorganization in the daemon, the chain will rewind to the last common - * ancestor and then resume syncing. - */ -DB.prototype.sync = function() { - var self = this; - - if (self.bitcoindSyncing || self.node.stopping || !self.tip) { - return; - } - - self.bitcoindSyncing = true; - - var height; - - async.whilst(function() { - height = self.tip.__height; - return height < self.node.services.bitcoind.height && !self.node.stopping; - }, function(done) { - self.node.services.bitcoind.getBlock(height + 1, function(err, blockBuffer) { - if (err) { - return done(err); - } - - var block = Block.fromBuffer(blockBuffer); - - // TODO: expose prevHash as a string from bitcore - var prevHash = BufferUtil.reverse(block.header.prevHash).toString('hex'); - - if (prevHash === self.tip.hash) { - - // This block appends to the current chain tip and we can - // immediately add it to the chain and create indexes. - - // Populate height - block.__height = self.tip.__height + 1; - - // Create indexes - self.connectBlock(block, function(err) { - if (err) { - return done(err); - } - self.tip = block; - log.debug('Chain added block to main chain'); - self.emit('addblock', block); - setImmediate(done); - }); - } else { - // This block doesn't progress the current tip, so we'll attempt - // to rewind the chain to the common ancestor of the block and - // then we can resume syncing. - log.warn('Beginning reorg! Current tip: ' + self.tip.hash + '; New tip: ' + block.hash); - self.syncRewind(block, function(err) { - if(err) { - return done(err); - } - - log.warn('Reorg complete. New tip is ' + self.tip.hash); - done(); - }); - } - }); - }, function(err) { - if (err) { - Error.captureStackTrace(err); - return self.node.emit('error', err); - } - - if(self.node.stopping) { - self.bitcoindSyncing = false; - return; - } - - if (self.node.services.bitcoind.isSynced()) { - self.bitcoindSyncing = false; - self.node.emit('synced'); - } else { - self.bitcoindSyncing = false; - } - - }); - -}; - -module.exports = DB; diff --git a/lib/services/web.js b/lib/services/web.js index af773b877..b050b5d5b 100644 --- a/lib/services/web.js +++ b/lib/services/web.js @@ -1,15 +1,19 @@ 'use strict'; +var fs = require('fs'); var http = require('http'); var https = require('https'); var express = require('express'); var bodyParser = require('body-parser'); var socketio = require('socket.io'); -var BaseService = require('../service'); var inherits = require('util').inherits; + +var BaseService = require('../service'); +var bitcore = require('bitcore-lib'); +var _ = bitcore.deps._; var index = require('../'); var log = index.log; -var fs = require('fs'); + /** * This service represents a hub for combining several services over a single HTTP port. Services @@ -23,6 +27,7 @@ var fs = require('fs'); * @param {Object} options.httpsOptions - Options passed into https.createServer, defaults to node settings. * @param {String} options.httpsOptions.key - Path to key file * @param {String} options.httpsOptions.cert - Path to cert file + * @param {Boolean} options.enableSocketRPC - Option to enable/disable websocket RPC handling * @param {Number} options.port - The port for the service, defaults to node settings. */ var WebService = function(options) { @@ -32,6 +37,9 @@ var WebService = function(options) { this.httpsOptions = options.httpsOptions || this.node.httpsOptions; this.port = options.port || this.node.port || 3456; + this.enableSocketRPC = _.isUndefined(options.enableSocketRPC) ? + WebService.DEFAULT_SOCKET_RPC : options.enableSocketRPC; + this.node.on('ready', function() { self.eventNames = self.getEventNames(); self.setupAllRoutes(); @@ -43,6 +51,7 @@ var WebService = function(options) { inherits(WebService, BaseService); WebService.dependencies = []; +WebService.DEFAULT_SOCKET_RPC = true; /** * Called by Node to start the service @@ -149,21 +158,31 @@ WebService.prototype.getEventNames = function() { return eventNames; }; +WebService.prototype._getRemoteAddress = function(socket) { + return socket.client.request.headers['cf-connecting-ip'] || socket.conn.remoteAddress; +}; + /** * This function is responsible for managing a socket.io connection, including * instantiating a new Bus, subscribing/unsubscribing and handling RPC commands. * @param {Socket} socket - A socket.io socket instance */ WebService.prototype.socketHandler = function(socket) { - var bus = this.node.openBus(); + var self = this; + var remoteAddress = self._getRemoteAddress(socket); + var bus = this.node.openBus({remoteAddress: remoteAddress}); - socket.on('message', this.socketMessageHandler.bind(this)); + if (this.enableSocketRPC) { + socket.on('message', this.socketMessageHandler.bind(this)); + } socket.on('subscribe', function(name, params) { + log.info(remoteAddress, 'web socket subscribe:', name); bus.subscribe(name, params); }); socket.on('unsubscribe', function(name, params) { + log.info(remoteAddress, 'web socket unsubscribe:', name); bus.unsubscribe(name, params); }); @@ -183,6 +202,7 @@ WebService.prototype.socketHandler = function(socket) { }); socket.on('disconnect', function() { + log.info(remoteAddress, 'web socket disconnect'); bus.close(); }); }; diff --git a/lib/transaction.js b/lib/transaction.js deleted file mode 100644 index f55df37bf..000000000 --- a/lib/transaction.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -var async = require('async'); -var levelup = require('levelup'); -var bitcore = require('bitcore-lib'); -var Transaction = bitcore.Transaction; - -var MAX_TRANSACTION_LIMIT = 5; - -Transaction.prototype.populateInputs = function(db, poolTransactions, callback) { - var self = this; - - if(this.isCoinbase()) { - return setImmediate(callback); - } - - async.eachLimit( - this.inputs, - db.maxTransactionLimit || MAX_TRANSACTION_LIMIT, - function(input, next) { - self._populateInput(db, input, poolTransactions, next); - }, - callback - ); -}; - -Transaction.prototype._populateInput = function(db, input, poolTransactions, callback) { - if (!input.prevTxId || !Buffer.isBuffer(input.prevTxId)) { - return callback(new Error('Input is expected to have prevTxId as a buffer')); - } - var txid = input.prevTxId.toString('hex'); - db.getTransaction(txid, true, function(err, prevTx) { - if(err instanceof levelup.errors.NotFoundError) { - // Check the pool for transaction - for(var i = 0; i < poolTransactions.length; i++) { - if(txid === poolTransactions[i].hash) { - input.output = poolTransactions[i].outputs[input.outputIndex]; - return callback(); - } - } - - return callback(new Error('Previous tx ' + input.prevTxId.toString('hex') + ' not found')); - } else if(err) { - callback(err); - } else { - input.output = prevTx.outputs[input.outputIndex]; - callback(); - } - }); -}; - -Transaction.prototype._checkSpent = function(db, input, poolTransactions, callback) { - // TODO check and see if another transaction in the pool spent the output - db.isSpentDB(input, function(spent) { - if(spent) { - return callback(new Error('Input already spent')); - } else { - callback(); - } - }); -}; - -module.exports = Transaction; diff --git a/lib/utils.js b/lib/utils.js index 72cc58c12..cae2a5fe9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -21,4 +21,9 @@ utils.startAtZero = function startAtZero(obj, key) { } }; +utils.isAbsolutePath = require('path').isAbsolute; +if (!utils.isAbsolutePath) { + utils.isAbsolutePath = require('path-is-absolute'); +} + module.exports = utils; diff --git a/package.json b/package.json index 8b31e7934..5258a2305 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "description": "Full node with extended capabilities using Bitcore and Bitcoin Core", "author": "BitPay ", "version": "2.1.1-dev", - "lastBuild": "2.1.1", "main": "./index.js", "repository": "git://github.com/bitpay/bitcore-node.git", "homepage": "https://github.com/bitpay/bitcore-node", @@ -28,18 +27,17 @@ } ], "bin": { - "bitcore-node": "./bin/bitcore-node" + "bitcore-node": "./bin/bitcore-node", + "bitcoind": "./bin/bitcoind" }, "scripts": { - "install": "./bin/install", - "build": "./bin/build", - "clean": "./bin/clean", - "package": "node bin/package.js", - "upload": "node bin/upload.js", - "start": "node bin/start.js", - "test": "NODE_ENV=test mocha -R spec --recursive", - "coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive", - "libbitcoind": "node bin/start-libbitcoind.js" + "preinstall": "./scripts/download", + "verify": "./scripts/download --skip-bitcoin-download --verify-bitcoin-download", + "test": "mocha -R spec --recursive", + "regtest": "./scripts/regtest", + "jshint": "jshint --reporter=node_modules/jshint-stylish ./lib", + "coverage": "istanbul cover _mocha -- --recursive", + "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" }, "tags": [ "bitcoin", @@ -47,46 +45,39 @@ ], "dependencies": { "async": "^1.3.0", - "bindings": "^1.2.1", + "bitcoind-rpc": "^0.6.0", "bitcore-lib": "^0.13.13", "body-parser": "^1.13.3", "colors": "^1.1.2", "commander": "^2.8.1", "errno": "^0.1.4", "express": "^4.13.3", - "leveldown": "bitpay/leveldown#bitpay-1.4.4", - "levelup": "^1.3.1", "liftoff": "^2.2.0", - "memdown": "^1.0.0", + "lru-cache": "^4.0.1", "mkdirp": "0.5.0", - "nan": "^2.0.9", "npm": "^2.14.1", + "path-is-absolute": "^1.0.0", "semver": "^5.0.1", - "socket.io": "bitpay/socket.io#bitpay-1.3.7", - "socket.io-client": "bitpay/socket.io-client#bitpay-1.3.7" + "socket.io": "^1.4.5", + "socket.io-client": "^1.4.5", + "zmq": "^2.14.0" + }, + "optionalDependencies": { + "bufferutil": "~1.2.1", + "utf-8-validate": "~1.2.1" }, "devDependencies": { - "aws-sdk": "~2.0.0-rc.15", "benchmark": "1.0.0", - "bitcoin": "^2.3.2", - "bitcoind-rpc": "^0.3.0", - "chai": "^3.0.0", - "mocha": "~1.16.2", + "bitcore-p2p": "^1.1.0", + "chai": "^3.5.0", + "coveralls": "^2.11.9", + "istanbul": "^0.4.3", + "jshint": "^2.9.2", + "jshint-stylish": "^2.1.0", + "mocha": "^2.4.5", "proxyquire": "^1.3.1", "rimraf": "^2.4.2", - "sinon": "^1.15.4", - "bitcore-p2p": "~1.0.0" - }, - "engines": { - "node": "^0.12 || ^4.2" + "sinon": "^1.15.4" }, - "os": [ - "darwin", - "linux" - ], - "cpu": [ - "x64", - "arm" - ], "license": "MIT" } diff --git a/regtest/bitcoind.js b/regtest/bitcoind.js new file mode 100644 index 000000000..13346416c --- /dev/null +++ b/regtest/bitcoind.js @@ -0,0 +1,485 @@ +'use strict'; + +// To run the tests: $ mocha -R spec regtest/bitcoind.js + +var path = require('path'); +var index = require('..'); +var log = index.log; + +var chai = require('chai'); +var bitcore = require('bitcore-lib'); +var BN = bitcore.crypto.BN; +var async = require('async'); +var rimraf = require('rimraf'); +var bitcoind; + +/* jshint unused: false */ +var should = chai.should(); +var assert = chai.assert; +var sinon = require('sinon'); +var BitcoinRPC = require('bitcoind-rpc'); +var transactionData = []; +var blockHashes = []; +var utxos; +var client; +var coinbasePrivateKey; +var privateKey = bitcore.PrivateKey(); +var destKey = bitcore.PrivateKey(); + +describe('Bitcoind Functionality', function() { + + before(function(done) { + this.timeout(60000); + + // Add the regtest network + bitcore.Networks.enableRegtest(); + var regtestNetwork = bitcore.Networks.get('regtest'); + + var datadir = __dirname + '/data'; + + rimraf(datadir + '/regtest', function(err) { + + if (err) { + throw err; + } + + bitcoind = require('../').services.Bitcoin({ + spawn: { + datadir: datadir, + exec: path.resolve(__dirname, '../bin/bitcoind') + }, + node: { + network: regtestNetwork, + getNetworkName: function() { + return 'regtest'; + } + } + }); + + bitcoind.on('error', function(err) { + log.error('error="%s"', err.message); + }); + + log.info('Waiting for Bitcoin Core to initialize...'); + + bitcoind.start(function() { + log.info('Bitcoind started'); + + client = new BitcoinRPC({ + protocol: 'http', + host: '127.0.0.1', + port: 30331, + user: 'bitcoin', + pass: 'local321', + rejectUnauthorized: false + }); + + log.info('Generating 100 blocks...'); + + // Generate enough blocks so that the initial coinbase transactions + // can be spent. + + setImmediate(function() { + client.generate(150, function(err, response) { + if (err) { + throw err; + } + blockHashes = response.result; + + log.info('Preparing test data...'); + + // Get all of the unspent outputs + client.listUnspent(0, 150, function(err, response) { + utxos = response.result; + + async.mapSeries(utxos, function(utxo, next) { + async.series([ + function(finished) { + // Load all of the transactions for later testing + client.getTransaction(utxo.txid, function(err, txresponse) { + if (err) { + throw err; + } + // add to the list of transactions for testing later + transactionData.push(txresponse.result.hex); + finished(); + }); + }, + function(finished) { + // Get the private key for each utxo + client.dumpPrivKey(utxo.address, function(err, privresponse) { + if (err) { + throw err; + } + utxo.privateKeyWIF = privresponse.result; + finished(); + }); + } + ], next); + }, function(err) { + if (err) { + throw err; + } + done(); + }); + }); + }); + }); + }); + }); + }); + + after(function(done) { + this.timeout(60000); + bitcoind.node.stopping = true; + bitcoind.stop(function(err, result) { + done(); + }); + }); + + describe('get blocks by hash', function() { + + [0,1,2,3,5,6,7,8,9].forEach(function(i) { + it('generated block ' + i, function(done) { + bitcoind.getBlock(blockHashes[i], function(err, block) { + if (err) { + throw err; + } + should.exist(block); + block.hash.should.equal(blockHashes[i]); + done(); + }); + }); + }); + }); + + describe('get blocks as buffers', function() { + [0,1,2,3,5,6,7,8,9].forEach(function(i) { + it('generated block ' + i, function(done) { + bitcoind.getRawBlock(blockHashes[i], function(err, block) { + if (err) { + throw err; + } + should.exist(block); + (block instanceof Buffer).should.equal(true); + done(); + }); + }); + }); + }); + + describe('get errors as error instances', function() { + it('will wrap an rpc into a javascript error', function(done) { + bitcoind.client.getBlock(1000000000, function(err, response) { + var error = bitcoind._wrapRPCError(err); + (error instanceof Error).should.equal(true); + error.message.should.equal(err.message); + error.code.should.equal(err.code); + should.exist(error.stack); + done(); + }); + }); + }); + + describe('get blocks by height', function() { + + [0,1,2,3,4,5,6,7,8,9].forEach(function(i) { + it('generated block ' + i, function(done) { + // add the genesis block + var height = i + 1; + bitcoind.getBlock(i + 1, function(err, block) { + if (err) { + throw err; + } + should.exist(block); + block.hash.should.equal(blockHashes[i]); + done(); + }); + }); + }); + + it('will get error with number greater than tip', function(done) { + bitcoind.getBlock(1000000000, function(err, response) { + should.exist(err); + err.code.should.equal(-8); + done(); + }); + }); + + }); + + describe('get transactions by hash', function() { + [0,1,2,3,4,5,6,7,8,9].forEach(function(i) { + it('for tx ' + i, function(done) { + var txhex = transactionData[i]; + var tx = new bitcore.Transaction(); + tx.fromString(txhex); + bitcoind.getTransaction(tx.hash, function(err, response) { + if (err) { + throw err; + } + assert(response.toString('hex') === txhex, 'incorrect tx data result'); + done(); + }); + }); + }); + + it('will return error if the transaction does not exist', function(done) { + var txid = '6226c407d0e9705bdd7158e60983e37d0f5d23529086d6672b07d9238d5aa618'; + bitcoind.getTransaction(txid, function(err, response) { + should.exist(err); + done(); + }); + }); + }); + + describe('get transactions as buffers', function() { + [0,1,2,3,4,5,6,7,8,9].forEach(function(i) { + it('for tx ' + i, function(done) { + var txhex = transactionData[i]; + var tx = new bitcore.Transaction(); + tx.fromString(txhex); + bitcoind.getRawTransaction(tx.hash, function(err, response) { + if (err) { + throw err; + } + response.should.be.instanceOf(Buffer); + assert(response.toString('hex') === txhex, 'incorrect tx data result'); + done(); + }); + }); + }); + + it('will return error if the transaction does not exist', function(done) { + var txid = '6226c407d0e9705bdd7158e60983e37d0f5d23529086d6672b07d9238d5aa618'; + bitcoind.getRawTransaction(txid, function(err, response) { + should.exist(err); + done(); + }); + }); + }); + + describe('get block header', function() { + var expectedWork = new BN(6); + [1,2,3,4,5,6,7,8,9].forEach(function(i) { + it('generate block ' + i, function(done) { + bitcoind.getBlockHeader(blockHashes[i], function(err, blockIndex) { + if (err) { + return done(err); + } + should.exist(blockIndex); + should.exist(blockIndex.chainWork); + var work = new BN(blockIndex.chainWork, 'hex'); + work.toString(16).should.equal(expectedWork.toString(16)); + expectedWork = expectedWork.add(new BN(2)); + should.exist(blockIndex.prevHash); + blockIndex.hash.should.equal(blockHashes[i]); + blockIndex.prevHash.should.equal(blockHashes[i - 1]); + blockIndex.height.should.equal(i + 1); + done(); + }); + }); + }); + it('will get null prevHash for the genesis block', function(done) { + bitcoind.getBlockHeader(0, function(err, header) { + if (err) { + return done(err); + } + should.exist(header); + should.equal(header.prevHash, undefined); + done(); + }); + }); + it('will get error for block not found', function(done) { + bitcoind.getBlockHeader('notahash', function(err, header) { + should.exist(err); + done(); + }); + }); + }); + + describe('get block index by height', function() { + var expectedWork = new BN(6); + [2,3,4,5,6,7,8,9].forEach(function(i) { + it('generate block ' + i, function() { + bitcoind.getBlockHeader(i, function(err, header) { + should.exist(header); + should.exist(header.chainWork); + var work = new BN(header.chainWork, 'hex'); + work.toString(16).should.equal(expectedWork.toString(16)); + expectedWork = expectedWork.add(new BN(2)); + should.exist(header.prevHash); + header.hash.should.equal(blockHashes[i - 1]); + header.prevHash.should.equal(blockHashes[i - 2]); + header.height.should.equal(i); + }); + }); + }); + it('will get error with number greater than tip', function(done) { + bitcoind.getBlockHeader(100000, function(err, header) { + should.exist(err); + done(); + }); + }); + }); + + describe('send transaction functionality', function() { + + it('will not error and return the transaction hash', function(done) { + + // create and sign the transaction + var tx = bitcore.Transaction(); + tx.from(utxos[0]); + tx.change(privateKey.toAddress()); + tx.to(destKey.toAddress(), utxos[0].amount * 1e8 - 1000); + tx.sign(bitcore.PrivateKey.fromWIF(utxos[0].privateKeyWIF)); + + // test sending the transaction + bitcoind.sendTransaction(tx.serialize(), function(err, hash) { + if (err) { + return done(err); + } + hash.should.equal(tx.hash); + done(); + }); + + }); + + it('will throw an error if an unsigned transaction is sent', function(done) { + var tx = bitcore.Transaction(); + tx.from(utxos[1]); + tx.change(privateKey.toAddress()); + tx.to(destKey.toAddress(), utxos[1].amount * 1e8 - 1000); + bitcoind.sendTransaction(tx.uncheckedSerialize(), function(err, hash) { + should.exist(err); + (err instanceof Error).should.equal(true); + should.not.exist(hash); + done(); + }); + }); + + it('will throw an error for unexpected types (tx decode failed)', function(done) { + var garbage = new Buffer('abcdef', 'hex'); + bitcoind.sendTransaction(garbage, function(err, hash) { + should.exist(err); + should.not.exist(hash); + var num = 23; + bitcoind.sendTransaction(num, function(err, hash) { + should.exist(err); + (err instanceof Error).should.equal(true); + should.not.exist(hash); + done(); + }); + }); + }); + + it('will emit "tx" events', function(done) { + var tx = bitcore.Transaction(); + tx.from(utxos[2]); + tx.change(privateKey.toAddress()); + tx.to(destKey.toAddress(), utxos[2].amount * 1e8 - 1000); + tx.sign(bitcore.PrivateKey.fromWIF(utxos[2].privateKeyWIF)); + + var serialized = tx.serialize(); + + bitcoind.once('tx', function(buffer) { + buffer.toString('hex').should.equal(serialized); + done(); + }); + bitcoind.sendTransaction(serialized, function(err, hash) { + if (err) { + return done(err); + } + should.exist(hash); + }); + }); + + }); + + describe('fee estimation', function() { + it('will estimate fees', function(done) { + bitcoind.estimateFee(1, function(err, fees) { + if (err) { + return done(err); + } + fees.should.equal(-1); + done(); + }); + }); + }); + + describe('tip updates', function() { + it('will get an event when the tip is new', function(done) { + this.timeout(4000); + bitcoind.on('tip', function(height) { + if (height === 151) { + done(); + } + }); + client.generate(1, function(err, response) { + if (err) { + throw err; + } + }); + }); + }); + + describe('get detailed transaction', function() { + it('should include details for coinbase tx', function(done) { + bitcoind.getDetailedTransaction(utxos[0].txid, function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx.height); + tx.height.should.be.a('number'); + should.exist(tx.blockTimestamp); + should.exist(tx.blockHash); + tx.coinbase.should.equal(true); + tx.version.should.equal(1); + tx.hex.should.be.a('string'); + tx.locktime.should.equal(0); + tx.feeSatoshis.should.equal(0); + tx.outputSatoshis.should.equal(50 * 1e8); + tx.inputSatoshis.should.equal(0); + tx.inputs.length.should.equal(1); + tx.outputs.length.should.equal(1); + should.equal(tx.inputs[0].prevTxId, null); + should.equal(tx.inputs[0].outputIndex, null); + tx.inputs[0].script.should.be.a('string'); + should.equal(tx.inputs[0].scriptAsm, null); + should.equal(tx.inputs[0].address, null); + should.equal(tx.inputs[0].satoshis, null); + tx.outputs[0].satoshis.should.equal(50 * 1e8); + tx.outputs[0].script.should.be.a('string'); + tx.outputs[0].scriptAsm.should.be.a('string'); + tx.outputs[0].spentTxId.should.be.a('string'); + tx.outputs[0].spentIndex.should.equal(0); + tx.outputs[0].spentHeight.should.be.a('number'); + tx.outputs[0].address.should.be.a('string'); + done(); + }); + }); + }); + + describe('#getInfo', function() { + it('will get information', function(done) { + bitcoind.getInfo(function(err, info) { + if (err) { + return done(err); + } + info.network.should.equal('regtest'); + should.exist(info); + should.exist(info.version); + should.exist(info.blocks); + should.exist(info.timeOffset); + should.exist(info.connections); + should.exist(info.difficulty); + should.exist(info.testnet); + should.exist(info.relayFee); + should.exist(info.errors); + done(); + }); + }); + }); + +}); diff --git a/regtest/cluster.js b/regtest/cluster.js new file mode 100644 index 000000000..c8ab87549 --- /dev/null +++ b/regtest/cluster.js @@ -0,0 +1,183 @@ +'use strict'; + +var path = require('path'); +var async = require('async'); +var spawn = require('child_process').spawn; + +var BitcoinRPC = require('bitcoind-rpc'); +var rimraf = require('rimraf'); +var bitcore = require('bitcore-lib'); +var chai = require('chai'); +var should = chai.should(); + +var index = require('..'); +var log = index.log; +log.debug = function() {}; +var BitcoreNode = index.Node; +var BitcoinService = index.services.Bitcoin; + +describe('Bitcoin Cluster', function() { + var node; + var daemons = []; + var execPath = path.resolve(__dirname, '../bin/bitcoind'); + var nodesConf = [ + { + datadir: path.resolve(__dirname, './data/node1'), + conf: path.resolve(__dirname, './data/node1/bitcoin.conf'), + rpcuser: 'bitcoin', + rpcpassword: 'local321', + rpcport: 30521, + zmqpubrawtx: 'tcp://127.0.0.1:30611', + zmqpubhashblock: 'tcp://127.0.0.1:30611' + }, + { + datadir: path.resolve(__dirname, './data/node2'), + conf: path.resolve(__dirname, './data/node2/bitcoin.conf'), + rpcuser: 'bitcoin', + rpcpassword: 'local321', + rpcport: 30522, + zmqpubrawtx: 'tcp://127.0.0.1:30622', + zmqpubhashblock: 'tcp://127.0.0.1:30622' + }, + { + datadir: path.resolve(__dirname, './data/node3'), + conf: path.resolve(__dirname, './data/node3/bitcoin.conf'), + rpcuser: 'bitcoin', + rpcpassword: 'local321', + rpcport: 30523, + zmqpubrawtx: 'tcp://127.0.0.1:30633', + zmqpubhashblock: 'tcp://127.0.0.1:30633' + } + ]; + + before(function(done) { + log.info('Starting 3 bitcoind daemons'); + this.timeout(60000); + async.each(nodesConf, function(nodeConf, next) { + var opts = [ + '--regtest', + '--datadir=' + nodeConf.datadir, + '--conf=' + nodeConf.conf + ]; + + rimraf(path.resolve(nodeConf.datadir, './regtest'), function(err) { + if (err) { + return done(err); + } + + var process = spawn(execPath, opts, {stdio: 'inherit'}); + + var client = new BitcoinRPC({ + protocol: 'http', + host: '127.0.0.1', + port: nodeConf.rpcport, + user: nodeConf.rpcuser, + pass: nodeConf.rpcpassword + }); + + daemons.push(process); + + async.retry({times: 10, interval: 5000}, function(ready) { + client.getInfo(ready); + }, next); + + }); + + }, done); + }); + + after(function(done) { + this.timeout(10000); + setTimeout(function() { + async.each(daemons, function(process, next) { + process.once('exit', next); + process.kill('SIGINT'); + }, done); + }, 1000); + }); + + it('step 1: will connect to three bitcoind daemons', function(done) { + this.timeout(20000); + var configuration = { + network: 'regtest', + services: [ + { + name: 'bitcoind', + module: BitcoinService, + config: { + connect: [ + { + rpchost: '127.0.0.1', + rpcport: 30521, + rpcuser: 'bitcoin', + rpcpassword: 'local321', + zmqpubrawtx: 'tcp://127.0.0.1:30611' + }, + { + rpchost: '127.0.0.1', + rpcport: 30522, + rpcuser: 'bitcoin', + rpcpassword: 'local321', + zmqpubrawtx: 'tcp://127.0.0.1:30622' + }, + { + rpchost: '127.0.0.1', + rpcport: 30523, + rpcuser: 'bitcoin', + rpcpassword: 'local321', + zmqpubrawtx: 'tcp://127.0.0.1:30633' + } + ] + } + } + ] + }; + + var regtest = bitcore.Networks.get('regtest'); + should.exist(regtest); + + node = new BitcoreNode(configuration); + + node.on('error', function(err) { + log.error(err); + }); + + node.on('ready', function() { + done(); + }); + + node.start(function(err) { + if (err) { + return done(err); + } + }); + + }); + + it('step 2: receive block events', function(done) { + this.timeout(10000); + node.services.bitcoind.once('tip', function(height) { + height.should.equal(1); + done(); + }); + node.generateBlock(1, function(err, hashes) { + if (err) { + return done(err); + } + should.exist(hashes); + }); + }); + + it('step 3: get blocks', function(done) { + async.times(3, function(n, next) { + node.getBlock(1, function(err, block) { + if (err) { + return next(err); + } + should.exist(block); + next(); + }); + }, done); + }); + +}); diff --git a/integration/data/.gitignore b/regtest/data/.gitignore similarity index 100% rename from integration/data/.gitignore rename to regtest/data/.gitignore diff --git a/regtest/data/bitcoin.conf b/regtest/data/bitcoin.conf new file mode 100644 index 000000000..95cf0afb0 --- /dev/null +++ b/regtest/data/bitcoin.conf @@ -0,0 +1,12 @@ +server=1 +whitelist=127.0.0.1 +txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +zmqpubrawtx=tcp://127.0.0.1:30332 +zmqpubhashblock=tcp://127.0.0.1:30332 +rpcallowip=127.0.0.1 +rpcport=30331 +rpcuser=bitcoin +rpcpassword=local321 diff --git a/integration/data/bitcoind.crt b/regtest/data/bitcoind.crt similarity index 100% rename from integration/data/bitcoind.crt rename to regtest/data/bitcoind.crt diff --git a/integration/data/bitcoind_no_pass.key b/regtest/data/bitcoind_no_pass.key similarity index 100% rename from integration/data/bitcoind_no_pass.key rename to regtest/data/bitcoind_no_pass.key diff --git a/regtest/data/node1/bitcoin.conf b/regtest/data/node1/bitcoin.conf new file mode 100644 index 000000000..54ed2aecb --- /dev/null +++ b/regtest/data/node1/bitcoin.conf @@ -0,0 +1,16 @@ +server=1 +whitelist=127.0.0.1 +txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +addnode=127.0.0.1:30432 +addnode=127.0.0.1:30433 +port=30431 +rpcport=30521 +zmqpubrawtx=tcp://127.0.0.1:30611 +zmqpubhashblock=tcp://127.0.0.1:30611 +rpcallowip=127.0.0.1 +rpcuser=bitcoin +rpcpassword=local321 +keypool=3 diff --git a/regtest/data/node2/bitcoin.conf b/regtest/data/node2/bitcoin.conf new file mode 100644 index 000000000..bcd09fe66 --- /dev/null +++ b/regtest/data/node2/bitcoin.conf @@ -0,0 +1,16 @@ +server=1 +whitelist=127.0.0.1 +txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +addnode=127.0.0.1:30431 +addnode=127.0.0.1:30433 +port=30432 +rpcport=30522 +zmqpubrawtx=tcp://127.0.0.1:30622 +zmqpubhashblock=tcp://127.0.0.1:30622 +rpcallowip=127.0.0.1 +rpcuser=bitcoin +rpcpassword=local321 +keypool=3 diff --git a/regtest/data/node3/bitcoin.conf b/regtest/data/node3/bitcoin.conf new file mode 100644 index 000000000..8be13ef30 --- /dev/null +++ b/regtest/data/node3/bitcoin.conf @@ -0,0 +1,16 @@ +server=1 +whitelist=127.0.0.1 +txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +addnode=127.0.0.1:30431 +addnode=127.0.0.1:30432 +port=30433 +rpcport=30523 +zmqpubrawtx=tcp://127.0.0.1:30633 +zmqpubhashblock=tcp://127.0.0.1:30633 +rpcallowip=127.0.0.1 +rpcuser=bitcoin +rpcpassword=local321 +keypool=3 diff --git a/integration/regtest-node.js b/regtest/node.js similarity index 60% rename from integration/regtest-node.js rename to regtest/node.js index 9523f79ae..2e8539abd 100644 --- a/integration/regtest-node.js +++ b/regtest/node.js @@ -1,20 +1,13 @@ 'use strict'; -// These tests require bitcore-node Bitcoin Core bindings to be compiled with -// the environment variable BITCORENODE_ENV=test. This enables the use of regtest -// functionality by including the wallet in the build. -// To run the tests: $ mocha -R spec integration/regtest-node.js +// To run the tests: $ mocha -R spec regtest/node.js +var path = require('path'); var index = require('..'); var async = require('async'); var log = index.log; log.debug = function() {}; -if (process.env.BITCORENODE_ENV !== 'test') { - log.info('Please set the environment variable BITCORENODE_ENV=test and make sure bindings are compiled for testing'); - process.exit(); -} - var chai = require('chai'); var bitcore = require('bitcore-lib'); var rimraf = require('rimraf'); @@ -24,12 +17,9 @@ var should = chai.should(); var BitcoinRPC = require('bitcoind-rpc'); var index = require('..'); -var Transaction = index.Transaction; +var Transaction = bitcore.Transaction; var BitcoreNode = index.Node; -var AddressService = index.services.Address; var BitcoinService = index.services.Bitcoin; -var encoding = require('../lib/services/address/encoding'); -var DBService = index.services.DB; var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG'; var testKey; var client; @@ -42,7 +32,7 @@ describe('Node Functionality', function() { var regtest; before(function(done) { - this.timeout(30000); + this.timeout(20000); var datadir = __dirname + '/data'; @@ -55,23 +45,17 @@ describe('Node Functionality', function() { } var configuration = { - datadir: datadir, network: 'regtest', services: [ - { - name: 'db', - module: DBService, - config: {} - }, { name: 'bitcoind', module: BitcoinService, - config: {} - }, - { - name: 'address', - module: AddressService, - config: {} + config: { + spawn: { + datadir: datadir, + exec: path.resolve(__dirname, '../bin/bitcoind') + } + } } ] }; @@ -85,37 +69,35 @@ describe('Node Functionality', function() { log.error(err); }); - node.on('ready', function() { + node.start(function(err) { + if (err) { + return done(err); + } client = new BitcoinRPC({ - protocol: 'https', + protocol: 'http', host: '127.0.0.1', - port: 18332, + port: 30331, user: 'bitcoin', pass: 'local321', rejectUnauthorized: false }); var syncedHandler = function() { - if (node.services.db.tip.__height === 150) { - node.removeListener('synced', syncedHandler); + if (node.services.bitcoind.height === 150) { + node.services.bitcoind.removeListener('synced', syncedHandler); done(); } }; - node.on('synced', syncedHandler); + node.services.bitcoind.on('synced', syncedHandler); - client.generate(150, function(err, response) { + client.generate(150, function(err) { if (err) { throw err; } }); - }); - node.start(function(err) { - if (err) { - throw err; - } }); @@ -132,104 +114,31 @@ describe('Node Functionality', function() { }); }); - var invalidatedBlockHash; - - it('will handle a reorganization', function(done) { - - var count; - var blockHash; - - async.series([ - function(next) { - client.getBlockCount(function(err, response) { - if (err) { - return next(err); - } - count = response.result; - next(); - }); - }, - function(next) { - client.getBlockHash(count, function(err, response) { - if (err) { - return next(err); - } - invalidatedBlockHash = response.result; - next(); - }); - }, - function(next) { - client.invalidateBlock(invalidatedBlockHash, next); - }, - function(next) { - client.getBlockCount(function(err, response) { - if (err) { - return next(err); - } - response.result.should.equal(count - 1); - next(); - }); - } - ], function(err) { - if (err) { - throw err; - } - var blocksRemoved = 0; - var blocksAdded = 0; - - var removeBlock = function() { - blocksRemoved++; - }; - - node.services.db.on('removeblock', removeBlock); - - var addBlock = function() { - blocksAdded++; - if (blocksAdded === 2 && blocksRemoved === 1) { - node.services.db.removeListener('addblock', addBlock); - node.services.db.removeListener('removeblock', removeBlock); - done(); - } - }; - - node.services.db.on('addblock', addBlock); - - // We need to add a transaction to the mempool so that the next block will - // have a different hash as the hash has been invalidated. - client.sendToAddress(testKey.toAddress(regtest).toString(), 10, function(err) { - if (err) { - throw err; - } - client.generate(2, function(err, response) { - if (err) { - throw err; - } - }); - }); - }); - - }); - - it('isMainChain() will return false for stale/orphan block', function(done) { - node.services.bitcoind.isMainChain(invalidatedBlockHash).should.equal(false); - setImmediate(done); - }); - describe('Bus Functionality', function() { it('subscribes and unsubscribes to an event on the bus', function(done) { var bus = node.openBus(); - var block; - bus.subscribe('db/block'); - bus.on('db/block', function(data) { - bus.unsubscribe('db/block'); - data.should.be.equal(block); - done(); + var blockExpected; + var blockReceived; + bus.subscribe('bitcoind/hashblock'); + bus.on('bitcoind/hashblock', function(data) { + bus.unsubscribe('bitcoind/hashblock'); + if (blockExpected) { + data.should.be.equal(blockExpected); + done(); + } else { + blockReceived = data; + } }); client.generate(1, function(err, response) { if (err) { throw err; } - block = response.result[0]; + if (blockReceived) { + blockReceived.should.be.equal(response.result[0]); + done(); + } else { + blockExpected = response.result[0]; + } }); }); }); @@ -237,20 +146,37 @@ describe('Node Functionality', function() { describe('Address Functionality', function() { var address; var unspentOutput; - before(function() { + before(function(done) { + this.timeout(10000); address = testKey.toAddress(regtest).toString(); + var startHeight = node.services.bitcoind.height; + node.services.bitcoind.on('tip', function(height) { + if (height === startHeight + 3) { + done(); + } + }); + client.sendToAddress(testKey.toAddress(regtest).toString(), 10, function(err) { + if (err) { + throw err; + } + client.generate(3, function(err) { + if (err) { + throw err; + } + }); + }); }); it('should be able to get the balance of the test address', function(done) { - node.services.address.getBalance(address, false, function(err, balance) { + node.getAddressBalance(address, false, function(err, data) { if (err) { throw err; } - balance.should.equal(10 * 1e8); + data.balance.should.equal(10 * 1e8); done(); }); }); it('can get unspent outputs for address', function(done) { - node.services.address.getUnspentOutputs(address, false, function(err, results) { + node.getAddressUnspentOutputs(address, false, function(err, results) { if (err) { throw err; } @@ -265,7 +191,7 @@ describe('Node Functionality', function() { to: 10, queryMempool: false }; - node.services.address.getAddressHistory(address, options, function(err, results) { + node.getAddressHistory(address, options, function(err, results) { if (err) { throw err; } @@ -278,9 +204,8 @@ describe('Node Functionality', function() { info.addresses[address].inputIndexes.should.deep.equal([]); info.satoshis.should.equal(10 * 1e8); info.confirmations.should.equal(3); - info.timestamp.should.be.a('number'); - info.fees.should.be.within(950, 970); - info.tx.should.be.an.instanceof(Transaction); + info.tx.blockTimestamp.should.be.a('number'); + info.tx.feeSatoshis.should.be.within(950, 4000); done(); }); }); @@ -288,16 +213,16 @@ describe('Node Functionality', function() { var options = { queryMempool: false }; - node.services.address.getAddressSummary(address, options, function(err, results) { + node.getAddressSummary(address, options, function(err, results) { if (err) { throw err; } results.totalReceived.should.equal(1000000000); results.totalSpent.should.equal(0); results.balance.should.equal(1000000000); - results.unconfirmedBalance.should.equal(0); + should.not.exist(results.unconfirmedBalance); results.appearances.should.equal(1); - results.unconfirmedAppearances.should.equal(0); + should.not.exist(results.unconfirmedAppearances); results.txids.length.should.equal(1); done(); }); @@ -316,10 +241,20 @@ describe('Node Functionality', function() { var address5; var testKey6; var address6; + var tx2Amount; + var tx2Hash; before(function(done) { /* jshint maxstatements: 50 */ + // Finished once all blocks have been mined + var startHeight = node.services.bitcoind.height; + node.services.bitcoind.on('tip', function(height) { + if (height === startHeight + 5) { + done(); + } + }); + testKey2 = bitcore.PrivateKey.fromWIF('cNfF4jXiLHQnFRsxaJyr2YSGcmtNYvxQYSakNhuDGxpkSzAwn95x'); address2 = testKey2.toAddress(regtest).toString(); @@ -347,8 +282,6 @@ describe('Node Functionality', function() { unspentOutputSpentTxId = tx.id; - node.services.bitcoind.sendTransaction(tx.serialize()); - function mineBlock(next) { client.generate(1, function(err, response) { if (err) { @@ -359,13 +292,18 @@ describe('Node Functionality', function() { }); } - client.generate(1, function(err, response) { + node.sendTransaction(tx.serialize(), function(err, hash) { if (err) { - throw err; + return done(err); } - should.exist(response); - node.once('synced', function() { - node.services.address.getUnspentOutputs(address, false, function(err, results) { + + client.generate(1, function(err, response) { + if (err) { + throw err; + } + should.exist(response); + + node.getAddressUnspentOutputs(address, false, function(err, results) { /* jshint maxstatements: 50 */ if (err) { throw err; @@ -375,28 +313,42 @@ describe('Node Functionality', function() { async.series([ function(next) { var tx2 = new Transaction(); + tx2Amount = results[0].satoshis - 10000; tx2.from(results[0]); - tx2.to(address2, results[0].satoshis - 10000); + tx2.to(address2, tx2Amount); tx2.change(address); tx2.sign(testKey); - node.services.bitcoind.sendTransaction(tx2.serialize()); - mineBlock(next); + tx2Hash = tx2.hash; + node.sendTransaction(tx2.serialize(), function(err) { + if (err) { + return next(err); + } + mineBlock(next); + }); }, function(next) { var tx3 = new Transaction(); tx3.from(results[1]); tx3.to(address3, results[1].satoshis - 10000); tx3.change(address); tx3.sign(testKey); - node.services.bitcoind.sendTransaction(tx3.serialize()); - mineBlock(next); + node.sendTransaction(tx3.serialize(), function(err) { + if (err) { + return next(err); + } + mineBlock(next); + }); }, function(next) { var tx4 = new Transaction(); tx4.from(results[2]); tx4.to(address4, results[2].satoshis - 10000); tx4.change(address); tx4.sign(testKey); - node.services.bitcoind.sendTransaction(tx4.serialize()); - mineBlock(next); + node.sendTransaction(tx4.serialize(), function(err) { + if (err) { + return next(err); + } + mineBlock(next); + }); }, function(next) { var tx5 = new Transaction(); tx5.from(results[3]); @@ -405,19 +357,22 @@ describe('Node Functionality', function() { tx5.to(address6, results[4].satoshis - 10000); tx5.change(address); tx5.sign(testKey); - node.services.bitcoind.sendTransaction(tx5.serialize()); - mineBlock(next); + node.sendTransaction(tx5.serialize(), function(err) { + if (err) { + return next(err); + } + mineBlock(next); + }); } ], function(err) { if (err) { throw err; } - node.once('synced', function() { - done(); - }); }); }); + }); + }); }); @@ -431,22 +386,23 @@ describe('Node Functionality', function() { address6 ]; var options = {}; - node.services.address.getAddressHistory(addresses, options, function(err, results) { + node.getAddressHistory(addresses, options, function(err, results) { if (err) { throw err; } results.totalCount.should.equal(4); var history = results.items; history.length.should.equal(4); - history[0].height.should.equal(157); + history[0].tx.height.should.equal(159); history[0].confirmations.should.equal(1); - history[1].height.should.equal(156); + history[1].tx.height.should.equal(158); should.exist(history[1].addresses[address4]); - history[2].height.should.equal(155); + history[2].tx.height.should.equal(157); should.exist(history[2].addresses[address3]); - history[3].height.should.equal(154); + history[3].tx.height.should.equal(156); should.exist(history[3].addresses[address2]); - history[3].satoshis.should.equal(99990000); + history[3].satoshis.should.equal(tx2Amount); + history[3].tx.hash.should.equal(tx2Hash); history[3].confirmations.should.equal(4); done(); }); @@ -461,20 +417,20 @@ describe('Node Functionality', function() { address6 ]; var options = { - start: 157, - end: 156 + start: 158, + end: 157 }; - node.services.address.getAddressHistory(addresses, options, function(err, results) { + node.getAddressHistory(addresses, options, function(err, results) { if (err) { throw err; } results.totalCount.should.equal(2); var history = results.items; history.length.should.equal(2); - history[0].height.should.equal(157); - history[0].confirmations.should.equal(1); - history[1].height.should.equal(156); - should.exist(history[1].addresses[address4]); + history[0].tx.height.should.equal(158); + history[0].confirmations.should.equal(2); + history[1].tx.height.should.equal(157); + should.exist(history[1].addresses[address3]); done(); }); }); @@ -488,18 +444,18 @@ describe('Node Functionality', function() { address6 ]; var options = { - start: 155, - end: 154 + start: 157, + end: 156 }; - node.services.address.getAddressHistory(addresses, options, function(err, results) { + node.getAddressHistory(addresses, options, function(err, results) { if (err) { throw err; } results.totalCount.should.equal(2); var history = results.items; history.length.should.equal(2); - history[0].height.should.equal(155); - history[1].height.should.equal(154); + history[0].tx.height.should.equal(157); + history[1].tx.height.should.equal(156); done(); }); }); @@ -516,16 +472,16 @@ describe('Node Functionality', function() { from: 0, to: 3 }; - node.services.address.getAddressHistory(addresses, options, function(err, results) { + node.getAddressHistory(addresses, options, function(err, results) { if (err) { throw err; } results.totalCount.should.equal(4); var history = results.items; history.length.should.equal(3); - history[0].height.should.equal(157); + history[0].tx.height.should.equal(159); history[0].confirmations.should.equal(1); - history[1].height.should.equal(156); + history[1].tx.height.should.equal(158); should.exist(history[1].addresses[address4]); done(); }); @@ -536,32 +492,32 @@ describe('Node Functionality', function() { address ]; var options = {}; - node.services.address.getAddressHistory(addresses, options, function(err, results) { + node.getAddressHistory(addresses, options, function(err, results) { if (err) { throw err; } results.totalCount.should.equal(6); var history = results.items; history.length.should.equal(6); - history[0].height.should.equal(157); + history[0].tx.height.should.equal(159); history[0].addresses[address].inputIndexes.should.deep.equal([0, 1]); history[0].addresses[address].outputIndexes.should.deep.equal([2]); history[0].confirmations.should.equal(1); - history[1].height.should.equal(156); - history[2].height.should.equal(155); - history[3].height.should.equal(154); - history[4].height.should.equal(153); + history[1].tx.height.should.equal(158); + history[2].tx.height.should.equal(157); + history[3].tx.height.should.equal(156); + history[4].tx.height.should.equal(155); history[4].satoshis.should.equal(-10000); history[4].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]); history[4].addresses[address].inputIndexes.should.deep.equal([0]); - history[5].height.should.equal(150); + history[5].tx.height.should.equal(152); history[5].satoshis.should.equal(10 * 1e8); done(); }); }); it('summary for an address (sending and receiving)', function(done) { - node.services.address.getAddressSummary(address, {}, function(err, results) { + node.getAddressSummary(address, {}, function(err, results) { if (err) { throw err; } @@ -582,7 +538,7 @@ describe('Node Functionality', function() { address ]; var options = {}; - node.services.address.getAddressHistory(addresses, options, function(err, results) { + node.getAddressHistory(addresses, options, function(err, results) { if (err) { throw err; } @@ -597,13 +553,13 @@ describe('Node Functionality', function() { from: 0, to: 1 }; - node.services.address.getAddressHistory(address, options, function(err, results) { + node.getAddressHistory(address, options, function(err, results) { if (err) { throw err; } var history = results.items; history.length.should.equal(1); - history[0].height.should.equal(157); + history[0].tx.height.should.equal(159); done(); }); }); @@ -612,13 +568,13 @@ describe('Node Functionality', function() { from: 1, to: 2 }; - node.services.address.getAddressHistory(address, options, function(err, results) { + node.getAddressHistory(address, options, function(err, results) { if (err) { throw err; } var history = results.items; history.length.should.equal(1); - history[0].height.should.equal(156); + history[0].tx.height.should.equal(158); done(); }); }); @@ -627,13 +583,13 @@ describe('Node Functionality', function() { from: 2, to: 3 }; - node.services.address.getAddressHistory(address, options, function(err, results) { + node.getAddressHistory(address, options, function(err, results) { if (err) { throw err; } var history = results.items; history.length.should.equal(1); - history[0].height.should.equal(155); + history[0].tx.height.should.equal(157); done(); }); }); @@ -642,13 +598,13 @@ describe('Node Functionality', function() { from: 3, to: 4 }; - node.services.address.getAddressHistory(address, options, function(err, results) { + node.getAddressHistory(address, options, function(err, results) { if (err) { throw err; } var history = results.items; history.length.should.equal(1); - history[0].height.should.equal(154); + history[0].tx.height.should.equal(156); done(); }); }); @@ -657,13 +613,13 @@ describe('Node Functionality', function() { from: 4, to: 5 }; - node.services.address.getAddressHistory(address, options, function(err, results) { + node.getAddressHistory(address, options, function(err, results) { if (err) { throw err; } var history = results.items; history.length.should.equal(1); - history[0].height.should.equal(153); + history[0].tx.height.should.equal(155); history[0].satoshis.should.equal(-10000); history[0].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]); history[0].addresses[address].inputIndexes.should.deep.equal([0]); @@ -675,13 +631,13 @@ describe('Node Functionality', function() { from: 5, to: 6 }; - node.services.address.getAddressHistory(address, options, function(err, results) { + node.getAddressHistory(address, options, function(err, results) { if (err) { throw err; } var history = results.items; history.length.should.equal(1); - history[0].height.should.equal(150); + history[0].tx.height.should.equal(152); history[0].satoshis.should.equal(10 * 1e8); done(); }); @@ -693,7 +649,7 @@ describe('Node Functionality', function() { describe('Mempool Index', function() { var unspentOutput; before(function(done) { - node.services.address.getUnspentOutputs(address, false, function(err, results) { + node.getAddressUnspentOutputs(address, false, function(err, results) { if (err) { throw err; } @@ -704,100 +660,31 @@ describe('Node Functionality', function() { }); it('will update the mempool index after new tx', function(done) { - + var memAddress = bitcore.PrivateKey().toAddress(node.network).toString(); var tx = new Transaction(); tx.from(unspentOutput); - tx.to(address, unspentOutput.satoshis - 1000); + tx.to(memAddress, unspentOutput.satoshis - 1000); tx.fee(1000); tx.sign(testKey); - node.services.bitcoind.sendTransaction(tx.serialize()); - - setImmediate(function() { - var addrObj = encoding.getAddressInfo(address); - node.services.address._getOutputsMempool(address, addrObj.hashBuffer, - addrObj.hashTypeBuffer, function(err, outs) { + node.services.bitcoind.sendTransaction(tx.serialize(), function(err, hash) { + node.getAddressTxids(memAddress, {}, function(err, txids) { if (err) { - throw err; + return done(err); } - outs.length.should.equal(1); - done(); - }); - }); - }); - - }); - - describe('#getInputForOutput(db)', function() { - it('will get the input txid and input index', function(done) { - var txid = outputForIsSpentTest1.txid; - var outputIndex = outputForIsSpentTest1.outputIndex; - var options = { - queryMempool: true - }; - node.services.address.getInputForOutput(txid, outputIndex, options, function(err, result) { - result.inputTxId.should.equal(unspentOutputSpentTxId); - result.inputIndex.should.equal(0); - done(); - }); - }); - }); - - describe('#isSpent and #getInputForOutput(mempool)', function() { - var spentOutput; - var spentOutputInputTxId; - it('will return true if an input is spent in a confirmed transaction', function(done) { - var txid = outputForIsSpentTest1.txid; - var outputIndex = outputForIsSpentTest1.outputIndex; - var result = node.services.bitcoind.isSpent(txid, outputIndex); - result.should.equal(true); - done(); - }); - //CCoinsViewMemPool only checks for spent outputs that are not the mempool - it('will correctly return false for an input that is spent in an unconfirmed transaction', function(done) { - node.services.address.getUnspentOutputs(address, false, function(err, results) { - if (err) { - throw err; - } - - var unspentOutput = results[0]; - - var tx = new Transaction(); - tx.from(unspentOutput); - tx.to(address, unspentOutput.satoshis - 1000); - tx.fee(1000); - tx.sign(testKey); - - node.services.bitcoind.sendTransaction(tx.serialize()); - spentOutput = unspentOutput; - spentOutputInputTxId = tx.hash; - - setImmediate(function() { - var result = node.services.bitcoind.isSpent(unspentOutput.txid, unspentOutput.outputIndex); - result.should.equal(false); + txids.length.should.equal(1); + txids[0].should.equal(hash); done(); }); }); }); - it('will get the input txid and input index (mempool)', function(done) { - var txid = spentOutput.txid; - var outputIndex = spentOutput.outputIndex; - var options = { - queryMempool: true - }; - node.services.address.getInputForOutput(txid, outputIndex, options, function(err, result) { - result.inputTxId.should.equal(spentOutputInputTxId); - result.inputIndex.should.equal(0); - done(); - }); - }); - }); }); describe('Orphaned Transactions', function() { + this.timeout(8000); var orphanedTransaction; before(function(done) { @@ -805,6 +692,14 @@ describe('Node Functionality', function() { var invalidatedBlockHash; async.series([ + function(next) { + client.sendToAddress(testKey.toAddress(regtest).toString(), 10, function(err) { + if (err) { + return next(err); + } + client.generate(1, next); + }); + }, function(next) { client.getBlockCount(function(err, response) { if (err) { @@ -829,6 +724,7 @@ describe('Node Functionality', function() { return next(err); } orphanedTransaction = response.result.tx[1]; + should.exist(orphanedTransaction); next(); }); }, @@ -846,10 +742,11 @@ describe('Node Functionality', function() { it('will not show confirmation count for orphaned transaction', function(done) { // This test verifies that in the situation that the transaction is not in the mempool and // is included in an orphaned block transaction index that the confirmation count will be unconfirmed. - node.services.bitcoind.getTransactionWithBlockInfo(orphanedTransaction, false, function(err, data) { + node.getDetailedTransaction(orphanedTransaction, function(err, data) { if (err) { return done(err); } + should.exist(data); should.exist(data.height); data.height.should.equal(-1); done(); diff --git a/integration/p2p.js b/regtest/p2p.js similarity index 84% rename from integration/p2p.js rename to regtest/p2p.js index b578b9088..4af160bd7 100644 --- a/integration/p2p.js +++ b/regtest/p2p.js @@ -1,12 +1,11 @@ 'use strict'; +// To run the tests: $ mocha -R spec regtest/p2p.js + +var path = require('path'); var index = require('..'); var log = index.log; -if (process.env.BITCORENODE_ENV !== 'test') { - log.info('Please set the environment variable BITCORENODE_ENV=test and make sure bindings are compiled for testing'); - process.exit(); -} var p2p = require('bitcore-p2p'); var Peer = p2p.Peer; var Messages = p2p.Messages; @@ -40,36 +39,23 @@ describe('P2P Functionality', function() { before(function(done) { this.timeout(100000); - // Add the regtest network - bitcore.Networks.remove(bitcore.Networks.testnet); - bitcore.Networks.add({ - name: 'regtest', - alias: 'regtest', - pubkeyhash: 0x6f, - privatekey: 0xef, - scripthash: 0xc4, - xpubkey: 0x043587cf, - xprivkey: 0x04358394, - networkMagic: 0xfabfb5da, - port: 18444, - dnsSeeds: [ ] - }); - + // enable regtest + bitcore.Networks.enableRegtest(); var regtestNetwork = bitcore.Networks.get('regtest'); var datadir = __dirname + '/data'; - rimraf(datadir + '/regtest', function(err) {; - + rimraf(datadir + '/regtest', function(err) { if (err) { throw err; } bitcoind = require('../').services.Bitcoin({ - node: { + spawn: { datadir: datadir, - network: { - name: 'regtest' - } + exec: path.resolve(__dirname, '../bin/bitcoind') + }, + node: { + network: bitcore.Networks.testnet } }); @@ -79,13 +65,16 @@ describe('P2P Functionality', function() { log.info('Waiting for Bitcoin Core to initialize...'); - bitcoind.start(function() { + bitcoind.start(function(err) { + if (err) { + throw err; + } log.info('Bitcoind started'); client = new BitcoinRPC({ - protocol: 'https', + protocol: 'http', host: '127.0.0.1', - port: 18332, + port: 30331, user: 'bitcoin', pass: 'local321', rejectUnauthorized: false @@ -174,6 +163,7 @@ describe('P2P Functionality', function() { this.timeout(20000); peer.on('disconnect', function() { log.info('Peer disconnected'); + bitcoind.node.stopping = true; bitcoind.stop(function(err, result) { done(); }); @@ -186,13 +176,11 @@ describe('P2P Functionality', function() { var usedTxs = {}; - bitcoind.on('tx', function(result) { - var txFromResult = new Transaction().fromBuffer(result.buffer); + bitcoind.on('tx', function(buffer) { + var txFromResult = new Transaction().fromBuffer(buffer); var tx = usedTxs[txFromResult.id]; should.exist(tx); - result.buffer.toString('hex').should.equal(tx.serialize()); - result.hash.should.equal(tx.hash); - result.mempool.should.equal(true); + buffer.toString('hex').should.equal(tx.serialize()); delete usedTxs[tx.id]; if (Object.keys(usedTxs).length === 0) { done(); diff --git a/scripts/download b/scripts/download new file mode 100755 index 000000000..f142b9865 --- /dev/null +++ b/scripts/download @@ -0,0 +1,119 @@ +#!/bin/bash + +set -e + +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.." +platform=`uname -a | awk '{print tolower($1)}'` +arch=`uname -m` +version="0.12.0" +url="https://github.com/bitpay/bitcoin/releases/download" +tag="v0.12-bitcore" + +if [ "${platform}" == "linux" ]; then + if [ "${arch}" == "x86_64" ]; then + tarball_name="bitcoin-${version}-linux64.tar.gz" + elif [ "${arch}" == "x86_32" ]; then + tarball_name="bitcoin-${version}-linux32.tar.gz" + fi +elif [ "${platform}" == "darwin" ]; then + tarball_name="bitcoin-${version}-osx64.tar.gz" +else + echo "Bitcoin binary distribution not available for platform and architecture" + exit -1 +fi + +binary_url="${url}/${tag}/${tarball_name}" +shasums_url="${url}/${tag}/SHA256SUMS.asc" + +download_bitcoind() { + + cd "${root_dir}/bin" + + echo "Downloading bitcoin: ${binary_url}" + + is_curl=true + if hash curl 2>/dev/null; then + curl --fail -I $binary_url >/dev/null 2>&1 + else + is_curl=false + wget --server-response --spider $binary_url >/dev/null 2>&1 + fi + + if test $? -eq 0; then + if [ "${is_curl}" = true ]; then + curl -L $binary_url > $tarball_name + curl -L $shasums_url > SHA256SUMS.asc + else + wget $binary_url + wget $shasums_url + fi + if test -e "${tarball_name}"; then + echo "Unpacking bitcoin distribution" + tar -xvzf $tarball_name + if test $? -eq 0; then + ln -sf "bitcoin-${version}/bin/bitcoind" + return; + fi + fi + fi + echo "Bitcoin binary distribution could not be downloaded" + exit -1 +} + +verify_download() { + echo "Verifying signatures of bitcoin download" + gpg --verify "${root_dir}/bin/SHA256SUMS.asc" + + if hash shasum 2>/dev/null; then + shasum_cmd="shasum -a 256" + else + shasum_cmd="sha256sum" + fi + + download_sha=$(${shasum_cmd} "${root_dir}/bin/${tarball_name}" | awk '{print $1}') + expected_sha=$(cat "${root_dir}/bin/SHA256SUMS.asc" | grep "${tarball_name}" | awk '{print $1}') + echo "Checksum (download): ${download_sha}" + echo "Checksum (verified): ${expected_sha}" + if [ "${download_sha}" != "${expected_sha}" ]; then + echo -e "\033[1;31mChecksums did NOT match!\033[0m\n" + exit 1 + else + echo -e "\033[1;32mChecksums matched!\033[0m\n" + fi +} + +download=1 +verify=0 + +if [ "${SKIP_BITCOIN_DOWNLOAD}" = 1 ]; then + download=0; +fi + +if [ "${VERIFY_BITCOIN_DOWNLOAD}" = 1 ]; then + verify=1; +fi + +while [ -n "$1" ]; do + param="$1" + value="$2" + + case $param in + --skip-bitcoin-download) + download=0 + ;; + --verify-bitcoin-download) + verify=1 + ;; + esac + shift +done + +if [ "${download}" = 1 ]; then + download_bitcoind +fi + +if [ "${verify}" = 1 ]; then + verify_download +fi + +exit 0 diff --git a/scripts/regtest b/scripts/regtest new file mode 100755 index 000000000..96078911d --- /dev/null +++ b/scripts/regtest @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +_mocha -R spec regtest/p2p.js +_mocha -R spec regtest/bitcoind.js +_mocha -R spec regtest/cluster.js +_mocha -R spec regtest/node.js diff --git a/src/libbitcoind.cc b/src/libbitcoind.cc deleted file mode 100644 index 4627b6d35..000000000 --- a/src/libbitcoind.cc +++ /dev/null @@ -1,1614 +0,0 @@ -/** - * bitcoind.js - a binding for node.js which links to libbitcoind.so/dylib. - * Copyright (c) 2015, BitPay (MIT License) - * - * libbitcoind.cc: - * A bitcoind node.js binding. - */ - -#include "libbitcoind.h" - -using namespace std; -using namespace boost; -using namespace node; -using namespace v8; -using Nan::New; -using Nan::Null; -using Nan::Set; -using Nan::ThrowError; -using Nan::GetCurrentContext; -using Nan::GetFunction; -using v8::FunctionTemplate; - -/** - * Bitcoin Globals - */ - -// These global functions and variables are -// required to be defined/exposed here. - -extern void WaitForShutdown(boost::thread_group* threadGroup); -static termios orig_termios; -extern CTxMemPool mempool; -extern int64_t nTimeBestReceived; - -/** - * Node.js Internal Function Templates - */ - -static void -tx_notifier(uv_async_t *handle); - -static void -txleave_notifier(uv_async_t *handle); - -static void -async_tip_update(uv_work_t *req); - -static void -async_tip_update_after(uv_work_t *req); - -static void -async_start_node(uv_work_t *req); - -static void -async_start_node_after(uv_work_t *req); - -static void -async_blocks_ready(uv_work_t *req); - -static void -async_blocks_ready_after(uv_work_t *req); - -static void -async_stop_node(uv_work_t *req); - -static void -async_stop_node_after(uv_work_t *req); - -static int -start_node(void); - -static void -start_node_thread(void); - -static void -async_get_block(uv_work_t *req); - -static void -async_get_block_after(uv_work_t *req); - -static void -async_get_tx(uv_work_t *req); - -static void -async_get_tx_after(uv_work_t *req); - -static void -async_get_tx_and_info(uv_work_t *req); - -static void -async_get_tx_and_info_after(uv_work_t *req); - -static bool -queueTx(const CTransaction&); - -static bool -queueTxLeave(const CTransaction&); - -extern "C" void -init(Handle); - -/** - * Private Global Variables - * Used only by bitcoind functions. - */ -static std::vector txQueue; -static std::vector txQueueLeave; -static uv_async_t txmon_async; -static uv_async_t txmonleave_async; -static Eternal txmon_callback; -static Eternal txmonleave_callback; -static bool txmon_callback_available; -static bool txmonleave_callback_available; - -static volatile bool shutdown_complete = false; -static char *g_data_dir = NULL; -static bool g_rpc = false; -static bool g_testnet = false; -static bool g_regtest = false; -static bool g_txindex = false; - -static boost::thread_group threadGroup; - -/** - * Private Structs - * Used for async functions and necessary linked lists at points. - */ - -struct async_tip_update_data { - uv_work_t req; - size_t result; - Isolate* isolate; - Persistent callback; -}; - -/** - * async_node_data - * Where the uv async request data resides. - */ - -struct async_block_ready_data { - uv_work_t req; - std::string err_msg; - std::string result; - Isolate* isolate; - Persistent callback; -}; - -/** - * async_node_data - * Where the uv async request data resides. - */ - -struct async_node_data { - uv_work_t req; - std::string err_msg; - std::string result; - std::string datadir; - bool rpc; - bool testnet; - bool regtest; - bool txindex; - Isolate* isolate; - Persistent callback; -}; - -/** - * async_block_data - */ - -struct async_block_data { - uv_work_t req; - std::string err_msg; - uint256 hash; - int64_t height; - char* buffer; - uint32_t size; - CBlock cblock; - CBlockIndex* cblock_index; - Isolate* isolate; - Persistent callback; -}; - -/** - * async_tx_data - */ - -struct async_tx_data { - uv_work_t req; - std::string err_msg; - std::string txid; - std::string blockHash; - uint32_t nTime; - int64_t height; - bool queryMempool; - CTransaction ctx; - Isolate* isolate; - Persistent callback; -}; - -/** - * Helpers - */ - -static bool -set_cooked(void); - -/** - * SyncPercentage() - * bitcoind.syncPercentage() - * provides a float value >= indicating the progress of the blockchain sync - */ -NAN_METHOD(SyncPercentage) { - const CChainParams& chainParams = Params(); - float progress = 0; - progress = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), chainActive.Tip()); - info.GetReturnValue().Set(progress * 100); -}; - -NAN_METHOD(GetBestBlockHash) { - LOCK(cs_main); - info.GetReturnValue().Set(New(chainActive.Tip()->GetBlockHash().GetHex()).ToLocalChecked()); -} - -NAN_METHOD(GetNextBlockHash) { - - if (info.Length() < 1 || !info[0]->IsString()) { - return ThrowError("Usage: bitcoind.getNextBlockHash(blockhash)"); - } - - CBlockIndex* pblockindex; - v8::String::Utf8Value param1(info[0]->ToString()); - std::string *hash = new std::string(*param1); - uint256 shash = uint256S(*hash); - pblockindex = mapBlockIndex[shash]; - CBlockIndex* pnextblockindex = chainActive.Next(pblockindex); - if (pnextblockindex) { - uint256 nexthash = pnextblockindex->GetBlockHash(); - std::string rethash = nexthash.ToString(); - info.GetReturnValue().Set(New(rethash).ToLocalChecked()); - } else { - info.GetReturnValue().Set(Null()); - } - -} - -/** - * IsSynced() - * bitcoind.isSynced() - * returns a boolean of bitcoin is fully synced - */ -NAN_METHOD(IsSynced) { - bool isDownloading = IsInitialBlockDownload(); - info.GetReturnValue().Set(New(!isDownloading)); -}; - -NAN_METHOD(StartTxMon) { - Isolate* isolate = info.GetIsolate(); - Local callback = Local::Cast(info[0]); - Eternal cb(isolate, callback); - txmon_callback = cb; - txmon_callback_available = true; - - CNodeSignals& nodeSignals = GetNodeSignals(); - nodeSignals.TxToMemPool.connect(&queueTx); - - uv_async_init(uv_default_loop(), &txmon_async, tx_notifier); - - info.GetReturnValue().Set(Null()); -}; - -NAN_METHOD(StartTxMonLeave) { - Isolate* isolate = info.GetIsolate(); - Local callback = Local::Cast(info[0]); - Eternal cb(isolate, callback); - txmonleave_callback = cb; - txmonleave_callback_available = true; - - CNodeSignals& nodeSignals = GetNodeSignals(); - nodeSignals.TxLeaveMemPool.connect(&queueTxLeave); - - uv_async_init(uv_default_loop(), &txmonleave_async, txleave_notifier); - - info.GetReturnValue().Set(Null()); -}; - -static void -tx_notifier(uv_async_t *handle) { - Isolate* isolate = Isolate::GetCurrent(); - HandleScope scope(isolate); - - Local results = Array::New(isolate); - int arrayIndex = 0; - - LOCK(cs_main); - BOOST_FOREACH(const CTransaction& tx, txQueue) { - - CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); - ssTx << tx; - std::string stx = ssTx.str(); - Nan::MaybeLocal txBuffer = Nan::CopyBuffer((char *)stx.c_str(), stx.size()); - - uint256 hash = tx.GetHash(); - - Local obj = New(); - - Nan::Set(obj, New("buffer").ToLocalChecked(), txBuffer.ToLocalChecked()); - Nan::Set(obj, New("hash").ToLocalChecked(), New(hash.GetHex()).ToLocalChecked()); - Nan::Set(obj, New("mempool").ToLocalChecked(), New(true)); - - results->Set(arrayIndex, obj); - arrayIndex++; - } - - const unsigned argc = 1; - Local argv[argc] = { - Local::New(isolate, results) - }; - - Local cb = txmon_callback.Get(isolate); - - cb->Call(isolate->GetCurrentContext()->Global(), argc, argv); - - txQueue.clear(); - -} -static bool -queueTx(const CTransaction& tx) { - LOCK(cs_main); - txQueue.push_back(tx); - uv_async_send(&txmon_async); - return true; -} - -static void -txleave_notifier(uv_async_t *handle) { - Isolate* isolate = Isolate::GetCurrent(); - HandleScope scope(isolate); - - Local results = Array::New(isolate); - int arrayIndex = 0; - - LOCK(cs_main); - BOOST_FOREACH(const CTransaction& tx, txQueueLeave) { - - CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); - ssTx << tx; - std::string stx = ssTx.str(); - Nan::MaybeLocal txBuffer = Nan::CopyBuffer((char *)stx.c_str(), stx.size()); - - uint256 hash = tx.GetHash(); - - Local obj = New(); - - Nan::Set(obj, New("buffer").ToLocalChecked(), txBuffer.ToLocalChecked()); - Nan::Set(obj, New("hash").ToLocalChecked(), New(hash.GetHex()).ToLocalChecked()); - - results->Set(arrayIndex, obj); - arrayIndex++; - } - - const unsigned argc = 1; - Local argv[argc] = { - Local::New(isolate, results) - }; - - Local cb = txmonleave_callback.Get(isolate); - - cb->Call(isolate->GetCurrentContext()->Global(), argc, argv); - - txQueueLeave.clear(); - -} -static bool -queueTxLeave(const CTransaction& tx) { - LOCK(cs_main); - txQueueLeave.push_back(tx); - uv_async_send(&txmonleave_async); - return true; -} - -/** - * Functions - */ - -NAN_METHOD(OnTipUpdate) { - Isolate* isolate = info.GetIsolate(); - HandleScope scope(isolate); - - async_tip_update_data *req = new async_tip_update_data(); - - Local callback = Local::Cast(info[0]); - req->callback.Reset(isolate, callback); - req->req.data = req; - req->isolate = isolate; - - int status = uv_queue_work(uv_default_loop(), - &req->req, async_tip_update, - (uv_after_work_cb)async_tip_update_after); - - assert(status == 0); - - info.GetReturnValue().Set(Null()); -} - -static void -async_tip_update(uv_work_t *req) { - async_tip_update_data *data = reinterpret_cast(req->data); - - size_t lastHeight = chainActive.Height(); - - while(lastHeight == (size_t)chainActive.Height() && !shutdown_complete) { - usleep(1E6); - } - - data->result = chainActive.Height(); - -} - -static void -async_tip_update_after(uv_work_t *r) { - async_tip_update_data *req = reinterpret_cast(r->data); - Isolate* isolate = req->isolate; - HandleScope scope(isolate); - Local cb = Local::New(isolate, req->callback); - - Nan::TryCatch try_catch; - Local result = Undefined(isolate); - - if (!shutdown_complete) { - result = New(req->result); - } - Local argv[1] = { - Local::New(isolate, result) - }; - cb->Call(isolate->GetCurrentContext()->Global(), 1, argv); - if (try_catch.HasCaught()) { - Nan::FatalException(try_catch); - } - - req->callback.Reset(); -} - -NAN_METHOD(OnBlocksReady) { - Isolate* isolate = info.GetIsolate(); - HandleScope scope(isolate); - - async_block_ready_data *req = new async_block_ready_data(); - req->err_msg = std::string(""); - req->result = std::string(""); - req->req.data = req; - req->isolate = isolate; - - Local callback = Local::Cast(info[0]); - req->callback.Reset(isolate, callback); - - int status = uv_queue_work(uv_default_loop(), - &req->req, async_blocks_ready, - (uv_after_work_cb)async_blocks_ready_after); - - assert(status == 0); - - info.GetReturnValue().Set(Null()); -} - -/** - * async_start_node() - * Call start_node() and start all our boost threads. - */ - -static void -async_blocks_ready(uv_work_t *req) { - async_block_ready_data *data = reinterpret_cast(req->data); - data->result = std::string(""); - - while(!chainActive.Tip()) { - usleep(1E6); - } - - CBlockIndex* tip = chainActive.Tip(); - uint256 tipHash = tip->GetBlockHash(); - - // Wait to be able to query for blocks by hash - while(mapBlockIndex.count(tipHash) == 0) { - usleep(1E6); - } - - // Wait for chainActive to be able to get the hash - // for the genesis block for querying blocks by height - while(chainActive[0] == NULL) { - usleep(1E6); - } - - //If the wallet is enabled, then we should make sure we can load it -#ifdef ENABLE_WALLET - while(pwalletMain == NULL || RPCIsInWarmup(NULL)) { - usleep(1E6); - } -#endif - - // Wait until we can get a lock on cs_main - // And therefore ready to be able to quickly - // query for transactions from the mempool. - LOCK(cs_main); - { - return; - } - -} - -static void -async_blocks_ready_after(uv_work_t *r) { - async_block_ready_data* req = reinterpret_cast(r->data); - Isolate* isolate = req->isolate; - HandleScope scope(isolate); - - Nan::TryCatch try_catch; - Local cb = Local::New(isolate, req->callback); - - if (req->err_msg != "") { - Local err = Exception::Error(New(req->err_msg).ToLocalChecked()); - Local argv[1] = { err }; - cb->Call(isolate->GetCurrentContext()->Global(), 1, argv); - } else { - Local argv[2] = { - v8::Null(isolate), - Local::New(isolate, New(req->result).ToLocalChecked()) - }; - cb->Call(isolate->GetCurrentContext()->Global(), 2, argv); - } - - if (try_catch.HasCaught()) { - Nan::FatalException(try_catch); - } - - req->callback.Reset(); -} - -/** - * StartBitcoind() - * bitcoind.start(callback) - * Start the bitcoind node with AppInit2() on a separate thread. - */ -NAN_METHOD(StartBitcoind) { - Isolate* isolate = info.GetIsolate(); - HandleScope scope(isolate); - - Local callback; - std::string datadir = std::string(""); - bool rpc = false; - bool testnet = false; - bool regtest = false; - bool txindex = false; - - if (info.Length() >= 2 && info[0]->IsObject() && info[1]->IsFunction()) { - Local options = Local::Cast(info[0]); - if (options->Get(New("datadir").ToLocalChecked())->IsString()) { - String::Utf8Value datadir_(options->Get(New("datadir").ToLocalChecked())->ToString()); - datadir = std::string(*datadir_); - } - if (options->Get(New("rpc").ToLocalChecked())->IsBoolean()) { - rpc = options->Get(New("rpc").ToLocalChecked())->ToBoolean()->IsTrue(); - } - if (options->Get(New("network").ToLocalChecked())->IsString()) { - String::Utf8Value network_(options->Get(New("network").ToLocalChecked())->ToString()); - std::string network = std::string(*network_); - if (network == "testnet") { - testnet = true; - } else if (network == "regtest") { - regtest = true; - } - } - if (options->Get(New("txindex").ToLocalChecked())->IsBoolean()) { - txindex = options->Get(New("txindex").ToLocalChecked())->ToBoolean()->IsTrue(); - } - callback = Local::Cast(info[1]); - } else if (info.Length() >= 2 - && (info[0]->IsUndefined() || info[0]->IsNull()) - && info[1]->IsFunction()) { - callback = Local::Cast(info[1]); - } else if (info.Length() >= 1 && info[0]->IsFunction()) { - callback = Local::Cast(info[0]); - } else { - return ThrowError( - "Usage: bitcoind.start(callback)"); - } - - // - // Run bitcoind's StartNode() on a separate thread. - // - - async_node_data *req = new async_node_data(); - req->err_msg = std::string(""); - req->result = std::string(""); - req->datadir = datadir; - req->rpc = rpc; - req->testnet = testnet; - req->regtest = regtest; - req->txindex = txindex; - - req->isolate = isolate; - req->callback.Reset(isolate, callback); - req->req.data = req; - - int status = uv_queue_work(uv_default_loop(), - &req->req, async_start_node, - (uv_after_work_cb)async_start_node_after); - - assert(status == 0); - - info.GetReturnValue().Set(Null()); -} - -/** - * async_start_node() - * Call start_node() and start all our boost threads. - */ - -static void -async_start_node(uv_work_t *req) { - async_node_data *data = reinterpret_cast(req->data); - if (data->datadir != "") { - g_data_dir = (char *)data->datadir.c_str(); - } else { - g_data_dir = (char *)malloc(sizeof(char) * 512); - snprintf(g_data_dir, sizeof(char) * 512, "%s/.bitcoind.js", getenv("HOME")); - } - g_rpc = (bool)data->rpc; - g_testnet = (bool)data->testnet; - g_regtest = (bool)data->regtest; - g_txindex = (bool)data->txindex; - tcgetattr(STDIN_FILENO, &orig_termios); - start_node(); - data->result = std::string("bitcoind opened."); -} - -/** - * async_start_node_after() - * Execute our callback. - */ - -static void -async_start_node_after(uv_work_t *r) { - async_node_data *req = reinterpret_cast(r->data); - Isolate* isolate = req->isolate; - HandleScope scope(isolate); - - Nan::TryCatch try_catch; - Local cb = Local::New(isolate, req->callback); - - if (req->err_msg != "") { - Local err = Exception::Error(New(req->err_msg).ToLocalChecked()); - Local argv[1] = { err }; - cb->Call(isolate->GetCurrentContext()->Global(), 1, argv); - } else { - Local argv[2] = { - v8::Null(isolate), - Local::New(isolate, New(req->result).ToLocalChecked()) - }; - cb->Call(isolate->GetCurrentContext()->Global(), 2, argv); - } - - if (try_catch.HasCaught()) { - Nan::FatalException(try_catch); - } - - req->callback.Reset(); -} - -/** - * start_node(void) - * Start AppInit2() on a separate thread, wait for - * Unfortunately, we need to wait for the initialization - * to unhook the signal handlers so we can use them - * from node.js in javascript. - */ - -static int -start_node(void) { - SetupEnvironment(); - - noui_connect(); - - new boost::thread(boost::bind(&start_node_thread)); - return 0; -} - -static void -start_node_thread(void) { - CScheduler scheduler; - - // Workaround for AppInit2() arg parsing. Not ideal, but it works. - int argc = 0; - char **argv = (char **)malloc((4 + 1) * sizeof(char **)); - - argv[argc] = (char *)"bitcoind"; - argc++; - - if (g_data_dir) { - const int argl = 9 + strlen(g_data_dir) + 1; - char *arg = (char *)malloc(sizeof(char) * argl); - int w = snprintf(arg, argl, "-datadir=%s", g_data_dir); - if (w >= 10 && w <= argl) { - arg[w] = '\0'; - argv[argc] = arg; - argc++; - } else { - if (set_cooked()) { - fprintf(stderr, "bitcoind.js: Bad -datadir value.\n"); - } - } - } - - if (g_rpc) { - argv[argc] = (char *)"-server"; - argc++; - } - - if (g_testnet) { - argv[argc] = (char *)"-testnet"; - argc++; - } - - if (g_regtest) { - argv[argc] = (char *)"-regtest"; - argc++; - } - - argv[argc] = (char *)"-txindex"; - argc++; - - argv[argc] = NULL; - - bool fRet = false; - try { - ParseParameters((const int)argc, (const char **)argv); - - if (!boost::filesystem::is_directory(GetDataDir(false))) { - if (set_cooked()) { - fprintf(stderr, - "bitcoind.js: Specified data directory \"%s\" does not exist.\n", - mapArgs["-datadir"].c_str()); - } - shutdown_complete = true; - _exit(1); - return; - } - - try { - ReadConfigFile(mapArgs, mapMultiArgs); - } catch(std::exception &e) { - if (set_cooked()) { - fprintf(stderr, - "bitcoind.js: Error reading configuration file: %s\n", e.what()); - } - shutdown_complete = true; - _exit(1); - return; - } - - if (!SelectParamsFromCommandLine()) { - if (set_cooked()) { - fprintf(stderr, - "bitcoind.js: Invalid combination of -regtest and -testnet.\n"); - } - shutdown_complete = true; - _exit(1); - return; - } - - CreatePidFile(GetPidFile(), getpid()); - - fRet = AppInit2(threadGroup, scheduler); - - } catch (std::exception& e) { - if (set_cooked()) { - fprintf(stderr, "bitcoind.js: AppInit2(): std::exception\n"); - } - } catch (...) { - if (set_cooked()) { - fprintf(stderr, "bitcoind.js: AppInit2(): other exception\n"); - } - } - - if (!fRet) - { - threadGroup.interrupt_all(); - } else { - WaitForShutdown(&threadGroup); - } - Shutdown(); - shutdown_complete = true; - -} - -/** - * StopBitcoind() - * bitcoind.stop(callback) - */ - -NAN_METHOD(StopBitcoind) { - Isolate* isolate = info.GetIsolate(); - HandleScope scope(isolate); - - if (info.Length() < 1 || !info[0]->IsFunction()) { - return ThrowError( - "Usage: bitcoind.stop(callback)"); - } - - Local callback = Local::Cast(info[0]); - - // - // Run bitcoind's StartShutdown() on a separate thread. - // - - async_node_data *req = new async_node_data(); - req->err_msg = std::string(""); - req->result = std::string(""); - req->callback.Reset(isolate, callback); - req->req.data = req; - req->isolate = isolate; - - int status = uv_queue_work(uv_default_loop(), - &req->req, async_stop_node, - (uv_after_work_cb)async_stop_node_after); - - assert(status == 0); - info.GetReturnValue().Set(Null()); - -} - -/** - * async_stop_node() - * Call StartShutdown() to join the boost threads, which will call Shutdown() - * and set shutdown_complete to true to notify the main node.js thread. - */ - -static void -async_stop_node(uv_work_t *req) { - async_node_data *data = reinterpret_cast(req->data); - - StartShutdown(); - - while(!shutdown_complete) { - usleep(1E6); - } - data->result = std::string("bitcoind shutdown."); -} - -/** - * async_stop_node_after() - * Execute our callback. - */ - -static void -async_stop_node_after(uv_work_t *r) { - async_node_data* req = reinterpret_cast(r->data); - Isolate* isolate = req->isolate; - HandleScope scope(isolate); - - Nan::TryCatch try_catch; - Local cb = Local::New(isolate, req->callback); - - if (req->err_msg != "") { - Local err = Exception::Error(New(req->err_msg).ToLocalChecked()); - Local argv[1] = { err }; - cb->Call(isolate->GetCurrentContext()->Global(), 1, argv); - } else { - Local argv[2] = { - Local::New(isolate, Null()), - Local::New(isolate, New(req->result).ToLocalChecked()) - }; - cb->Call(isolate->GetCurrentContext()->Global(), 2, argv); - } - - if (try_catch.HasCaught()) { - Nan::FatalException(try_catch); - } - req->callback.Reset(); -} - -/** - * GetBlock() - * bitcoind.getBlock([blockhash,blockheight], callback) - * Read any block from disk asynchronously. - */ - -NAN_METHOD(GetBlock) { - Isolate* isolate = info.GetIsolate(); - HandleScope scope(isolate); - if (info.Length() < 2 - || (!info[0]->IsString() && !info[0]->IsNumber()) - || !info[1]->IsFunction()) { - return ThrowError( - "Usage: bitcoind.getBlock([blockhash,blockheight], callback)"); - } - - async_block_data *req = new async_block_data(); - - if (info[0]->IsNumber()) { - int64_t height = info[0]->IntegerValue(); - req->err_msg = std::string(""); - req->height = height; - } else { - std::string hash = *Nan::Utf8String(info[0]); - req->err_msg = std::string(""); - req->hash = uint256S(hash); - req->height = -1; - } - - Local callback = Local::Cast(info[1]); - req->req.data = req; - req->isolate = isolate; - req->callback.Reset(isolate, callback); - - int status = uv_queue_work(uv_default_loop(), - &req->req, async_get_block, - (uv_after_work_cb)async_get_block_after); - - assert(status == 0); - - info.GetReturnValue().Set(Null()); -} - -static void -async_get_block(uv_work_t *req) { - async_block_data* data = reinterpret_cast(req->data); - - CBlockIndex* pblockindex; - - if (data->height != -1) { - pblockindex = chainActive[data->height]; - if (pblockindex == NULL) { - data->err_msg = std::string("Block not found."); - return; - } - } else { - if (mapBlockIndex.count(data->hash) == 0) { - data->err_msg = std::string("Block not found."); - return; - } else { - pblockindex = mapBlockIndex[data->hash]; - } - } - - const CDiskBlockPos& pos = pblockindex->GetBlockPos(); - - // We can read directly from the file, and pass that, we don't need to - // deserialize the entire block only for it to then be serialized - // and then deserialized again in JavaScript - - // Open history file to read - CAutoFile filein(OpenBlockFile(pos, true), SER_DISK, CLIENT_VERSION); - if (filein.IsNull()) { - data->err_msg = std::string("ReadBlockFromDisk: OpenBlockFile failed"); - return; - } - - // Get the actual file, seeked position and rewind a uint32_t - FILE* blockFile = filein.release(); - long int filePos = ftell(blockFile); - fseek(blockFile, filePos - sizeof(uint32_t), SEEK_SET); - - // Read the size of the block - uint32_t size = 0; - fread(&size, sizeof(uint32_t), 1, blockFile); - - // Read block - char* buffer = (char *)malloc(sizeof(char) * size); - fread((void *)buffer, sizeof(char), size, blockFile); - fclose(blockFile); - - data->buffer = buffer; - data->size = size; - data->cblock_index = pblockindex; - -} - -static void -async_get_block_after(uv_work_t *r) { - async_block_data* req = reinterpret_cast(r->data); - Isolate *isolate = req->isolate; - HandleScope scope(isolate); - - Nan::TryCatch try_catch; - Local cb = Local::New(isolate, req->callback); - - if (req->err_msg != "") { - Local err = Exception::Error(New(req->err_msg).ToLocalChecked()); - Local argv[1] = { err }; - cb->Call(isolate->GetCurrentContext()->Global(), 1, argv); - } else { - - Nan::MaybeLocal rawNodeBuffer = Nan::NewBuffer(req->buffer, req->size); - - Local argv[2] = { - Local::New(isolate, Null()), - rawNodeBuffer.ToLocalChecked() - }; - cb->Call(isolate->GetCurrentContext()->Global(), 2, argv); - } - - if (try_catch.HasCaught()) { - Nan::FatalException(try_catch); - } - - req->callback.Reset(); -} - -/** - * GetTransaction() - * bitcoind.getTransaction(txid, queryMempool, callback) - * Read any transaction from disk asynchronously. - */ - -NAN_METHOD(GetTransaction) { - Isolate* isolate = info.GetIsolate(); - HandleScope scope(isolate); - if (info.Length() < 3 - || !info[0]->IsString() - || !info[1]->IsBoolean() - || !info[2]->IsFunction()) { - return ThrowError( - "Usage: daemon.getTransaction(txid, queryMempool, callback)"); - } - - std::string txid = *Nan::Utf8String(info[0]); - - bool queryMempool = info[1]->BooleanValue(); - Local callback = Local::Cast(info[2]); - - async_tx_data *req = new async_tx_data(); - - req->err_msg = std::string(""); - req->txid = txid; - req->queryMempool = queryMempool; - req->isolate = isolate; - req->req.data = req; - req->callback.Reset(isolate, callback); - - int status = uv_queue_work(uv_default_loop(), - &req->req, async_get_tx, - (uv_after_work_cb)async_get_tx_after); - - assert(status == 0); - - info.GetReturnValue().Set(Null()); -} - -static void -async_get_tx(uv_work_t *req) { - async_tx_data* data = reinterpret_cast(req->data); - - uint256 blockhash; - uint256 hash = uint256S(data->txid); - CTransaction ctx; - - if (data->queryMempool) { - LOCK(cs_main); - { - if (mempool.lookup(hash, ctx)) - { - data->ctx = ctx; - return; - } - } - } - - CDiskTxPos postx; - if (pblocktree->ReadTxIndex(hash, postx)) { - - CAutoFile file(OpenBlockFile(postx, true), SER_DISK, CLIENT_VERSION); - - if (file.IsNull()) { - data->err_msg = std::string("%s: OpenBlockFile failed", __func__); - return; - } - - const int HEADER_SIZE = sizeof(int32_t) + sizeof(uint32_t) * 3 + sizeof(char) * 64; - - try { - fseek(file.Get(), postx.nTxOffset + HEADER_SIZE, SEEK_CUR); - file >> ctx; - data->ctx = ctx; - } catch (const std::exception& e) { - data->err_msg = std::string("Deserialize or I/O error - %s", __func__); - return; - } - - } - -} - -static void -async_get_tx_after(uv_work_t *r) { - async_tx_data* req = reinterpret_cast(r->data); - Isolate* isolate = req->isolate; - HandleScope scope(isolate); - - CTransaction ctx = req->ctx; - Nan::TryCatch try_catch; - Local cb = Local::New(isolate, req->callback); - - if (req->err_msg != "") { - Local err = Exception::Error(New(req->err_msg).ToLocalChecked()); - Local argv[1] = { err }; - cb->Call(isolate->GetCurrentContext()->Global(), 1, argv); - } else { - - if (!ctx.IsNull()) { - CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); - ssTx << ctx; - std::string stx = ssTx.str(); - Nan::MaybeLocal result = Nan::CopyBuffer((char *)stx.c_str(), stx.size()); - Local argv[2] = { - Local::New(isolate, Null()), - result.ToLocalChecked() - }; - cb->Call(isolate->GetCurrentContext()->Global(), 2, argv); - - } else { - Local argv[2] = { - Local::New(isolate, Null()), - Local::New(isolate, Null()) - }; - cb->Call(isolate->GetCurrentContext()->Global(), 2, argv); - } - - } - - if (try_catch.HasCaught()) { - Nan::FatalException(try_catch); - } - - req->callback.Reset(); -} - -/** - * GetTransactionWithBlockInfo() - * bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback) - * Read any transaction from disk asynchronously with block timestamp and height. - */ - -NAN_METHOD(GetTransactionWithBlockInfo) { - Isolate* isolate = info.GetIsolate(); - HandleScope scope(isolate); - if (info.Length() < 3 - || !info[0]->IsString() - || !info[1]->IsBoolean() - || !info[2]->IsFunction()) { - return ThrowError( - "Usage: bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback)"); - } - - String::Utf8Value txid_(info[0]->ToString()); - bool queryMempool = info[1]->BooleanValue(); - Local callback = Local::Cast(info[2]); - - async_tx_data *req = new async_tx_data(); - - req->err_msg = std::string(""); - req->txid = std::string(""); - - std::string txid = std::string(*txid_); - - req->txid = txid; - req->queryMempool = queryMempool; - req->req.data = req; - req->isolate = isolate; - req->callback.Reset(isolate, callback); - - int status = uv_queue_work(uv_default_loop(), - &req->req, async_get_tx_and_info, - (uv_after_work_cb)async_get_tx_and_info_after); - - assert(status == 0); - - info.GetReturnValue().Set(Null()); -} - -static void -async_get_tx_and_info(uv_work_t *req) { - async_tx_data* data = reinterpret_cast(req->data); - - uint256 hash = uint256S(data->txid); - uint256 blockHash; - CTransaction ctx; - - if (data->queryMempool) { - LOCK(mempool.cs); - map::const_iterator i = mempool.mapTx.find(hash); - if (i != mempool.mapTx.end()) { - data->ctx = i->second.GetTx(); - data->nTime = i->second.GetTime(); - data->height = -1; - return; - } - } - - CDiskTxPos postx; - if (pblocktree->ReadTxIndex(hash, postx)) { - - CAutoFile file(OpenBlockFile(postx, true), SER_DISK, CLIENT_VERSION); - - if (file.IsNull()) { - data->err_msg = std::string("%s: OpenBlockFile failed", __func__); - return; - } - - CBlockHeader blockHeader; - - try { - // Read header first to get block timestamp and hash - file >> blockHeader; - blockHash = blockHeader.GetHash(); - data->blockHash = blockHash.GetHex(); - data->nTime = blockHeader.nTime; - fseek(file.Get(), postx.nTxOffset, SEEK_CUR); - file >> ctx; - data->ctx = ctx; - } catch (const std::exception& e) { - data->err_msg = std::string("Deserialize or I/O error - %s", __func__); - return; - } - - // get block height - CBlockIndex* blockIndex; - - if (mapBlockIndex.count(blockHash) == 0) { - data->height = -1; - } else { - blockIndex = mapBlockIndex[blockHash]; - if (!chainActive.Contains(blockIndex)) { - data->height = -1; - } else { - data->height = blockIndex->nHeight; - } - } - - } - -} - -static void -async_get_tx_and_info_after(uv_work_t *r) { - async_tx_data* req = reinterpret_cast(r->data); - Isolate* isolate = req->isolate; - HandleScope scope(isolate); - - CTransaction ctx = req->ctx; - Nan::TryCatch try_catch; - Local cb = Local::New(isolate, req->callback); - Local obj = New(); - - if (req->err_msg != "") { - Local err = Exception::Error(New(req->err_msg).ToLocalChecked()); - Local argv[1] = { err }; - cb->Call(isolate->GetCurrentContext()->Global(), 1, argv); - } else { - - CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); - ssTx << ctx; - std::string stx = ssTx.str(); - Nan::MaybeLocal rawNodeBuffer = Nan::CopyBuffer((char *)stx.c_str(), stx.size()); - - Nan::Set(obj, New("blockHash").ToLocalChecked(), New(req->blockHash).ToLocalChecked()); - Nan::Set(obj, New("height").ToLocalChecked(), New(req->height)); - Nan::Set(obj, New("timestamp").ToLocalChecked(), New(req->nTime)); - Nan::Set(obj, New("buffer").ToLocalChecked(), rawNodeBuffer.ToLocalChecked()); - - Local argv[2] = { - Local::New(isolate, Null()), - obj - }; - cb->Call(isolate->GetCurrentContext()->Global(), 2, argv); - } - if (try_catch.HasCaught()) { - Nan::FatalException(try_catch); - } - req->callback.Reset(); -} - -/** - * IsSpent() - * bitcoind.isSpent() - * Determine if an outpoint is spent - */ -NAN_METHOD(IsSpent) { - if (info.Length() > 2) { - return ThrowError( - "Usage: bitcoind.isSpent(txid, outputIndex)"); - } - - String::Utf8Value arg(info[0]->ToString()); - std::string argStr = std::string(*arg); - const uint256 txid = uint256S(argStr); - int outputIndex = info[1]->IntegerValue(); - - { - LOCK(mempool.cs); - CCoinsView dummy; - CCoinsViewCache view(&dummy); - - CCoinsViewMemPool viewMemPool(pcoinsTip, mempool); - view.SetBackend(viewMemPool); - - if (view.HaveCoins(txid)) { - const CCoins* coins = view.AccessCoins(txid); - if (coins && coins->IsAvailable(outputIndex)) { - info.GetReturnValue().Set(New(false)); - return; - } - } - } - info.GetReturnValue().Set(New(true)); -}; - -/** - * GetBlockIndex() - * bitcoind.getBlockIndex() - * Get index information about a block by hash including: - * - the total amount of work (expected number of hashes) in the chain up to - * and including this block. - * - the previous hash of the block - */ -NAN_METHOD(GetBlockIndex) { - Isolate* isolate = Isolate::GetCurrent(); - HandleScope scope(isolate); - - CBlockIndex* blockIndex; - - if (info[0]->IsNumber()) { - int64_t height = info[0]->IntegerValue(); - blockIndex = chainActive[height]; - - if (blockIndex == NULL) { - info.GetReturnValue().Set(Null()); - return; - } - - } else { - String::Utf8Value hash_(info[0]->ToString()); - std::string hashStr = std::string(*hash_); - uint256 hash = uint256S(hashStr); - if (mapBlockIndex.count(hash) == 0) { - info.GetReturnValue().Set(Null()); - } else { - blockIndex = mapBlockIndex[hash]; - } - } - - Local obj = New(); - - arith_uint256 cw = blockIndex->nChainWork; - CBlockIndex* prevBlockIndex = blockIndex->pprev; - if (&prevBlockIndex->phashBlock != 0) { - const uint256* prevHash = prevBlockIndex->phashBlock; - Nan::Set(obj, New("prevHash").ToLocalChecked(), New(prevHash->GetHex()).ToLocalChecked()); - } else { - Nan::Set(obj, New("prevHash").ToLocalChecked(), Null()); - } - - Nan::Set(obj, New("hash").ToLocalChecked(), New(blockIndex->phashBlock->GetHex()).ToLocalChecked()); - Nan::Set(obj, New("chainWork").ToLocalChecked(), New(cw.GetHex()).ToLocalChecked()); - - Nan::Set(obj, New("height").ToLocalChecked(), New(blockIndex->nHeight)); - - info.GetReturnValue().Set(obj); -}; - - -/** - * IsMainChain() - * bitcoind.isMainChain() - * - * @param {string} - block hash - * @returns {boolean} - True if the block is in the main chain. False if it is an orphan. - */ -NAN_METHOD(IsMainChain) { - Isolate* isolate = Isolate::GetCurrent(); - HandleScope scope(isolate); - - CBlockIndex* blockIndex; - - String::Utf8Value hash_(info[0]->ToString()); - std::string hashStr = std::string(*hash_); - uint256 hash = uint256S(hashStr); - if (mapBlockIndex.count(hash) == 0) { - info.GetReturnValue().Set(Null()); - } else { - blockIndex = mapBlockIndex[hash]; - } - - if (chainActive.Contains(blockIndex)) { - info.GetReturnValue().Set(New(true)); - } else { - info.GetReturnValue().Set(New(false)); - } -} - -/** - * GetInfo() - * bitcoind.getInfo() - * Get miscellaneous information - */ - -NAN_METHOD(GetInfo) { - if (info.Length() > 0) { - return ThrowError( - "Usage: bitcoind.getInfo()"); - } - - Local obj = New(); - - proxyType proxy; - GetProxy(NET_IPV4, proxy); - - Nan::Set(obj, New("version").ToLocalChecked(), New(CLIENT_VERSION)); - Nan::Set(obj, New("protocolversion").ToLocalChecked(), New(PROTOCOL_VERSION)); - Nan::Set(obj, New("blocks").ToLocalChecked(), New((int)chainActive.Height())->ToInt32()); - Nan::Set(obj, New("timeoffset").ToLocalChecked(), New(GetTimeOffset())); - Nan::Set(obj, New("connections").ToLocalChecked(), New((int)vNodes.size())->ToInt32()); - Nan::Set(obj, New("difficulty").ToLocalChecked(), New((double)GetDifficulty())); - Nan::Set(obj, New("testnet").ToLocalChecked(), New(Params().NetworkIDString() == "test")); - Nan::Set(obj, New("network").ToLocalChecked(), New(Params().NetworkIDString()).ToLocalChecked()); - Nan::Set(obj, New("relayfee").ToLocalChecked(), New(::minRelayTxFee.GetFeePerK())); // double - Nan::Set(obj, New("errors").ToLocalChecked(), New(GetWarnings("statusbar")).ToLocalChecked()); - - info.GetReturnValue().Set(obj); -} - -/** - * Estimate Fee - * @blocks {number} - The number of blocks until confirmed - */ - -NAN_METHOD(EstimateFee) { - Isolate* isolate = Isolate::GetCurrent(); - HandleScope scope(isolate); - - int nBlocks = info[0]->NumberValue(); - if (nBlocks < 1) { - nBlocks = 1; - } - - CFeeRate feeRate = mempool.estimateFee(nBlocks); - - if (feeRate == CFeeRate(0)) { - info.GetReturnValue().Set(New(-1.0)); - return; - } - - CAmount nFee = feeRate.GetFeePerK(); - - info.GetReturnValue().Set(New(nFee)); - -} - -/** - * Send Transaction - * bitcoind.sendTransaction() - * Will add a transaction to the mempool and broadcast to connected peers. - * @param {string} - The serialized hex string of the transaction. - * @param {boolean} - Skip absurdly high fee checks - */ -NAN_METHOD(SendTransaction) { - Isolate* isolate = Isolate::GetCurrent(); - HandleScope scope(isolate); - - LOCK(cs_main); - - // Decode the transaction - v8::String::Utf8Value param1(info[0]->ToString()); - std::string *input = new std::string(*param1); - CTransaction tx; - if (!DecodeHexTx(tx, *input)) { - return ThrowError("TX decode failed"); - } - uint256 hashTx = tx.GetHash(); - - // Skip absurdly high fee check - bool allowAbsurdFees = false; - if (info.Length() > 1) { - allowAbsurdFees = info[1]->BooleanValue(); - } - - CCoinsViewCache &view = *pcoinsTip; - const CCoins* existingCoins = view.AccessCoins(hashTx); - bool fHaveMempool = mempool.exists(hashTx); - bool fHaveChain = existingCoins && existingCoins->nHeight < 1000000000; - if (!fHaveMempool && !fHaveChain) { - CValidationState state; - bool fMissingInputs; - - // Attempt to add the transaction to the mempool - if (!AcceptToMemoryPool(mempool, state, tx, false, &fMissingInputs, !allowAbsurdFees)) { - if (state.IsInvalid()) { - return ThrowError((boost::lexical_cast(state.GetRejectCode()) + ": " + state.GetRejectReason()).c_str()); - } else { - if (fMissingInputs) { - return ThrowError("Missing inputs"); - } - return ThrowError(state.GetRejectReason().c_str()); - } - } - } else if (fHaveChain) { - return ThrowError("transaction already in block chain"); - } - - // Relay the transaction connect peers - RelayTransaction(tx); - - info.GetReturnValue().Set(Local::New(isolate, New(hashTx.GetHex()).ToLocalChecked())); -} - -/** - * GetMempoolTransactions - * bitcoind.getMempoolTransactions() - * Will return an array of transaction buffers. - */ -NAN_METHOD(GetMempoolTransactions) { - Isolate* isolate = info.GetIsolate(); - HandleScope scope(isolate); - - Local transactions = Array::New(isolate); - int arrayIndex = 0; - - { - LOCK(mempool.cs); - - // Iterate through the entire mempool - std::map mapTx = mempool.mapTx; - - for(std::map::iterator it = mapTx.begin(); - it != mapTx.end(); - it++) { - CTxMemPoolEntry entry = it->second; - const CTransaction tx = entry.GetTx(); - CDataStream dataStreamTx(SER_NETWORK, PROTOCOL_VERSION); - dataStreamTx << tx; - std::string txString = dataStreamTx.str(); - Nan::MaybeLocal txBuffer = Nan::CopyBuffer((char *)txString.c_str(), txString.size()); - transactions->Set(arrayIndex, txBuffer.ToLocalChecked()); - arrayIndex++; - } - } - - info.GetReturnValue().Set(transactions); - -} - -/** - * AddMempoolUncheckedTransaction - */ -NAN_METHOD(AddMempoolUncheckedTransaction) { - v8::String::Utf8Value param1(info[0]->ToString()); - std::string *input = new std::string(*param1); - - CTransaction tx; - if (!DecodeHexTx(tx, *input)) { - return ThrowError("could not decode tx"); - } - bool added = mempool.addUnchecked(tx.GetHash(), CTxMemPoolEntry(tx, 0, 0, 0.0, 1)); - info.GetReturnValue().Set(New(added)); - -} - -/** - * Helpers - */ - -static bool -set_cooked(void) { - uv_tty_t tty; - tty.mode = 1; - tty.orig_termios = orig_termios; - - if (!uv_tty_set_mode(&tty, 0)) { - printf("\x1b[H\x1b[J"); - return true; - } - - return false; -} - -/** - * Init() - * Initialize the singleton object known as bitcoind. - */ -NAN_MODULE_INIT(init) { - Nan::Set(target, New("start").ToLocalChecked(), GetFunction(New(StartBitcoind)).ToLocalChecked()); - Nan::Set(target, New("onBlocksReady").ToLocalChecked(), GetFunction(New(OnBlocksReady)).ToLocalChecked()); - Nan::Set(target, New("onTipUpdate").ToLocalChecked(), GetFunction(New(OnTipUpdate)).ToLocalChecked()); - Nan::Set(target, New("stop").ToLocalChecked(), GetFunction(New(StopBitcoind)).ToLocalChecked()); - Nan::Set(target, New("getBlock").ToLocalChecked(), GetFunction(New(GetBlock)).ToLocalChecked()); - Nan::Set(target, New("getTransaction").ToLocalChecked(), GetFunction(New(GetTransaction)).ToLocalChecked()); - Nan::Set(target, New("getTransactionWithBlockInfo").ToLocalChecked(), GetFunction(New(GetTransactionWithBlockInfo)).ToLocalChecked()); - Nan::Set(target, New("getInfo").ToLocalChecked(), GetFunction(New(GetInfo)).ToLocalChecked()); - Nan::Set(target, New("isSpent").ToLocalChecked(), GetFunction(New(IsSpent)).ToLocalChecked()); - Nan::Set(target, New("getBlockIndex").ToLocalChecked(), GetFunction(New(GetBlockIndex)).ToLocalChecked()); - Nan::Set(target, New("isMainChain").ToLocalChecked(), GetFunction(New(IsMainChain)).ToLocalChecked()); - Nan::Set(target, New("getMempoolTransactions").ToLocalChecked(), GetFunction(New(GetMempoolTransactions)).ToLocalChecked()); - Nan::Set(target, New("addMempoolUncheckedTransaction").ToLocalChecked(), GetFunction(New(AddMempoolUncheckedTransaction)).ToLocalChecked()); - Nan::Set(target, New("sendTransaction").ToLocalChecked(), GetFunction(New(SendTransaction)).ToLocalChecked()); - Nan::Set(target, New("estimateFee").ToLocalChecked(), GetFunction(New(EstimateFee)).ToLocalChecked()); - Nan::Set(target, New("startTxMon").ToLocalChecked(), GetFunction(New(StartTxMon)).ToLocalChecked()); - Nan::Set(target, New("startTxMonLeave").ToLocalChecked(), GetFunction(New(StartTxMonLeave)).ToLocalChecked()); - Nan::Set(target, New("syncPercentage").ToLocalChecked(), GetFunction(New(SyncPercentage)).ToLocalChecked()); - Nan::Set(target, New("isSynced").ToLocalChecked(), GetFunction(New(IsSynced)).ToLocalChecked()); - Nan::Set(target, New("getBestBlockHash").ToLocalChecked(), GetFunction(New(GetBestBlockHash)).ToLocalChecked()); - Nan::Set(target, New("getNextBlockHash").ToLocalChecked(), GetFunction(New(GetNextBlockHash)).ToLocalChecked()); -} - -NODE_MODULE(libbitcoind, init); diff --git a/src/libbitcoind.h b/src/libbitcoind.h deleted file mode 100644 index 90d2ca97b..000000000 --- a/src/libbitcoind.h +++ /dev/null @@ -1,19 +0,0 @@ -#include "main.h" -#include "addrman.h" -#include "alert.h" -#include "base58.h" -#include "init.h" -#include "noui.h" -#include "rpcserver.h" -#include "txdb.h" -#include -#include -#include -#include "nan.h" -#include "scheduler.h" -#include "core_io.h" -#include "script/bitcoinconsensus.h" -#include "consensus/validation.h" -#ifdef ENABLE_WALLET -#include "wallet/wallet.h" -#endif diff --git a/test/bin/get-tarball-name.js b/test/bin/get-tarball-name.js deleted file mode 100644 index 6f8bdbde8..000000000 --- a/test/bin/get-tarball-name.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var path = require('path'); -var getTarballName = require('../../bin/get-tarball-name'); -var execSync = require('child_process').execSync; - -describe('#getTarballName', function() { - it('will return the expected tarball name', function() { - var name = getTarballName(); - var version = require(path.resolve(__dirname + '../../../package.json')).version; - var platform = process.platform; - var arch = execSync(path.resolve(__dirname) + '/../../bin/variables.sh arch'); - var abi = process.versions.modules; - var expected = 'libbitcoind-' + version + '-node' + abi + '-' + platform + '-' + arch + '.tgz'; - name.should.equal(expected); - }); -}); diff --git a/test/bus.integration.js b/test/bus.integration.js index 98bc8ab53..42a27d844 100644 --- a/test/bus.integration.js +++ b/test/bus.integration.js @@ -1,12 +1,16 @@ +'use strict'; + +var sinon = require('sinon'); var Service = require('../lib/service'); var BitcoreNode = require('../lib/node'); var util = require('util'); -var EventEmitter = require('events').EventEmitter; var should = require('chai').should(); +var index = require('../lib'); +var log = index.log; var TestService = function(options) { this.node = options.node; -} +}; util.inherits(TestService, Service); TestService.dependencies = []; @@ -40,6 +44,14 @@ TestService.prototype.unsubscribe = function(name, emitter) { describe('Bus Functionality', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('should subscribe to testEvent', function(done) { var node = new BitcoreNode({ datadir: './', diff --git a/test/data/bitcoin.conf b/test/data/bitcoin.conf index 475d225d7..353387fac 100644 --- a/test/data/bitcoin.conf +++ b/test/data/bitcoin.conf @@ -1,17 +1,23 @@ #testnet=1 #irc=0 -#upnp=0 +upnp=0 server=1 whitelist=127.0.0.1 txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +dbcache=8192 +checkblocks=144 +maxuploadtarget=1024 +zmqpubrawtx=tcp://127.0.0.1:28332 +zmqpubhashblock=tcp://127.0.0.1:28332 -# listen on different ports port=20000 +rpcport=50001 rpcallowip=127.0.0.1 rpcuser=bitcoin rpcpassword=local321 - - diff --git a/test/data/default.bitcoin.conf b/test/data/default.bitcoin.conf new file mode 100644 index 000000000..3665db542 --- /dev/null +++ b/test/data/default.bitcoin.conf @@ -0,0 +1,12 @@ +server=1 +whitelist=127.0.0.1 +txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +zmqpubrawtx=tcp://127.0.0.1:28332 +zmqpubhashblock=tcp://127.0.0.1:28332 +rpcallowip=127.0.0.1 +rpcuser=bitcoin +rpcpassword=local321 +uacomment=bitcore diff --git a/test/index.unit.js b/test/index.unit.js deleted file mode 100644 index c2190441a..000000000 --- a/test/index.unit.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var index = require('..'); - -describe('Index', function() { - describe('#nodeVersionCheck', function() { - it('will throw informative error message with incompatible Node.js version 4.1.2', function() { - (function() { - index.nodeVersionCheck('4.1.2', '>=0.12.0 <1'); - }).should.throw('Node.js version'); - }); - it('will throw informative error message with incompatible Node.js version 0.10.40', function() { - (function() { - index.nodeVersionCheck('4.1.2', '>=0.12.0 <1'); - }).should.throw('Node.js version'); - }); - }); -}); diff --git a/test/logger.unit.js b/test/logger.unit.js new file mode 100644 index 000000000..755228145 --- /dev/null +++ b/test/logger.unit.js @@ -0,0 +1,83 @@ +'use strict'; + +var sinon = require('sinon'); +var chai = require('chai'); +var should = chai.should(); +var Logger = require('../lib/logger'); + +describe('Logger', function() { + var sandbox = sinon.sandbox.create(); + afterEach(function() { + sandbox.restore(); + }); + + it('will instatiate without options', function() { + var logger = new Logger(); + should.exist(logger); + logger.formatting.should.equal(true); + }); + + it('will instatiate with formatting option', function() { + var logger = new Logger({ + formatting: false + }); + logger.formatting.should.equal(false); + var logger2 = new Logger({ + formatting: true + }); + logger2.formatting.should.equal(true); + }); + + it('will log with formatting', function() { + var logger = new Logger({formatting: true}); + + sandbox.stub(console, 'info'); + logger.info('Test info log'); + console.info.callCount.should.equal(1); + console.info.restore(); + + sandbox.stub(console, 'error'); + logger.error(new Error('Test error log')); + console.error.callCount.should.equal(1); + console.error.restore(); + + sandbox.stub(console, 'log'); + logger.debug('Test debug log'); + console.log.callCount.should.equal(1); + console.log.restore(); + + sandbox.stub(console, 'warn'); + logger.warn('Test warn log'); + console.warn.callCount.should.equal(1); + console.warn.restore(); + }); + + it('will log without formatting', function() { + var logger = new Logger({formatting: false}); + + sandbox.stub(console, 'info'); + logger.info('Test info log'); + console.info.callCount.should.equal(1); + should.not.exist(console.info.args[0][0].match(/^\[/)); + console.info.restore(); + + sandbox.stub(console, 'error'); + logger.error(new Error('Test error log')); + console.error.callCount.should.equal(1); + console.error.args[0][0].should.be.instanceof(Error); + console.error.restore(); + + sandbox.stub(console, 'log'); + logger.debug('Test debug log'); + console.log.callCount.should.equal(1); + should.equal(console.log.args[0][0].match(/^\[/), null); + console.log.restore(); + + sandbox.stub(console, 'warn'); + logger.warn('Test warn log'); + console.warn.callCount.should.equal(1); + should.equal(console.warn.args[0][0].match(/^\[/), null); + console.warn.restore(); + }); + +}); diff --git a/test/node.unit.js b/test/node.unit.js index 1e843fe04..dc7b4a3eb 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -7,12 +7,12 @@ var Networks = bitcore.Networks; var proxyquire = require('proxyquire'); var util = require('util'); var BaseService = require('../lib/service'); +var index = require('../lib'); +var log = index.log; describe('Bitcore Node', function() { - var baseConfig = { - datadir: 'testdir' - }; + var baseConfig = {}; var Node; @@ -34,7 +34,6 @@ describe('Bitcore Node', function() { }); it('will set properties', function() { var config = { - datadir: 'testdir', services: [ { name: 'test1', @@ -49,11 +48,15 @@ describe('Bitcore Node', function() { node._unloadedServices[0].name.should.equal('test1'); node._unloadedServices[0].module.should.equal(TestService); node.network.should.equal(Networks.defaultNetwork); + var node2 = TestNode(config); + node2._unloadedServices.length.should.equal(1); + node2._unloadedServices[0].name.should.equal('test1'); + node2._unloadedServices[0].module.should.equal(TestService); + node2.network.should.equal(Networks.defaultNetwork); }); it('will set network to testnet', function() { var config = { network: 'testnet', - datadir: 'testdir', services: [ { name: 'test1', @@ -69,7 +72,6 @@ describe('Bitcore Node', function() { it('will set network to regtest', function() { var config = { network: 'regtest', - datadir: 'testdir', services: [ { name: 'test1', @@ -84,6 +86,26 @@ describe('Bitcore Node', function() { should.exist(regtest); node.network.should.equal(regtest); }); + it('will be able to disable log formatting', function() { + var config = { + network: 'regtest', + services: [ + { + name: 'test1', + module: TestService + } + ], + formatLogs: false + }; + var TestNode = proxyquire('../lib/node', {}); + var node = new TestNode(config); + node.log.formatting.should.equal(false); + + var TestNode = proxyquire('../lib/node', {}); + config.formatLogs = true; + var node2 = new TestNode(config); + node2.log.formatting.should.equal(true); + }); }); describe('#openBus', function() { @@ -92,6 +114,11 @@ describe('Bitcore Node', function() { var bus = node.openBus(); bus.node.should.equal(node); }); + it('will use remoteAddress config option', function() { + var node = new Node(baseConfig); + var bus = node.openBus({remoteAddress: '127.0.0.1'}); + bus.remoteAddress.should.equal('127.0.0.1'); + }); }); describe('#getAllAPIMethods', function() { @@ -171,12 +198,20 @@ describe('Bitcore Node', function() { }); describe('#_startService', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); it('will instantiate an instance and load api methods', function() { var node = new Node(baseConfig); function TestService() {} util.inherits(TestService, BaseService); TestService.prototype.start = sinon.stub().callsArg(0); - TestService.prototype.getData = function() {}; + var getData = sinon.stub(); + TestService.prototype.getData = getData; TestService.prototype.getAPIMethods = function() { return [ ['getData', this, this.getData, 1] @@ -194,6 +229,8 @@ describe('Bitcore Node', function() { TestService.prototype.start.callCount.should.equal(1); should.exist(node.services.testservice); should.exist(node.getData); + node.getData(); + getData.callCount.should.equal(1); }); }); it('will give an error from start', function() { @@ -213,6 +250,13 @@ describe('Bitcore Node', function() { }); describe('#start', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); it('will call start for each service', function(done) { var node = new Node(baseConfig); @@ -301,7 +345,38 @@ describe('Bitcore Node', function() { }); }); + describe('#getNetworkName', function() { + afterEach(function() { + bitcore.Networks.disableRegtest(); + }); + it('it will return the network name for livenet', function() { + var node = new Node(baseConfig); + node.getNetworkName().should.equal('livenet'); + }); + it('it will return the network name for testnet', function() { + var baseConfig = { + network: 'testnet' + }; + var node = new Node(baseConfig); + node.getNetworkName().should.equal('testnet'); + }); + it('it will return the network for regtest', function() { + var baseConfig = { + network: 'regtest' + }; + var node = new Node(baseConfig); + node.getNetworkName().should.equal('regtest'); + }); + }); + describe('#stop', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); it('will call stop for each service', function(done) { var node = new Node(baseConfig); function TestService() {} diff --git a/test/scaffold/default-base-config.integration.js b/test/scaffold/default-base-config.integration.js index 5b09b3cd4..b622c742f 100644 --- a/test/scaffold/default-base-config.integration.js +++ b/test/scaffold/default-base-config.integration.js @@ -1,6 +1,7 @@ 'use strict'; var should = require('chai').should(); +var path = require('path'); var defaultBaseConfig = require('../../lib/scaffold/default-base-config'); describe('#defaultBaseConfig', function() { @@ -9,29 +10,19 @@ describe('#defaultBaseConfig', function() { var home = process.env.HOME; var info = defaultBaseConfig(); info.path.should.equal(cwd); - info.config.datadir.should.equal(home + '/.bitcoin'); info.config.network.should.equal('livenet'); info.config.port.should.equal(3001); - info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); + info.config.services.should.deep.equal(['bitcoind', 'web']); + var bitcoind = info.config.servicesConfig.bitcoind; + bitcoind.spawn.datadir.should.equal(home + '/.bitcoin'); + bitcoind.spawn.exec.should.equal(path.resolve(__dirname, '../../bin/bitcoind')); }); it('be able to specify a network', function() { - var cwd = process.cwd(); - var home = process.env.HOME; var info = defaultBaseConfig({network: 'testnet'}); - info.path.should.equal(cwd); - info.config.datadir.should.equal(home + '/.bitcoin'); info.config.network.should.equal('testnet'); - info.config.port.should.equal(3001); - info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); }); it('be able to specify a datadir', function() { - var cwd = process.cwd(); - var home = process.env.HOME; var info = defaultBaseConfig({datadir: './data2', network: 'testnet'}); - info.path.should.equal(cwd); - info.config.datadir.should.equal('./data2'); - info.config.network.should.equal('testnet'); - info.config.port.should.equal(3001); - info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); + info.config.servicesConfig.bitcoind.spawn.datadir.should.equal('./data2'); }); }); diff --git a/test/scaffold/default-config.integration.js b/test/scaffold/default-config.integration.js index cbe00816f..83e674e5a 100644 --- a/test/scaffold/default-config.integration.js +++ b/test/scaffold/default-config.integration.js @@ -1,21 +1,29 @@ 'use strict'; +var path = require('path'); var should = require('chai').should(); var sinon = require('sinon'); var proxyquire = require('proxyquire'); describe('#defaultConfig', function() { + var expectedExecPath = path.resolve(__dirname, '../../bin/bitcoind'); + it('will return expected configuration', function() { var config = JSON.stringify({ - datadir: process.env.HOME + '/.bitcore/data', network: 'livenet', port: 3001, services: [ 'bitcoind', - 'db', - 'address', 'web' - ] + ], + servicesConfig: { + bitcoind: { + spawn: { + datadir: process.env.HOME + '/.bitcore/data', + exec: expectedExecPath + } + } + } }, null, 2); var defaultConfig = proxyquire('../../lib/scaffold/default-config', { fs: { @@ -32,28 +40,35 @@ describe('#defaultConfig', function() { sync: sinon.stub() } }); - var cwd = process.cwd(); var home = process.env.HOME; var info = defaultConfig(); info.path.should.equal(home + '/.bitcore'); - info.config.datadir.should.equal(home + '/.bitcore/data'); info.config.network.should.equal('livenet'); info.config.port.should.equal(3001); - info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); + info.config.services.should.deep.equal(['bitcoind', 'web']); + var bitcoind = info.config.servicesConfig.bitcoind; + should.exist(bitcoind); + bitcoind.spawn.datadir.should.equal(home + '/.bitcore/data'); + bitcoind.spawn.exec.should.equal(expectedExecPath); }); it('will include additional services', function() { var config = JSON.stringify({ - datadir: process.env.HOME + '/.bitcore/data', network: 'livenet', port: 3001, services: [ 'bitcoind', - 'db', - 'address', 'web', 'insight-api', 'insight-ui' - ] + ], + servicesConfig: { + bitcoind: { + spawn: { + datadir: process.env.HOME + '/.bitcore/data', + exec: expectedExecPath + } + } + } }, null, 2); var defaultConfig = proxyquire('../../lib/scaffold/default-config', { fs: { @@ -75,16 +90,17 @@ describe('#defaultConfig', function() { additionalServices: ['insight-api', 'insight-ui'] }); info.path.should.equal(home + '/.bitcore'); - info.config.datadir.should.equal(home + '/.bitcore/data'); info.config.network.should.equal('livenet'); info.config.port.should.equal(3001); info.config.services.should.deep.equal([ 'bitcoind', - 'db', - 'address', 'web', 'insight-api', 'insight-ui' ]); + var bitcoind = info.config.servicesConfig.bitcoind; + should.exist(bitcoind); + bitcoind.spawn.datadir.should.equal(home + '/.bitcore/data'); + bitcoind.spawn.exec.should.equal(expectedExecPath); }); }); diff --git a/test/scaffold/start.integration.js b/test/scaffold/start.integration.js index 2374d163c..887572b6e 100644 --- a/test/scaffold/start.integration.js +++ b/test/scaffold/start.integration.js @@ -3,19 +3,33 @@ var should = require('chai').should(); var sinon = require('sinon'); var proxyquire = require('proxyquire'); -var AddressService = require('../../lib/services/address'); +var BitcoinService = require('../../lib/services/bitcoind'); +var index = require('../../lib'); +var log = index.log; describe('#start', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'error'); + }); + afterEach(function() { + sandbox.restore(); + }); + describe('will dynamically create a node from a configuration', function() { it('require each bitcore-node service with default config', function(done) { var node; var TestNode = function(options) { options.services[0].should.deep.equal({ - name: 'address', - module: AddressService, - config: {} + name: 'bitcoind', + module: BitcoinService, + config: { + spawn: { + datadir: './data' + } + } }); }; TestNode.prototype.start = sinon.stub().callsArg(0); @@ -28,13 +42,21 @@ describe('#start', function() { '../node': TestNode }); + starttest.registerExitHandlers = sinon.stub(); + node = starttest({ path: __dirname, config: { services: [ - 'address' + 'bitcoind' ], - datadir: './data' + servicesConfig: { + bitcoind: { + spawn: { + datadir: './data' + } + } + } } }); node.should.be.instanceof(TestNode); @@ -51,11 +73,13 @@ describe('#start', function() { '../node': TestNode }); starttest.cleanShutdown = sinon.stub(); + starttest.registerExitHandlers = sinon.stub(); + starttest({ path: __dirname, config: { services: [], - datadir: './testdir' + servicesConfig: {} } }); setImmediate(function() { @@ -67,10 +91,13 @@ describe('#start', function() { var node; var TestNode = function(options) { options.services[0].should.deep.equal({ - name: 'address', - module: AddressService, + name: 'bitcoind', + module: BitcoinService, config: { - param: 'test' + param: 'test', + spawn: { + datadir: './data' + } } }); }; @@ -83,19 +110,23 @@ describe('#start', function() { var starttest = proxyquire('../../lib/scaffold/start', { '../node': TestNode }); + starttest.registerExitHandlers = sinon.stub(); node = starttest({ path: __dirname, config: { services: [ - 'address' + 'bitcoind' ], servicesConfig: { - 'address': { - param: 'test' + 'bitcoind': { + param: 'test', + spawn: { + datadir: './data' + } } }, - datadir: './data' + } }); node.should.be.instanceof(TestNode); diff --git a/test/scaffold/start.unit.js b/test/scaffold/start.unit.js index c955cbf95..efc062999 100644 --- a/test/scaffold/start.unit.js +++ b/test/scaffold/start.unit.js @@ -8,6 +8,35 @@ var proxyquire = require('proxyquire'); var start = require('../../lib/scaffold/start'); describe('#start', function() { + describe('#checkConfigVersion2', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(console, 'warn'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will give true with "datadir" at root', function() { + var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2; + var v2 = checkConfigVersion2({datadir: '/home/user/.bitcore/data', services: []}); + v2.should.equal(true); + }); + it('will give true with "address" service enabled', function() { + var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2; + var v2 = checkConfigVersion2({services: ['address']}); + v2.should.equal(true); + }); + it('will give true with "db" service enabled', function() { + var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2; + var v2 = checkConfigVersion2({services: ['db']}); + v2.should.equal(true); + }); + it('will give false without "datadir" at root and "address", "db" services disabled', function() { + var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2; + var v2 = checkConfigVersion2({services: []}); + v2.should.equal(false); + }); + }); describe('#setupServices', function() { var cwd = process.cwd(); var setupServices = proxyquire('../../lib/scaffold/start', {}).setupServices; @@ -96,34 +125,6 @@ describe('#start', function() { }).should.throw('Could not load service'); }); }); - describe('#registerSyncHandlers', function() { - it('will log the sync status at an interval', function(done) { - var log = { - info: sinon.stub() - }; - var registerSyncHandlers = proxyquire('../../lib/scaffold/start', { - '../': { - log: log - } - }).registerSyncHandlers; - var node = new EventEmitter(); - node.services = { - db: new EventEmitter() - }; - node.services.db.tip = { - hash: 'hash', - __height: 10 - }; - registerSyncHandlers(node, 10); - node.emit('ready'); - node.services.db.emit('addblock'); - setTimeout(function() { - node.emit('synced'); - log.info.callCount.should.be.within(3, 4); - done(); - }, 35); - }); - }); describe('#cleanShutdown', function() { it('will call node stop and process exit', function() { var log = { @@ -212,110 +213,6 @@ describe('#start', function() { }); }); }); - describe('#spawnChildProcess', function() { - - it('should build the appropriate arguments to spawn a child process', function() { - var child = { - unref: function() {} - }; - var _process = { - exit: function() {}, - env: { - __bitcore_node: false - }, - argv: [ - 'node', - 'bitcore-node' - ], - cwd: function(){return ''}, - pid: 999, - execPath: '/tmp' - }; - var fd = {}; - var spawn = sinon.stub().returns(child); - var openSync = sinon.stub().returns(fd); - var spawnChildProcess = proxyquire('../../lib/scaffold/start', { - fs: { - openSync: openSync - }, - child_process: { - spawn: spawn - } - }).spawnChildProcess; - - spawnChildProcess('/tmp', _process); - - spawn.callCount.should.equal(1); - spawn.args[0][0].should.equal(_process.execPath); - var expected = [].concat(_process.argv); - expected.shift(); - spawn.args[0][1].should.deep.equal(expected); - var cp_opt = { - stdio: ['ignore', fd, fd], - env: _process.env, - cwd: '', - detached: true - }; - spawn.args[0][2].should.deep.equal(cp_opt); - openSync.callCount.should.equal(1); - openSync.args[0][0].should.equal('/tmp/bitcore-node.log'); - openSync.args[0][1].should.equal('a+'); - }); - it('should not spawn a new child process if there is already a daemon running', function() { - var _process = { - exit: function() {}, - env: { - __bitcore_node: true - }, - argv: [ - 'node', - 'bitcore-node' - ], - cwd: 'cwd', - pid: 999, - execPath: '/tmp' - }; - var spawnChildProcess = proxyquire('../../lib/scaffold/start', {}).spawnChildProcess; - spawnChildProcess('/tmp', _process).should.equal(999); - }); - }); - describe('daemon', function() { - var sandbox; - var spawn; - var setup; - var registerSync; - var registerExit; - var start = require('../../lib/scaffold/start'); - var options = { - config: { - datadir: '/tmp', - daemon: true - } - } - beforeEach(function() { - sandbox = sinon.sandbox.create(); - spawn = sandbox.stub(start, 'spawnChildProcess', function() {}); - setup = sandbox.stub(start, 'setupServices', function() {}); - registerSync = sandbox.stub(start, 'registerSyncHandlers', function() {}); - registerExit = sandbox.stub(start, 'registerExitHandlers', function() {}); - }); - afterEach(function() { - sandbox.restore(); - }); - it('call spawnChildProcess if there is a config option to do so', function() { - start(options); - registerSync.callCount.should.equal(1); - registerExit.callCount.should.equal(1); - spawn.callCount.should.equal(1); - }); - it('not call spawnChildProcess if there is not an option to do so', function() { - options.config.daemon = false; - start(options); - registerSync.callCount.should.equal(1); - registerExit.callCount.should.equal(1); - spawn.callCount.should.equal(0); - }); - }); describe('#registerExitHandlers', function() { var stub; var registerExitHandlers = require('../../lib/scaffold/start').registerExitHandlers; diff --git a/test/services/address/encoding.unit.js b/test/services/address/encoding.unit.js deleted file mode 100644 index e5ba73761..000000000 --- a/test/services/address/encoding.unit.js +++ /dev/null @@ -1,103 +0,0 @@ -'use strict'; - -var chai = require('chai'); -var should = chai.should(); -var sinon = require('sinon'); -var bitcorenode = require('../../../'); -var bitcore = require('bitcore-lib'); -var Address = bitcore.Address; -var Script = bitcore.Script; -var AddressService = bitcorenode.services.Address; -var Networks = bitcore.Networks; -var encoding = require('../../../lib/services/address/encoding'); - -var mockdb = { -}; - -var mocknode = { - network: Networks.testnet, - datadir: 'testdir', - db: mockdb, - services: { - bitcoind: { - on: sinon.stub() - } - } -}; - -describe('Address Service Encoding', function() { - - describe('#encodeSpentIndexSyncKey', function() { - it('will encode to 36 bytes (string)', function() { - var txidBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); - var key = encoding.encodeSpentIndexSyncKey(txidBuffer, 12); - key.length.should.equal(36); - }); - it('will be able to decode encoded value', function() { - var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'; - var txidBuffer = new Buffer(txid, 'hex'); - var key = encoding.encodeSpentIndexSyncKey(txidBuffer, 12); - var keyBuffer = new Buffer(key, 'binary'); - keyBuffer.slice(0, 32).toString('hex').should.equal(txid); - var outputIndex = keyBuffer.readUInt32BE(32); - outputIndex.should.equal(12); - }); - }); - - describe('#_encodeInputKeyMap/#_decodeInputKeyMap roundtrip', function() { - var encoded; - var outputTxIdBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); - it('encode key', function() { - encoded = encoding.encodeInputKeyMap(outputTxIdBuffer, 13); - }); - it('decode key', function() { - var key = encoding.decodeInputKeyMap(encoded); - key.outputTxId.toString('hex').should.equal(outputTxIdBuffer.toString('hex')); - key.outputIndex.should.equal(13); - }); - }); - - describe('#_encodeInputValueMap/#_decodeInputValueMap roundtrip', function() { - var encoded; - var inputTxIdBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); - it('encode key', function() { - encoded = encoding.encodeInputValueMap(inputTxIdBuffer, 7); - }); - it('decode key', function() { - var key = encoding.decodeInputValueMap(encoded); - key.inputTxId.toString('hex').should.equal(inputTxIdBuffer.toString('hex')); - key.inputIndex.should.equal(7); - }); - }); - - - describe('#extractAddressInfoFromScript', function() { - it('pay-to-publickey', function() { - var pubkey = new bitcore.PublicKey('022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da'); - var script = Script.buildPublicKeyOut(pubkey); - var info = encoding.extractAddressInfoFromScript(script, Networks.livenet); - info.addressType.should.equal(Address.PayToPublicKeyHash); - info.hashBuffer.toString('hex').should.equal('9674af7395592ec5d91573aa8d6557de55f60147'); - }); - it('pay-to-publickeyhash', function() { - var script = Script('OP_DUP OP_HASH160 20 0x0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG'); - var info = encoding.extractAddressInfoFromScript(script, Networks.livenet); - info.addressType.should.equal(Address.PayToPublicKeyHash); - info.hashBuffer.toString('hex').should.equal('0000000000000000000000000000000000000000'); - }); - it('pay-to-scripthash', function() { - var script = Script('OP_HASH160 20 0x0000000000000000000000000000000000000000 OP_EQUAL'); - var info = encoding.extractAddressInfoFromScript(script, Networks.livenet); - info.addressType.should.equal(Address.PayToScriptHash); - info.hashBuffer.toString('hex').should.equal('0000000000000000000000000000000000000000'); - }); - it('non-address script type', function() { - var buf = new Buffer(40); - buf.fill(0); - var script = Script('OP_RETURN 40 0x' + buf.toString('hex')); - var info = encoding.extractAddressInfoFromScript(script, Networks.livenet); - info.should.equal(false); - }); - }); - -}); diff --git a/test/services/address/history.unit.js b/test/services/address/history.unit.js deleted file mode 100644 index 2b6df06cd..000000000 --- a/test/services/address/history.unit.js +++ /dev/null @@ -1,544 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var sinon = require('sinon'); -var bitcore = require('bitcore-lib'); -var Transaction = require('../../../lib/transaction'); -var AddressHistory = require('../../../lib/services/address/history'); - -describe('Address Service History', function() { - - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - - describe('@constructor', function() { - it('will construct a new instance', function() { - var node = {}; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history.should.be.instanceof(AddressHistory); - history.node.should.equal(node); - history.options.should.equal(options); - history.addresses.should.equal(addresses); - history.detailedArray.should.deep.equal([]); - }); - it('will set addresses an array if only sent a string', function() { - var history = new AddressHistory({ - node: {}, - options: {}, - addresses: address - }); - history.addresses.should.deep.equal([address]); - }); - }); - - describe('#get', function() { - it('will give an error if length of addresses is too long', function(done) { - var node = {}; - var options = {}; - var addresses = []; - for (var i = 0; i < 101; i++) { - addresses.push(address); - } - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history.maxAddressesQuery = 100; - history.get(function(err) { - should.exist(err); - err.message.match(/Maximum/); - done(); - }); - }); - it('give error from getAddressSummary with one address', function(done) { - var node = { - services: { - address: { - getAddressSummary: sinon.stub().callsArgWith(2, new Error('test')) - } - } - }; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history.get(function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - it('give error from getAddressSummary with multiple addresses', function(done) { - var node = { - services: { - address: { - getAddressSummary: sinon.stub().callsArgWith(2, new Error('test2')) - } - } - }; - var options = {}; - var addresses = [address, address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history.get(function(err) { - should.exist(err); - err.message.should.equal('test2'); - done(); - }); - }); - it('will query get address summary directly with one address', function(done) { - var txids = []; - var summary = { - txids: txids - }; - var node = { - services: { - address: { - getAddressSummary: sinon.stub().callsArgWith(2, null, summary) - } - } - }; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history._mergeAndSortTxids = sinon.stub(); - history._paginateWithDetails = sinon.stub().callsArg(1); - history.get(function() { - history.node.services.address.getAddressSummary.callCount.should.equal(1); - history.node.services.address.getAddressSummary.args[0][0].should.equal(address); - history.node.services.address.getAddressSummary.args[0][1].should.deep.equal({ - noBalance: true - }); - history._paginateWithDetails.callCount.should.equal(1); - history._paginateWithDetails.args[0][0].should.equal(txids); - history._mergeAndSortTxids.callCount.should.equal(0); - done(); - }); - }); - it('will merge multiple summaries with multiple addresses', function(done) { - var txids = []; - var summary = { - txids: txids - }; - var node = { - services: { - address: { - getAddressSummary: sinon.stub().callsArgWith(2, null, summary) - } - } - }; - var options = {}; - var addresses = [address, address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history._mergeAndSortTxids = sinon.stub().returns(txids); - history._paginateWithDetails = sinon.stub().callsArg(1); - history.get(function() { - history.node.services.address.getAddressSummary.callCount.should.equal(2); - history.node.services.address.getAddressSummary.args[0][0].should.equal(address); - history.node.services.address.getAddressSummary.args[0][1].should.deep.equal({ - fullTxList: true, - noBalance: true - }); - history._paginateWithDetails.callCount.should.equal(1); - history._paginateWithDetails.args[0][0].should.equal(txids); - history._mergeAndSortTxids.callCount.should.equal(1); - done(); - }); - }); - }); - - describe('#_paginateWithDetails', function() { - it('slice txids based on "from" and "to" (3 to 30)', function() { - var node = {}; - var options = { - from: 3, - to: 30 - }; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - sinon.stub(history, 'getDetailedInfo', function(txid, next) { - this.detailedArray.push(txid); - next(); - }); - history._paginateWithDetails(txids, function(err, result) { - result.totalCount.should.equal(11); - result.items.should.deep.equal([7, 6, 5, 4, 3, 2, 1, 0]); - }); - }); - it('slice txids based on "from" and "to" (0 to 3)', function() { - var node = {}; - var options = { - from: 0, - to: 3 - }; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - sinon.stub(history, 'getDetailedInfo', function(txid, next) { - this.detailedArray.push(txid); - next(); - }); - history._paginateWithDetails(txids, function(err, result) { - result.totalCount.should.equal(11); - result.items.should.deep.equal([10, 9, 8]); - }); - }); - it('will given an error if the full details is too long', function() { - var node = {}; - var options = { - from: 0, - to: 3 - }; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - sinon.stub(history, 'getDetailedInfo', function(txid, next) { - this.detailedArray.push(txid); - next(); - }); - history.maxHistoryQueryLength = 1; - history._paginateWithDetails(txids, function(err) { - should.exist(err); - err.message.match(/Maximum/); - }); - }); - it('will give full result without pagination options', function() { - var node = {}; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - sinon.stub(history, 'getDetailedInfo', function(txid, next) { - this.detailedArray.push(txid); - next(); - }); - history._paginateWithDetails(txids, function(err, result) { - result.totalCount.should.equal(11); - result.items.should.deep.equal([10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); - }); - }); - }); - - describe('#_mergeAndSortTxids', function() { - it('will merge and sort multiple summaries', function() { - var summaries = [ - { - totalReceived: 10000000, - totalSpent: 0, - balance: 10000000, - appearances: 2, - unconfirmedBalance: 20000000, - unconfirmedAppearances: 2, - appearanceIds: { - '56fafeb01961831b926558d040c246b97709fd700adcaa916541270583e8e579': 154, - 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce': 120 - }, - unconfirmedAppearanceIds: { - 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681': 1452898347406, - 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693': 1452898331964 - } - }, - { - totalReceived: 59990000, - totalSpent: 0, - balance: 49990000, - appearances: 3, - unconfirmedBalance: 1000000, - unconfirmedAppearances: 3, - appearanceIds: { - 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2': 156, - 'f3c1ba3ef86a0420d6102e40e2cfc8682632ab95d09d86a27f5d466b9fa9da47': 152, - 'f637384e9f81f18767ea50e00bce58fc9848b6588a1130529eebba22a410155f': 151 - }, - unconfirmedAppearanceIds: { - 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345': 1452897902377, - 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a': 1452897971363, - 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9': 1452897923107 - } - } - ]; - var node = {}; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = history._mergeAndSortTxids(summaries); - txids.should.deep.equal([ - 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', - 'f637384e9f81f18767ea50e00bce58fc9848b6588a1130529eebba22a410155f', - 'f3c1ba3ef86a0420d6102e40e2cfc8682632ab95d09d86a27f5d466b9fa9da47', - '56fafeb01961831b926558d040c246b97709fd700adcaa916541270583e8e579', - 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2', - 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345', - 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9', - 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a', - 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693', - 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681' - ]); - }); - }); - - describe('#getDetailedInfo', function() { - it('will add additional information to existing this.transactions', function(done) { - var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; - var tx = { - populateInputs: sinon.stub().callsArg(2), - __height: 20, - __timestamp: 1453134151, - isCoinbase: sinon.stub().returns(false), - getFee: sinon.stub().returns(1000) - }; - var history = new AddressHistory({ - node: { - services: { - db: { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, tx), - tip: { - __height: 300 - } - } - } - }, - options: {}, - addresses: [] - }); - history.getAddressDetailsForTransaction = sinon.stub().returns({ - addresses: {}, - satoshis: 1000, - }); - history.getDetailedInfo(txid, function(err) { - if (err) { - throw err; - } - history.node.services.db.getTransactionWithBlockInfo.callCount.should.equal(1); - done(); - }); - }); - it('will handle error from getTransactionFromBlock', function(done) { - var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; - var history = new AddressHistory({ - node: { - services: { - db: { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('test')), - } - } - }, - options: {}, - addresses: [] - }); - history.getDetailedInfo(txid, function(err) { - err.message.should.equal('test'); - done(); - }); - }); - it('will handle error from populateInputs', function(done) { - var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; - var history = new AddressHistory({ - node: { - services: { - db: { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, { - populateInputs: sinon.stub().callsArgWith(2, new Error('test')) - }), - } - } - }, - options: {}, - addresses: [] - }); - history.getDetailedInfo(txid, function(err) { - err.message.should.equal('test'); - done(); - }); - }); - it('will set this.transactions with correct information', function(done) { - // block #314159 - // txid 30169e8bf78bc27c4014a7aba3862c60e2e3cce19e52f1909c8255e4b7b3174e - // outputIndex 1 - var txAddress = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; - var txString = '0100000001a08ee59fcd5d86fa170abb6d925d62d5c5c476359681b70877c04f270c4ef246000000008a47304402203fb9b476bb0c37c9b9ed5784ebd67ae589492be11d4ae1612be29887e3e4ce750220741ef83781d1b3a5df8c66fa1957ad0398c733005310d7d9b1d8c2310ef4f74c0141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02b0a75fac000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac20badc02000000001976a914809dc14496f99b6deb722cf46d89d22f4beb8efd88ac00000000'; - var previousTxString = '010000000155532fad2869bb951b0bd646a546887f6ee668c4c0ee13bf3f1c4bce6d6e3ed9000000008c4930460221008540795f4ef79b1d2549c400c61155ca5abbf3089c84ad280e1ba6db2a31abce022100d7d162175483d51174d40bba722e721542c924202a0c2970b07e680b51f3a0670141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02f0af3caf000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac80969800000000001976a91421277e65777760d1f3c7c982ba14ed8f934f005888ac00000000'; - var transaction = new Transaction(); - var previousTransaction = new Transaction(); - previousTransaction.fromString(previousTxString); - var previousTransactionTxid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; - transaction.fromString(txString); - var txid = transaction.hash; - transaction.__blockHash = '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45'; - transaction.__height = 314159; - transaction.__timestamp = 1407292005; - var history = new AddressHistory({ - node: { - services: { - db: { - tip: { - __height: 314159 - }, - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, transaction), - getTransaction: function(prevTxid, queryMempool, callback) { - prevTxid.should.equal(previousTransactionTxid); - setImmediate(function() { - callback(null, previousTransaction); - }); - } - } - } - }, - options: {}, - addresses: [txAddress] - }); - var transactionInfo = { - addresses: {}, - txid: txid, - timestamp: 1407292005, - satoshis: 48020000, - address: txAddress - }; - transactionInfo.addresses[txAddress] = {}; - transactionInfo.addresses[txAddress].outputIndexes = [1]; - transactionInfo.addresses[txAddress].inputIndexes = []; - history.getDetailedInfo(txid, function(err) { - if (err) { - throw err; - } - var info = history.detailedArray[0]; - info.addresses[txAddress].should.deep.equal({ - outputIndexes: [1], - inputIndexes: [] - }); - info.satoshis.should.equal(48020000); - info.height.should.equal(314159); - info.confirmations.should.equal(1); - info.timestamp.should.equal(1407292005); - info.fees.should.equal(20000); - info.tx.should.equal(transaction); - done(); - }); - }); - }); - - describe('#getAddressDetailsForTransaction', function() { - it('will calculate details for the transaction', function(done) { - /* jshint sub:true */ - var tx = bitcore.Transaction({ - 'hash': 'b12b3ae8489c5a566b629a3c62ce4c51c3870af550fb5dc77d715b669a91343c', - 'version': 1, - 'inputs': [ - { - 'prevTxId': 'a2b7ea824a92f4a4944686e67ec1001bc8785348b8c111c226f782084077b543', - 'outputIndex': 0, - 'sequenceNumber': 4294967295, - 'script': '47304402201b81c933297241960a57ae1b2952863b965ac8c9ec7466ff0b715712d27548d50220576e115b63864f003889443525f47c7cf0bc1e2b5108398da085b221f267ba2301210229766f1afa25ca499a51f8e01c292b0255a21a41bb6685564a1607a811ffe924', - 'scriptString': '71 0x304402201b81c933297241960a57ae1b2952863b965ac8c9ec7466ff0b715712d27548d50220576e115b63864f003889443525f47c7cf0bc1e2b5108398da085b221f267ba2301 33 0x0229766f1afa25ca499a51f8e01c292b0255a21a41bb6685564a1607a811ffe924', - 'output': { - 'satoshis': 1000000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - } - } - ], - 'outputs': [ - { - 'satoshis': 100000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - }, - { - 'satoshis': 200000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - }, - { - 'satoshis': 50000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - }, - { - 'satoshis': 300000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - }, - { - 'satoshis': 349990000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - } - ], - 'nLockTime': 0 - }); - var history = new AddressHistory({ - node: { - network: bitcore.Networks.testnet - }, - options: {}, - addresses: ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'] - }); - var details = history.getAddressDetailsForTransaction(tx); - should.exist(details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']); - details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].inputIndexes.should.deep.equal([0]); - details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].outputIndexes.should.deep.equal([ - 0, 1, 2, 3, 4 - ]); - details.satoshis.should.equal(-10000); - done(); - }); - }); - - describe('#getConfirmationsDetail', function() { - it('the correct confirmations when included in the tip', function(done) { - var history = new AddressHistory({ - node: { - services: { - db: { - tip: { - __height: 100 - } - } - } - }, - options: {}, - addresses: [] - }); - var transaction = { - __height: 100 - }; - history.getConfirmationsDetail(transaction).should.equal(1); - done(); - }); - }); -}); diff --git a/test/services/address/index.unit.js b/test/services/address/index.unit.js deleted file mode 100644 index 48bda199a..000000000 --- a/test/services/address/index.unit.js +++ /dev/null @@ -1,2676 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var sinon = require('sinon'); -var stream = require('stream'); -var levelup = require('levelup'); -var proxyquire = require('proxyquire'); -var bitcorenode = require('../../../'); -var AddressService = bitcorenode.services.Address; -var blockData = require('../../data/livenet-345003.json'); -var bitcore = require('bitcore-lib'); -var _ = bitcore.deps._; -var memdown = require('memdown'); -var leveldown = require('leveldown'); -var Networks = bitcore.Networks; -var EventEmitter = require('events').EventEmitter; -var errors = bitcorenode.errors; -var Transaction = require('../../../lib/transaction'); -var txData = require('../../data/transaction.json'); -var index = require('../../../lib'); -var log = index.log; -var constants = require('../../../lib/services/address/constants'); -var encoding = require('../../../lib/services/address/encoding'); - -var mockdb = { -}; - -var mocknode = { - network: Networks.testnet, - datadir: 'testdir', - db: mockdb, - services: { - bitcoind: { - on: sinon.stub() - } - } -}; - -describe('Address Service', function() { - var txBuf = new Buffer(txData[0], 'hex'); - - describe('@constructor', function() { - it('config to use memdown for mempool index', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.levelupStore.should.equal(memdown); - }); - it('config to use leveldown for mempool index', function() { - var am = new AddressService({ - node: mocknode - }); - am.levelupStore.should.equal(leveldown); - }); - }); - - describe('#start', function() { - it('will flush existing mempool', function(done) { - var leveldownmock = { - destroy: sinon.stub().callsArgWith(1, null) - }; - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(true) - }, - 'leveldown': leveldownmock, - 'levelup': sinon.stub().callsArgWith(2, null), - 'mkdirp': sinon.stub().callsArgWith(1, null) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function() { - leveldownmock.destroy.callCount.should.equal(1); - leveldownmock.destroy.args[0][0].should.equal('testdir/testnet3/bitcore-addressmempool.db'); - done(); - }); - }); - it('will mkdirp if directory does not exist', function(done) { - var leveldownmock = { - destroy: sinon.stub().callsArgWith(1, null) - }; - var mkdirpmock = sinon.stub().callsArgWith(1, null); - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(false) - }, - 'leveldown': leveldownmock, - 'levelup': sinon.stub().callsArgWith(2, null), - 'mkdirp': mkdirpmock - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function() { - mkdirpmock.callCount.should.equal(1); - mkdirpmock.args[0][0].should.equal('testdir/testnet3/bitcore-addressmempool.db'); - done(); - }); - }); - it('start levelup db for mempool', function(done) { - var levelupStub = sinon.stub().callsArg(2); - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(true) - }, - 'leveldown': { - destroy: sinon.stub().callsArgWith(1, null) - }, - 'levelup': levelupStub, - 'mkdirp': sinon.stub().callsArgWith(1, null) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function() { - levelupStub.callCount.should.equal(1); - var dbPath1 = levelupStub.args[0][0]; - dbPath1.should.equal('testdir/testnet3/bitcore-addressmempool.db'); - var options = levelupStub.args[0][1]; - options.db.should.equal(memdown); - options.keyEncoding.should.equal('binary'); - options.valueEncoding.should.equal('binary'); - options.fillCache.should.equal(false); - done(); - }); - }); - it('handle error from mkdirp', function(done) { - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(false) - }, - 'leveldown': { - destroy: sinon.stub().callsArgWith(1, null) - }, - 'levelup': sinon.stub().callsArgWith(2, null), - 'mkdirp': sinon.stub().callsArgWith(1, new Error('testerror')) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function(err) { - err.message.should.equal('testerror'); - done(); - }); - }); - it('handle error from levelup', function(done) { - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(false) - }, - 'leveldown': { - destroy: sinon.stub().callsArgWith(1, null) - }, - 'levelup': sinon.stub().callsArgWith(2, new Error('leveltesterror')), - 'mkdirp': sinon.stub().callsArgWith(1, null) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function(err) { - err.message.should.equal('leveltesterror'); - done(); - }); - }); - it('handle error from leveldown.destroy', function(done) { - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(true) - }, - 'leveldown': { - destroy: sinon.stub().callsArgWith(1, new Error('destroy')) - }, - 'levelup': sinon.stub().callsArgWith(2, null), - 'mkdirp': sinon.stub().callsArgWith(1, null) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function(err) { - err.message.should.equal('destroy'); - done(); - }); - }); - }); - - describe('#stop', function() { - it('will close mempool levelup', function(done) { - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - db: mockdb, - services: { - bitcoind: { - on: sinon.stub(), - removeListener: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.mempoolIndex = {}; - am.mempoolIndex.close = sinon.stub().callsArg(0); - am.stop(function() { - am.mempoolIndex.close.callCount.should.equal(1); - am.node.services.bitcoind.removeListener.callCount.should.equal(2); - done(); - }); - }); - }); - - describe('#_setMempoolIndexPath', function() { - it('should set the database path', function() { - var testnode = { - network: Networks.livenet, - datadir: process.env.HOME + '/.bitcoin', - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am._setMempoolIndexPath(); - am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/bitcore-addressmempool.db'); - }); - it('should load the db for testnet', function() { - var testnode = { - network: Networks.testnet, - datadir: process.env.HOME + '/.bitcoin', - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am._setMempoolIndexPath(); - am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/testnet3/bitcore-addressmempool.db'); - }); - it('error with unknown network', function() { - var testnode = { - network: 'unknown', - datadir: process.env.HOME + '/.bitcoin', - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - (function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }).should.throw('Unknown network'); - }); - it('should load the db with regtest', function() { - // Switch to use regtest - Networks.enableRegtest(); - var regtest = Networks.get('regtest'); - var testnode = { - network: regtest, - datadir: process.env.HOME + '/.bitcoin', - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-addressmempool.db'); - Networks.disableRegtest(); - }); - }); - - describe('#getAPIMethods', function() { - it('should return the correct methods', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var methods = am.getAPIMethods(); - methods.length.should.equal(7); - }); - }); - - describe('#getPublishEvents', function() { - it('will return an array of publish event objects', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.subscribe = sinon.spy(); - am.unsubscribe = sinon.spy(); - var events = am.getPublishEvents(); - - var callCount = 0; - function testName(event, name) { - event.name.should.equal(name); - event.scope.should.equal(am); - var emitter = new EventEmitter(); - var addresses = []; - event.subscribe(emitter, addresses); - am.subscribe.callCount.should.equal(callCount + 1); - am.subscribe.args[callCount][0].should.equal(name); - am.subscribe.args[callCount][1].should.equal(emitter); - am.subscribe.args[callCount][2].should.equal(addresses); - am.subscribe.thisValues[callCount].should.equal(am); - event.unsubscribe(emitter, addresses); - am.unsubscribe.callCount.should.equal(callCount + 1); - am.unsubscribe.args[callCount][0].should.equal(name); - am.unsubscribe.args[callCount][1].should.equal(emitter); - am.unsubscribe.args[callCount][2].should.equal(addresses); - am.unsubscribe.thisValues[callCount].should.equal(am); - callCount++; - } - events.forEach(function(event) { - testName(event, event.name); - }); - - }); - }); - - describe('#transactionOutputHandler', function() { - it('create a message for an address', function() { - var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); - var tx = bitcore.Transaction().fromBuffer(txBuf); - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.node.network = Networks.livenet; - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var addrObj = bitcore.Address(address); - var hashHex = addrObj.hashBuffer.toString('hex'); - var hashType = addrObj.type; - var messages = {}; - am.transactionOutputHandler(messages, tx, 0, true); - should.exist(messages[hashHex]); - var message = messages[hashHex]; - message.tx.should.equal(tx); - message.outputIndexes.should.deep.equal([0]); - message.addressInfo.hashBuffer.toString('hex').should.equal(hashHex); - message.addressInfo.addressType.should.equal(hashType); - message.addressInfo.hashHex.should.equal(hashHex); - message.rejected.should.equal(true); - }); - }); - - describe('#transactionHandler', function() { - it('will pass outputs to transactionOutputHandler and call transactionEventHandler and balanceEventHandler', function(done) { - var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); - var am1 = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var message = {}; - am1.transactionOutputHandler = function(messages) { - messages[address] = message; - }; - am1.transactionEventHandler = sinon.stub(); - am1.balanceEventHandler = sinon.stub(); - am1.transactionHandler({ - buffer: txBuf - }, function(err) { - if (err) { - throw err; - } - am1.transactionEventHandler.callCount.should.equal(1); - am1.balanceEventHandler.callCount.should.equal(1); - done(); - }); - - }); - }); - - describe('#blockHandler', function() { - var am; - var testBlock = bitcore.Block.fromString(blockData); - - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.node.network = Networks.livenet; - }); - - it('should create the correct operations when updating/adding outputs', function(done) { - var block = { - __height: 345003, - header: { - timestamp: 1424836934 - }, - transactions: testBlock.transactions.slice(0, 8) - }; - - am.blockHandler(block, true, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(151); - operations[0].type.should.equal('put'); - operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b0100000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); - operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac'); - operations[3].type.should.equal('put'); - operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e0100000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); - operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); - operations[4].type.should.equal('put'); - operations[4].key.toString('hex').should.equal('053d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); - operations[4].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); - operations[121].type.should.equal('put'); - operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c75280100000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); - operations[121].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac'); - done(); - }); - }); - it('should create the correct operations when removing outputs', function(done) { - var block = { - __height: 345003, - header: { - timestamp: 1424836934 - }, - transactions: testBlock.transactions.slice(0, 8) - }; - am.blockHandler(block, false, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(151); - operations[0].type.should.equal('del'); - operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b0100000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); - operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac'); - operations[3].type.should.equal('del'); - operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e0100000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); - operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); - operations[121].type.should.equal('del'); - operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c75280100000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); - operations[121].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac'); - done(); - }); - }); - it('should continue if output script is null', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode, - }); - - var block = { - __height: 345003, - header: { - timestamp: 1424836934 - }, - transactions: [ - { - id: '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', - inputs: [], - outputs: [ - { - script: null, - satoshis: 1000, - } - ], - isCoinbase: sinon.stub().returns(false) - } - ] - }; - - am.blockHandler(block, false, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(0); - done(); - }); - }); - it('will call event handlers', function() { - var testBlock = bitcore.Block.fromString(blockData); - var db = {}; - var testnode = { - datadir: 'testdir', - db: db, - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.transactionEventHandler = sinon.spy(); - am.balanceEventHandler = sinon.spy(); - - var block = { - __height: 345003, - header: { - timestamp: 1424836934 - }, - transactions: testBlock.transactions.slice(0, 8) - }; - - am.blockHandler( - block, - true, - function(err) { - if (err) { - throw err; - } - am.transactionEventHandler.callCount.should.equal(11); - am.balanceEventHandler.callCount.should.equal(11); - } - ); - }); - }); - - describe('#transactionEventHandler', function() { - it('will emit a transaction if there is a subscriber', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - am.subscriptions['address/transaction'] = {}; - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] = [emitter]; - var block = { - __height: 0, - timestamp: new Date() - }; - var tx = {}; - emitter.on('address/transaction', function(obj) { - obj.address.toString().should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - obj.tx.should.equal(tx); - obj.timestamp.should.equal(block.timestamp); - obj.height.should.equal(block.__height); - obj.outputIndexes.should.deep.equal([1]); - done(); - }); - am.transactionEventHandler({ - addressInfo: { - hashHex: address.hashBuffer.toString('hex'), - hashBuffer: address.hashBuffer, - addressType: address.type - }, - height: block.__height, - timestamp: block.timestamp, - outputIndexes: [1], - tx: tx - }); - }); - }); - - describe('#balanceEventHandler', function() { - it('will emit a balance if there is a subscriber', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] = [emitter]; - var block = {}; - var balance = 1000; - am.getBalance = sinon.stub().callsArgWith(2, null, balance); - emitter.on('address/balance', function(a, bal, b) { - a.toString().should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - bal.should.equal(balance); - b.should.equal(block); - done(); - }); - am.balanceEventHandler(block, { - hashHex: address.hashBuffer.toString('hex'), - hashBuffer: address.hashBuffer, - addressType: address.type - }); - }); - }); - - describe('#subscribe', function() { - it('will add emitters to the subscribers array (transaction)', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - var name = 'address/transaction'; - am.subscribe(name, emitter, [address]); - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter]); - - var address2 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - am.subscribe(name, emitter, [address2]); - am.subscriptions['address/transaction'][address2.hashBuffer.toString('hex')] - .should.deep.equal([emitter]); - - var emitter2 = new EventEmitter(); - am.subscribe(name, emitter2, [address]); - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter, emitter2]); - }); - it('will add an emitter to the subscribers array (balance)', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var name = 'address/balance'; - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - am.subscribe(name, emitter, [address]); - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter]); - - var address2 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - am.subscribe(name, emitter, [address2]); - am.subscriptions['address/balance'][address2.hashBuffer.toString('hex')] - .should.deep.equal([emitter]); - - var emitter2 = new EventEmitter(); - am.subscribe(name, emitter2, [address]); - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter, emitter2]); - }); - }); - - describe('#unsubscribe', function() { - it('will remove emitter from subscribers array (transaction)', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var emitter2 = new EventEmitter(); - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] = [emitter, emitter2]; - var name = 'address/transaction'; - am.unsubscribe(name, emitter, [address]); - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter2]); - }); - it('will remove emitter from subscribers array (balance)', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var emitter2 = new EventEmitter(); - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - var name = 'address/balance'; - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] = [emitter, emitter2]; - am.unsubscribe(name, emitter, [address]); - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter2]); - }); - it('should unsubscribe from all addresses if no addresses are specified', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var emitter2 = new EventEmitter(); - var address1 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - var hashHex1 = address1.hashBuffer.toString('hex'); - var address2 = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - var hashHex2 = address2.hashBuffer.toString('hex'); - am.subscriptions['address/balance'][hashHex1] = [emitter, emitter2]; - am.subscriptions['address/balance'][hashHex2] = [emitter2, emitter]; - am.unsubscribe('address/balance', emitter); - am.subscriptions['address/balance'][hashHex1].should.deep.equal([emitter2]); - am.subscriptions['address/balance'][hashHex2].should.deep.equal([emitter2]); - }); - }); - - describe('#getBalance', function() { - it('should sum up the unspent outputs', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var outputs = [ - {satoshis: 1000}, {satoshis: 2000}, {satoshis: 3000} - ]; - am.getUnspentOutputs = sinon.stub().callsArgWith(2, null, outputs); - am.getBalance('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N', false, function(err, balance) { - should.not.exist(err); - balance.should.equal(6000); - done(); - }); - }); - - it('will handle error from unspent outputs', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getUnspentOutputs = sinon.stub().callsArgWith(2, new Error('error')); - am.getBalance('someaddress', false, function(err) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - - }); - - describe('#createInputsStream', function() { - it('transform stream from buffer into object', function(done) { - var testnode = { - network: Networks.livenet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - tip: { - __height: 157 - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - addressService.createInputsDBStream = sinon.stub().returns(streamStub); - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createInputsStream(address, {}); - testStream.once('data', function(data) { - data.address.should.equal('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - data.hashType.should.equal('pubkeyhash'); - data.txid.should.equal('7b94e3c39386845ea383b8e726b20b5172ccd3ef9be008bbb133e3b63f07df72'); - data.inputIndex.should.equal(1); - data.height.should.equal(157); - data.confirmations.should.equal(1); - done(); - }); - streamStub.emit('data', { - key: new Buffer('030b2f0a0c31bfe0406b0ccc1381fdbe311946dadc01000000009d786cfeae288d74aaf9f51f215f9882e7bd7bc18af7a550683c4d7c6962f6372900000004', 'hex'), - value: new Buffer('7b94e3c39386845ea383b8e726b20b5172ccd3ef9be008bbb133e3b63f07df7200000001', 'hex') - }); - streamStub.emit('end'); - }); - }); - - describe('#createInputsDBStream', function() { - it('will stream all keys', function() { - var streamStub = sinon.stub().returns({}); - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - store: { - createReadStream: streamStub - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = {}; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createInputsDBStream(address, options); - should.exist(testStream); - streamStub.callCount.should.equal(1); - var expectedGt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; - // The expected "lt" value should be one value above the start value, due - // to the keys having additional data following it and can't be "equal". - var expectedLt = '03038a213afdfc551fc658e9a2a58a86e98d69b68701ffffffffff'; - streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); - streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); - }); - it('will stream keys based on a range of block heights', function() { - var streamStub = sinon.stub().returns({}); - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - store: { - createReadStream: streamStub - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = { - start: 1, - end: 0 - }; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createInputsDBStream(address, options); - should.exist(testStream); - streamStub.callCount.should.equal(1); - var expectedGt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; - // The expected "lt" value should be one value above the start value, due - // to the keys having additional data following it and can't be "equal". - var expectedLt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000002'; - streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); - streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); - }); - }); - - describe('#getInputs', function() { - var am; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - var db = { - tip: { - __height: 1 - } - }; - var testnode = { - network: Networks.livenet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }); - - it('will add mempool inputs on close', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var db = { - store: { - createReadStream: sinon.stub().returns(testStream) - }, - tip: { - __height: 10 - } - }; - var testnode = { - network: Networks.livenet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var args = { - start: 15, - end: 12, - queryMempool: true - }; - am._getInputsMempool = sinon.stub().callsArgWith(3, null, { - address: address, - hashType: 'pubkeyhash', - height: -1, - confirmations: 0 - }); - am.getInputs(address, args, function(err, inputs) { - should.not.exist(err); - inputs.length.should.equal(1); - inputs[0].address.should.equal(address); - inputs[0].height.should.equal(-1); - done(); - }); - testStream.push(null); - }); - it('will get inputs for an address and timestamp', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - start: 15, - end: 12, - queryMempool: true - }; - var createReadStreamCallCount = 0; - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, - hashTypeBuffer, new Buffer('000000000c', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, - hashTypeBuffer, new Buffer('0000000010', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am.node.services.bitcoind = { - getMempoolInputs: sinon.stub().returns([]) - }; - am._getInputsMempool = sinon.stub().callsArgWith(3, null, []); - am.getInputs(address, args, function(err, inputs) { - should.not.exist(err); - inputs.length.should.equal(1); - inputs[0].address.should.equal(address); - inputs[0].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); - inputs[0].inputIndex.should.equal(0); - inputs[0].height.should.equal(15); - done(); - }); - createReadStreamCallCount.should.equal(1); - var data = { - key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex') - }; - testStream.emit('data', data); - testStream.push(null); - }); - it('should get inputs for address', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - queryMempool: true - }; - var createReadStreamCallCount = 0; - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('0000000000', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('ffffffffff', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am.node.services.bitcoind = { - getMempoolInputs: sinon.stub().returns([]) - }; - am.getInputs(address, args, function(err, inputs) { - should.not.exist(err); - inputs.length.should.equal(1); - inputs[0].address.should.equal(address); - inputs[0].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); - inputs[0].inputIndex.should.equal(0); - inputs[0].height.should.equal(15); - done(); - }); - createReadStreamCallCount.should.equal(1); - var data = { - key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex') - }; - testStream.emit('data', data); - testStream.push(null); - }); - it('should give an error if the readstream has an error', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - am.node.services.db.store = { - createReadStream: sinon.stub().returns(testStream) - }; - - am.getInputs(address, {}, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - - testStream.emit('error', new Error('readstreamerror')); - setImmediate(function() { - testStream.push(null); - }); - }); - - }); - - describe('#_getInputsMempool', function() { - var am; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - var db = { - tip: { - __height: 1 - } - }; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }); - it('it will handle error', function(done) { - var testStream = new EventEmitter(); - am.mempoolIndex = {}; - am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - - am._getInputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - - testStream.emit('error', new Error('readstreamerror')); - setImmediate(function() { - testStream.emit('close'); - }); - }); - it('it will parse data', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - am.mempoolIndex = {}; - am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - - var nowTime = new Date().getTime(); - - am._getInputsMempool(address, hashBuffer, hashTypeBuffer, function(err, inputs) { - should.not.exist(err); - inputs.length.should.equal(1); - var input = inputs[0]; - input.address.should.equal(address); - input.txid.should.equal(txid); - input.hashType.should.equal('pubkeyhash'); - input.hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); - input.inputIndex.should.equal(5); - input.height.should.equal(-1); - input.confirmations.should.equal(0); - input.timestamp.should.equal(nowTime); - done(); - }); - - var txid = '5d32f0fff6871c377e00c16f48ebb5e89c723d0b9dd25f68fdda70c3392bee61'; - var inputIndex = 5; - var inputIndexBuffer = new Buffer(4); - var timestampBuffer = new Buffer(new Array(8)); - timestampBuffer.writeDoubleBE(nowTime); - inputIndexBuffer.writeUInt32BE(inputIndex); - var valueData = Buffer.concat([ - new Buffer(txid, 'hex'), - inputIndexBuffer, - timestampBuffer - ]); - // Note: key is not used currently - testStream.emit('data', { - value: valueData - }); - testStream.emit('close'); - }); - }); - - describe('#_getSpentMempool', function() { - it('will decode data from the database', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.mempoolIndex = {}; - var mempoolValue = Buffer.concat([ - new Buffer('85630d684f1f414264f88a31bddfc79dd0c00659330dcdc393b321c121f4078b', 'hex'), - new Buffer('00000003', 'hex') - ]); - am.mempoolIndex.get = sinon.stub().callsArgWith(1, null, mempoolValue); - var prevTxIdBuffer = new Buffer('e7888264d286be2da26b0a4dbd2fc5c9ece82b3e40e6791b137e4155b6da8981', 'hex'); - var outputIndex = 1; - var outputIndexBuffer = new Buffer('00000001', 'hex'); - var expectedKey = Buffer.concat([ - new Buffer('03', 'hex'), - prevTxIdBuffer, - outputIndexBuffer - ]).toString('hex'); - am._getSpentMempool(prevTxIdBuffer, outputIndex, function(err, value) { - if (err) { - throw err; - } - am.mempoolIndex.get.args[0][0].toString('hex').should.equal(expectedKey); - value.inputTxId.should.equal('85630d684f1f414264f88a31bddfc79dd0c00659330dcdc393b321c121f4078b'); - value.inputIndex.should.equal(3); - }); - }); - it('handle error from levelup', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.mempoolIndex = {}; - am.mempoolIndex.get = sinon.stub().callsArgWith(1, new Error('test')); - var prevTxIdBuffer = new Buffer('e7888264d286be2da26b0a4dbd2fc5c9ece82b3e40e6791b137e4155b6da8981', 'hex'); - var outputIndex = 1; - am._getSpentMempool(prevTxIdBuffer, outputIndex, function(err) { - err.message.should.equal('test'); - }); - }); - }); - - describe('#createOutputsStream', function() { - it('transform stream from buffer into object', function(done) { - var testnode = { - network: Networks.livenet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - tip: { - __height: 157 - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - addressService.createOutputsDBStream = sinon.stub().returns(streamStub); - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createOutputsStream(address, {}); - testStream.once('data', function(data) { - data.address.should.equal('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - data.hashType.should.equal('pubkeyhash'); - data.txid.should.equal('4078b72b09391f5146e2c564f5847d49b179f9946b253f780f65b140d46ef6f9'); - data.outputIndex.should.equal(2); - data.height.should.equal(157); - data.satoshis.should.equal(10000); - data.script.toString('hex').should.equal('76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'); - data.confirmations.should.equal(1); - done(); - }); - streamStub.emit('data', { - key: new Buffer('020b2f0a0c31bfe0406b0ccc1381fdbe311946dadc01000000009d4078b72b09391f5146e2c564f5847d49b179f9946b253f780f65b140d46ef6f900000002', 'hex'), - value: new Buffer('40c388000000000076a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac', 'hex') - }); - streamStub.emit('end'); - }); - }); - - describe('#createOutputsDBStream', function() { - it('will stream all keys', function() { - var streamStub = sinon.stub().returns({}); - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - store: { - createReadStream: streamStub - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = {}; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createOutputsDBStream(address, options); - should.exist(testStream); - streamStub.callCount.should.equal(1); - var expectedGt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; - // The expected "lt" value should be one value above the start value, due - // to the keys having additional data following it and can't be "equal". - var expectedLt = '02038a213afdfc551fc658e9a2a58a86e98d69b68701ffffffffff'; - streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); - streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); - }); - it('will stream keys based on a range of block heights', function() { - var streamStub = sinon.stub().returns({}); - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - store: { - createReadStream: streamStub - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = { - start: 1, - end: 0 - }; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createOutputsDBStream(address, options); - should.exist(testStream); - streamStub.callCount.should.equal(1); - var expectedGt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; - // The expected "lt" value should be one value above the start value, due - // to the keys having additional data following it and can't be "equal". - var expectedLt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000002'; - streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); - streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); - }); - }); - - describe('#getOutputs', function() { - var am; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var hashBuffer = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W').hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - var db = { - tip: { - __height: 1 - } - }; - var testnode = { - network: Networks.livenet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - var options = { - queryMempool: true - }; - - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }); - - it('will get outputs for an address and timestamp', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - start: 15, - end: 12, - queryMempool: true - }; - var createReadStreamCallCount = 0; - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); - am.getOutputs(address, args, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(1); - outputs[0].address.should.equal(address); - outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); - outputs[0].hashType.should.equal('pubkeyhash'); - outputs[0].hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); - outputs[0].outputIndex.should.equal(1); - outputs[0].satoshis.should.equal(4527773864); - outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); - outputs[0].height.should.equal(15); - done(); - }); - createReadStreamCallCount.should.equal(1); - var data = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') - }; - testStream.emit('data', data); - testStream.push(null); - }); - - it('should get outputs for an address', function(done) { - var readStream1 = new stream.Readable(); - readStream1._read = function() { /* do nothing */ }; - am.node.services.db.store = { - createReadStream: sinon.stub().returns(readStream1) - }; - - am._getOutputsMempool = sinon.stub().callsArgWith(3, null, [ - { - address: address, - height: -1, - hashType: 'pubkeyhash', - confirmations: 0, - txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371', - satoshis: 307627737, - script: '76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac', - outputIndex: 0 - } - ]); - - am.getOutputs(address, options, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(3); - outputs[0].address.should.equal(address); - outputs[0].hashType.should.equal('pubkeyhash'); - outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); - outputs[0].outputIndex.should.equal(1); - outputs[0].satoshis.should.equal(4527773864); - outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); - outputs[0].height.should.equal(345000); - outputs[1].address.should.equal(address); - outputs[1].hashType.should.equal('pubkeyhash'); - outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); - outputs[1].outputIndex.should.equal(2); - outputs[1].satoshis.should.equal(10000); - outputs[1].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); - outputs[1].height.should.equal(345004); - outputs[2].address.should.equal(address); - outputs[2].hashType.should.equal('pubkeyhash'); - outputs[2].txid.should.equal('aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371'); - outputs[2].script.should.equal('76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac'); - outputs[2].height.should.equal(-1); - outputs[2].confirmations.should.equal(0); - done(); - }); - - var data1 = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b6870100000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') - }; - - var data2 = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b6870100000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'), - value: new Buffer('40c388000000000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') - }; - - readStream1.emit('data', data1); - readStream1.emit('data', data2); - readStream1.push(null); - }); - - it('should give an error if the readstream has an error', function(done) { - var readStream2 = new stream.Readable(); - readStream2._read = function() { /* do nothing */ }; - am.node.services.db.store = { - createReadStream: sinon.stub().returns(readStream2) - }; - - am.getOutputs(address, options, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - - readStream2.emit('error', new Error('readstreamerror')); - setImmediate(function() { - readStream2.push(null); - }); - }); - - it('should print outputs for a p2sh address', function(done) { - // This address has the redeemScript 0x038a213afdfc551fc658e9a2a58a86e98d69b687, - // which is the same as the pkhash for the address 1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W. - // See https://github.com/bitpay/bitcore-node/issues/377 - var address = '321jRYeWBrLBWr2j1KYnAFGico3GUdd5q7'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.REDEEMSCRIPT; - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - start: 15, - end: 12, - queryMempool: true - }; - var createReadStreamCallCount = 0; - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); - am.getOutputs(address, args, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(1); - outputs[0].address.should.equal(address); - outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); - outputs[0].hashType.should.equal('scripthash'); - outputs[0].hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); - outputs[0].outputIndex.should.equal(1); - outputs[0].satoshis.should.equal(4527773864); - outputs[0].script.should.equal('a914038a213afdfc551fc658e9a2a58a86e98d69b68787'); - outputs[0].height.should.equal(15); - done(); - }); - createReadStreamCallCount.should.equal(1); - var data = { - // note '68702', '02' meaning p2sh redeemScript, not p2pkh - // value is also the p2sh script, not p2pkh - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68702000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('41f0de058a800000a914038a213afdfc551fc658e9a2a58a86e98d69b68787', 'hex') - }; - testStream.emit('data', data); - testStream.push(null); - }); - - it('should not print outputs for a p2pkh address, if the output was sent to a p2sh redeemScript', function(done) { - // This address has the redeemScript 0x038a213afdfc551fc658e9a2a58a86e98d69b687, - // which is the same as the pkhash for the address 1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W. - // See https://github.com/bitpay/bitcore-node/issues/377 - var address = '321jRYeWBrLBWr2j1KYnAFGico3GUdd5q7'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.REDEEMSCRIPT; - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - start: 15, - end: 12, - queryMempool: true - }; - var createReadStreamCallCount = 0; - - // Verifying that the db query is looking for a redeemScript, *not* a p2pkh - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); - am.getOutputs(address, args, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(0); - done(); - }); - createReadStreamCallCount.should.equal(1); - testStream.push(null); - }); - }); - - describe('#_getOutputsMempool', function() { - var am; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - var db = { - tip: { - __height: 1 - } - }; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }); - it('it will handle error', function(done) { - var testStream = new EventEmitter(); - am.mempoolIndex = {}; - am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - am._getOutputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - testStream.emit('error', new Error('readstreamerror')); - setImmediate(function() { - testStream.emit('close'); - }); - }); - it('it will parse data', function(done) { - var testStream = new EventEmitter(); - am.mempoolIndex = {}; - am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - - am._getOutputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { - if (err) { - throw err; - } - outputs.length.should.equal(1); - var output = outputs[0]; - output.address.should.equal(address); - output.hashType.should.equal('pubkeyhash'); - output.txid.should.equal(txid); - output.outputIndex.should.equal(outputIndex); - output.height.should.equal(-1); - output.satoshis.should.equal(3); - output.script.should.equal('ac'); - output.timestamp.should.equal(1452696715750); - output.confirmations.should.equal(0); - done(); - }); - - var txid = '5d32f0fff6871c377e00c16f48ebb5e89c723d0b9dd25f68fdda70c3392bee61'; - var txidBuffer = new Buffer(txid, 'hex'); - var outputIndex = 3; - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var keyData = Buffer.concat([ - constants.MEMPREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - txidBuffer, - outputIndexBuffer - ]); - - var valueData = Buffer.concat([ - new Buffer('4008000000000000', 'hex'), - new Buffer('427523b78c1e6000', 'hex'), - new Buffer('ac', 'hex') - ]); - - // Note: key is not used currently - testStream.emit('data', { - key: keyData, - value: valueData - }); - setImmediate(function() { - testStream.emit('close'); - }); - }); - }); - - describe('#getUnspentOutputs', function() { - it('should concatenate utxos for multiple addresses, even those with none found', function(done) { - var addresses = { - 'addr1': ['utxo1', 'utxo2'], - 'addr2': new errors.NoOutputs(), - 'addr3': ['utxo3'] - }; - - var db = {}; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { - var result = addresses[address]; - if(result instanceof Error) { - return callback(result); - } else { - return callback(null, result); - } - }; - - am.getUnspentOutputs(['addr1', 'addr2', 'addr3'], true, function(err, utxos) { - should.not.exist(err); - utxos.should.deep.equal(['utxo1', 'utxo2', 'utxo3']); - done(); - }); - }); - it('should give an error if an error occurred', function(done) { - var addresses = { - 'addr1': ['utxo1', 'utxo2'], - 'addr2': new Error('weird error'), - 'addr3': ['utxo3'] - }; - - var db = {}; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - db: db, - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { - var result = addresses[address]; - if(result instanceof Error) { - return callback(result); - } else { - return callback(null, result); - } - }; - - am.getUnspentOutputs(['addr1', 'addr2', 'addr3'], true, function(err, utxos) { - should.exist(err); - err.message.should.equal('weird error'); - done(); - }); - }); - - it('should also work for a single address', function(done) { - var addresses = { - 'addr1': ['utxo1', 'utxo2'], - 'addr2': new Error('weird error'), - 'addr3': ['utxo3'] - }; - - var db = {}; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - db: db, - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { - var result = addresses[address]; - if(result instanceof Error) { - return callback(result); - } else { - return callback(null, result); - } - }; - - am.getUnspentOutputs('addr1', true, function(err, utxos) { - should.not.exist(err); - utxos.should.deep.equal(['utxo1', 'utxo2']); - done(); - }); - }); - }); - - describe('#getUnspentOutputsForAddress', function() { - it('should filter out spent outputs', function(done) { - var outputs = [ - { - satoshis: 1000, - spent: false, - }, - { - satoshis: 2000, - spent: true - }, - { - satoshis: 3000, - spent: false - } - ]; - var i = 0; - - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getOutputs = sinon.stub().callsArgWith(2, null, outputs); - am.isUnspent = function(output, options, callback) { - callback(!outputs[i].spent); - i++; - }; - - am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(2); - outputs[0].satoshis.should.equal(1000); - outputs[1].satoshis.should.equal(3000); - done(); - }); - }); - it('should handle an error from getOutputs', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getOutputs = sinon.stub().callsArgWith(2, new Error('error')); - am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - it('should handle when there are no outputs', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getOutputs = sinon.stub().callsArgWith(2, null, []); - am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.exist(err); - err.should.be.instanceof(errors.NoOutputs); - outputs.length.should.equal(0); - done(); - }); - }); - }); - - describe('#isUnspent', function() { - var am; - - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - }); - - it('should give true when isSpent() gives false', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, false); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {}, function(unspent) { - unspent.should.equal(true); - done(); - }); - }); - - it('should give false when isSpent() gives true', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, true); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {},function(unspent) { - unspent.should.equal(false); - done(); - }); - }); - - it('should give false when isSpent() returns an error', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, new Error('error')); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {}, function(unspent) { - unspent.should.equal(false); - done(); - }); - }); - }); - - describe('#isSpent', function() { - var db = {}; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - db: db, - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - it('should give true if bitcoind.isSpent gives true (with output info)', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var isSpent = sinon.stub().returns(true); - am.node.services.bitcoind = { - isSpent: isSpent, - on: sinon.stub() - }; - var output = { - txid: '4228d3f41051f914f71a1dcbbe4098e29a07cc2672fdadab0763d88ffd6ffa57', - outputIndex: 3 - }; - am.isSpent(output, {}, function(spent) { - isSpent.callCount.should.equal(1); - isSpent.args[0][0].should.equal(output.txid); - isSpent.args[0][1].should.equal(output.outputIndex); - spent.should.equal(true); - done(); - }); - }); - it('should give true if bitcoind.isSpent gives true (with input)', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var isSpent = sinon.stub().returns(true); - am.node.services.bitcoind = { - isSpent: isSpent, - on: sinon.stub() - }; - var txid = '4228d3f41051f914f71a1dcbbe4098e29a07cc2672fdadab0763d88ffd6ffa57'; - var output = { - prevTxId: new Buffer(txid, 'hex'), - outputIndex: 4 - }; - am.isSpent(output, {}, function(spent) { - isSpent.callCount.should.equal(1); - isSpent.args[0][0].should.equal(txid); - isSpent.args[0][1].should.equal(output.outputIndex); - spent.should.equal(true); - done(); - }); - }); - it('should give true if bitcoind.isSpent is false and mempoolSpentIndex is true', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.node.services.bitcoind = { - isSpent: sinon.stub().returns(false), - on: sinon.stub() - }; - var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'; - var outputIndex = 0; - var output = { - prevTxId: new Buffer(txid, 'hex'), - outputIndex: outputIndex - }; - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var spentKey = Buffer.concat([ - new Buffer(txid, 'hex'), - outputIndexBuffer - ]).toString('binary'); - am.mempoolSpentIndex[spentKey] = true; - am.isSpent(output, {queryMempool: true}, function(spent) { - spent.should.equal(true); - done(); - }); - }); - it('should give false if spent in mempool with queryMempool set to false', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.node.services.bitcoind = { - isSpent: sinon.stub().returns(false), - on: sinon.stub() - }; - var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'; - var outputIndex = 0; - var output = { - prevTxId: new Buffer(txid, 'hex'), - outputIndex: outputIndex - }; - var spentKey = [txid, outputIndex].join('-'); - am.mempoolSpentIndex[spentKey] = new Buffer(5); - am.isSpent(output, {queryMempool: false}, function(spent) { - spent.should.equal(false); - done(); - }); - }); - it('default to querying the mempool', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.node.services.bitcoind = { - isSpent: sinon.stub().returns(false), - on: sinon.stub() - }; - var txidBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); - var outputIndex = 0; - var output = { - prevTxId: txidBuffer, - outputIndex: outputIndex - }; - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var spentKey = Buffer.concat([ - txidBuffer, - outputIndexBuffer - ]).toString('binary'); - am.mempoolSpentIndex[spentKey] = true; - am.isSpent(output, {}, function(spent) { - spent.should.equal(true); - done(); - }); - }); - }); - - describe('#getAddressHistory', function() { - it('will call get on address history instance', function(done) { - function TestAddressHistory(args) { - args.node.should.equal(mocknode); - args.addresses.should.deep.equal([]); - args.options.should.deep.equal({}); - } - TestAddressHistory.prototype.get = sinon.stub().callsArg(0); - var TestAddressService = proxyquire('../../../lib/services/address', { - './history': TestAddressHistory - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getAddressHistory([], {}, function(err, history) { - TestAddressHistory.prototype.get.callCount.should.equal(1); - done(); - }); - }); - }); - describe('#updateMempoolIndex/#removeMempoolIndex', function() { - var am; - var tx = Transaction().fromBuffer(txBuf); - var clock; - - beforeEach(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - clock = sinon.useFakeTimers(); - }); - - afterEach(function() { - clock.restore(); - }); - - it('will update the input and output indexes', function() { - am.mempoolIndex = {}; - am.mempoolIndex.batch = function(operations, callback) { - callback.should.be.a('function'); - Object.keys(am.mempoolSpentIndex).length.should.equal(14); - Object.keys(am.mempoolAddressIndex).length.should.equal(5); - _.values(am.mempoolAddressIndex).should.deep.equal([1,1,12,1,1]); - for (var i = 0; i < operations.length; i++) { - operations[i].type.should.equal('put'); - } - var nowTime = new Date().getTime(); - var nowTimeBuffer = new Buffer(8); - nowTimeBuffer.writeDoubleBE(nowTime); - var expectedValue = '45202ffdeb8344af4dec07cddf0478485dc65cc7d08303e45959630c89b51ea200000002' + - nowTimeBuffer.toString('hex'); - operations[7].value.toString('hex').should.equal(expectedValue); - var matches = 0; - - - for (var j = 0; j < operations.length; j++) { - var match = Buffer.concat([ - constants.MEMPREFIXES.SPENTS, - bitcore.Address('1JT7KDYwT9JY9o2vyqcKNSJgTWeKfV3ui8').hashBuffer - ]).toString('hex'); - - if (operations[j].key.slice(0, 21).toString('hex') === match) { - matches++; - } - } - matches.should.equal(12); - }; - am.updateMempoolIndex(tx, true); - }); - - it('will remove the input and output indexes', function() { - am.mempoolIndex = {}; - am.mempoolIndex.batch = function(operations, callback) { - callback.should.be.a('function'); - Object.keys(am.mempoolSpentIndex).length.should.equal(0); - for (var i = 0; i < operations.length; i++) { - operations[i].type.should.equal('del'); - } - Object.keys(am.mempoolAddressIndex).length.should.equal(0); - }; - am.updateMempoolIndex(tx, false); - }); - - }); - - describe('#getAddressSummary', function() { - var clock; - beforeEach(function() { - clock = sinon.useFakeTimers(); - sinon.stub(log, 'warn'); - }); - afterEach(function() { - clock.restore(); - log.warn.restore(); - }); - it('will handle error from _getAddressConfirmedSummary', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var options = {}; - addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, new Error('test')); - addressService.getAddressSummary(address, options, function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - it('will handle error from _getAddressMempoolSummary', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var options = {}; - addressService._getAddressConfirmedSummary = sinon.stub().callsArg(2); - addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(2, new Error('test2')); - addressService.getAddressSummary(address, options, function(err) { - should.exist(err); - err.message.should.equal('test2'); - done(); - }); - }); - it('will pass cache and summary between functions correctly', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var options = {}; - var cache = {}; - var summary = {}; - addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); - addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(3, null, cache); - addressService._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache); - addressService._transformAddressSummaryFromResult = sinon.stub().returns(summary); - addressService.getAddressSummary(address, options, function(err, sum) { - addressService._getAddressConfirmedSummary.callCount.should.equal(1); - addressService._getAddressMempoolSummary.callCount.should.equal(1); - addressService._getAddressMempoolSummary.args[0][2].should.equal(cache); - addressService._setAndSortTxidsFromAppearanceIds.callCount.should.equal(1); - addressService._setAndSortTxidsFromAppearanceIds.args[0][0].should.equal(cache); - addressService._transformAddressSummaryFromResult.callCount.should.equal(1); - addressService._transformAddressSummaryFromResult.args[0][0].should.equal(cache); - sum.should.equal(summary); - done(); - }); - }); - it('will log if there is a slow query', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var options = {}; - var cache = {}; - var summary = {}; - addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); - addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); - addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(3, null, cache); - addressService._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache); - addressService._transformAddressSummaryFromResult = sinon.stub().returns(summary); - addressService.getAddressSummary(address, options, function() { - log.warn.callCount.should.equal(1); - done(); - }); - clock.tick(6000); - }); - }); - - describe('#_getAddressConfirmedSummary', function() { - it('will pass arguments correctly', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = {}; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = {}; - as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, result); - as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, null, result); - as._getAddressConfirmedSummary(address, options, function(err) { - if (err) { - return done(err); - } - var expectedResult = { - appearanceIds: {}, - totalReceived: 0, - balance: 0, - unconfirmedAppearanceIds: {}, - unconfirmedBalance: 0 - }; - as._getAddressConfirmedInputsSummary.args[0][0].should.equal(address); - as._getAddressConfirmedInputsSummary.args[0][1].should.deep.equal(expectedResult); - as._getAddressConfirmedInputsSummary.args[0][2].should.deep.equal(options); - as._getAddressConfirmedOutputsSummary.args[0][0].should.equal(address); - as._getAddressConfirmedOutputsSummary.args[0][1].should.deep.equal(result); - as._getAddressConfirmedOutputsSummary.args[0][2].should.equal(options); - done(); - }); - }); - it('will pass error correctly (inputs)', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = {}; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = {}; - as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, new Error('test')); - as._getAddressConfirmedSummary(address, options, function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - it('will pass error correctly (outputs)', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = {}; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = {}; - as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, result); - as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, new Error('test')); - as._getAddressConfirmedSummary(address, options, function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - }); - - describe('#_getAddressConfirmedInputsSummary', function() { - it('will stream inputs and collect txids', function(done) { - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = { - appearanceIds: {} - }; - var options = {}; - var txid = 'f2cfc19d13f0c12199f70e420d84e2b3b1d4e499702aa9d737f8c24559c9ec47'; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - as.createInputsStream = sinon.stub().returns(streamStub); - as._getAddressConfirmedInputsSummary(address, result, options, function(err, result) { - if (err) { - return done(err); - } - result.appearanceIds[txid].should.equal(10); - done(); - }); - - streamStub.emit('data', { - txid: txid, - height: 10 - }); - streamStub.push(null); - }); - it('handle stream error', function(done) { - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var cache = {}; - var options = {}; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - as.createInputsStream = sinon.stub().returns(streamStub); - as._getAddressConfirmedInputsSummary(address, cache, options, function(err, cache) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - - streamStub.emit('error', new Error('test')); - streamStub.push(null); - }); - }); - - describe('#_getAddressConfirmedOutputsSummary', function() { - it('will stream inputs and collect txids', function(done) { - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub(), - isSpent: sinon.stub().returns(false) - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = { - appearanceIds: {}, - unconfirmedAppearanceIds: {}, - balance: 0, - totalReceived: 0, - unconfirmedBalance: 0 - }; - - var options = { - queryMempool: true - }; - var txid = 'f2cfc19d13f0c12199f70e420d84e2b3b1d4e499702aa9d737f8c24559c9ec47'; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - - as.createOutputsStream = sinon.stub().returns(streamStub); - - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(new Buffer(txid, 'hex'), 2); - as.mempoolSpentIndex[spentIndexSyncKey] = true; - - as._getAddressConfirmedOutputsSummary(address, result, options, function(err, cache) { - if (err) { - return done(err); - } - result.appearanceIds[txid].should.equal(10); - result.balance.should.equal(1000); - result.totalReceived.should.equal(1000); - result.unconfirmedBalance.should.equal(-1000); - done(); - }); - - streamStub.emit('data', { - txid: txid, - height: 10, - outputIndex: 2, - satoshis: 1000 - }); - streamStub.push(null); - }); - it('handle stream error', function(done) { - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = { - appearanceIds: {}, - unconfirmedAppearanceIds: {}, - balance: 0, - totalReceived: 0, - unconfirmedBalance: 0 - }; - - var options = {}; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - as.createOutputsStream = sinon.stub().returns(streamStub); - as._getAddressConfirmedOutputsSummary(address, result, options, function(err, cache) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - - streamStub.emit('error', new Error('test')); - streamStub.push(null); - }); - }); - - describe('#_setAndSortTxidsFromAppearanceIds', function() { - it('will sort correctly', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = { - appearanceIds: { - '22488dbb99aed86e7081ac480e3459fa40ccab7ee18bef98b84b3cdce6bf05be': 200, - '1c413601acbd608240fc635b95886c3c1f76ec8589c3392a58b5715ceb618e93': 100, - '206d3834c010d46a2cf478cb1c5fe252be41f683c8a738e3ebe27f1aae67f505': 101 - }, - unconfirmedAppearanceIds: { - 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681': 1452898347406, - 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693': 1452898331964, - 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345': 1452897902377, - 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a': 1452897971363, - 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9': 1452897923107 - } - }; - as._setAndSortTxidsFromAppearanceIds(result, function(err, result) { - if (err) { - return done(err); - } - should.exist(result.txids); - result.txids[0].should.equal('1c413601acbd608240fc635b95886c3c1f76ec8589c3392a58b5715ceb618e93'); - result.txids[1].should.equal('206d3834c010d46a2cf478cb1c5fe252be41f683c8a738e3ebe27f1aae67f505'); - result.txids[2].should.equal('22488dbb99aed86e7081ac480e3459fa40ccab7ee18bef98b84b3cdce6bf05be'); - result.unconfirmedTxids[0].should.equal('f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345'); - result.unconfirmedTxids[1].should.equal('f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9'); - result.unconfirmedTxids[2].should.equal('edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a'); - result.unconfirmedTxids[3].should.equal('ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693'); - result.unconfirmedTxids[4].should.equal('ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681'); - done(); - }); - }); - }); - - - describe('#_updateAddressIndex', function() { - it('should add using 2 keys', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - - _.values(as.mempoolAddressIndex).should.deep.equal([]); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index2', true); - as._updateAddressIndex('index2', true); - as.mempoolAddressIndex.should.deep.equal({ - "index1": 4, - "index2": 2 - }); - }); - - it('should add/remove using 2 keys', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - _.values(as.mempoolAddressIndex).should.deep.equal([]); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', false); - - as._updateAddressIndex('index2', true); - as._updateAddressIndex('index2', true); - as._updateAddressIndex('index2', false); - as._updateAddressIndex('index2', false); - as.mempoolAddressIndex.should.deep.equal({ - "index1": 3 - }); - as._updateAddressIndex('index2', false); - as.mempoolAddressIndex.should.deep.equal({ - "index1": 3 - }); - }); - }); - - - describe('#_getAddressMempoolSummary', function() { - it('skip if options not enabled', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var resultBase = { - unconfirmedAppearanceIds: {}, - unconfirmedBalance: 0 - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = {}; - as._getAddressMempoolSummary(address, options, resultBase, function(err, result) { - if (err) { - return done(err); - } - Object.keys(result.unconfirmedAppearanceIds).length.should.equal(0); - result.unconfirmedBalance.should.equal(0); - done(); - }); - }); - it('include all txids and balance from inputs and outputs', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var resultBase = { - unconfirmedAppearanceIds: {}, - unconfirmedBalance: 0 - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = { - queryMempool: true - }; - var mempoolInputs = [ - { - address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', - hashType: 'scripthash', - txid: '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', - inputIndex: 0, - timestamp: 1452874536321, - height: -1, - confirmations: 0 - } - ]; - var mempoolOutputs = [ - { - address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', - hashType: 'scripthash', - txid: '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d', - outputIndex: 0, - height: -1, - timestamp: 1452874521466, - satoshis: 131368318, - script: '76a9148c66db6e9f74b1db9c400eaa2aed3743417f38e688ac', - confirmations: 0 - }, - { - address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', - hashType: 'scripthash', - txid: '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247', - outputIndex: 0, - height: -1, - timestamp: 1452874521466, - satoshis: 131368318, - script: '76a9148c66db6e9f74b1db9c400eaa2aed3743417f38e688ac', - confirmations: 0 - } - ]; - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey( - new Buffer(mempoolOutputs[1].txid, 'hex'), - 0 - ); - as.mempoolSpentIndex[spentIndexSyncKey] = true; - - var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; - var addressIndex = encoding.encodeMempoolAddressIndexKey(address.hashBuffer, hashTypeBuffer); - as.mempoolAddressIndex[addressIndex] = 1; - - as._getInputsMempool = sinon.stub().callsArgWith(3, null, mempoolInputs); - as._getOutputsMempool = sinon.stub().callsArgWith(3, null, mempoolOutputs); - as._getAddressMempoolSummary(address, options, resultBase, function(err, result) { - if (err) { - return done(err); - } - var txid1 = '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8'; - var txid2 = '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d'; - var txid3 = '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247'; - result.unconfirmedAppearanceIds[txid1].should.equal(1452874536321); - result.unconfirmedAppearanceIds[txid2].should.equal(1452874521466); - result.unconfirmedAppearanceIds[txid3].should.equal(1452874521466); - result.unconfirmedBalance.should.equal(131368318); - done(); - }); - }); - }); - - describe('#_transformAddressSummaryFromResult', function() { - var result = { - totalReceived: 1000000, - balance: 500000, - txids: [ - '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', - 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92' - ], - appearanceIds: { - 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92': 100000, - '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8': 200000 - }, - unconfirmedAppearanceIds: { - '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d': 1452874536321, - '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247': 1452874521466 - }, - unconfirmedTxids: [ - '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247', - '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d' - ], - unconfirmedBalance: 500000 - }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - it('will transform result into summary', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = {}; - var summary = as._transformAddressSummaryFromResult(result, options); - summary.totalReceived.should.equal(1000000); - summary.totalSpent.should.equal(500000); - summary.balance.should.equal(500000); - summary.appearances.should.equal(2); - summary.unconfirmedAppearances.should.equal(2); - summary.unconfirmedBalance.should.equal(500000); - summary.txids.length.should.equal(4); - }); - it('will omit txlist', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = { - noTxList: true - }; - var summary = as._transformAddressSummaryFromResult(result, options); - should.not.exist(summary.txids); - }); - it('will include full appearance ids', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = { - fullTxList: true - }; - var summary = as._transformAddressSummaryFromResult(result, options); - should.exist(summary.appearanceIds); - should.exist(summary.unconfirmedAppearanceIds); - }); - }); - -}); diff --git a/test/services/bitcoind.unit.js b/test/services/bitcoind.unit.js index 47156b504..8719f7f5c 100644 --- a/test/services/bitcoind.unit.js +++ b/test/services/bitcoind.unit.js @@ -1,26 +1,44 @@ 'use strict'; +/* jshint sub: true */ + +var path = require('path'); +var EventEmitter = require('events').EventEmitter; var should = require('chai').should(); +var crypto = require('crypto'); +var bitcore = require('bitcore-lib'); +var _ = bitcore.deps._; var sinon = require('sinon'); var proxyquire = require('proxyquire'); var fs = require('fs'); var sinon = require('sinon'); -var readFileSync = sinon.stub().returns(fs.readFileSync(__dirname + '/../data/bitcoin.conf')); + +var index = require('../../lib'); +var log = index.log; +var errors = index.errors; + +var Transaction = bitcore.Transaction; +var readFileSync = sinon.stub().returns(fs.readFileSync(path.resolve(__dirname, '../data/bitcoin.conf'))); var BitcoinService = proxyquire('../../lib/services/bitcoind', { fs: { readFileSync: readFileSync } }); +var defaultBitcoinConf = fs.readFileSync(path.resolve(__dirname, '../data/default.bitcoin.conf'), 'utf8'); describe('Bitcoin Service', function() { + var txhex = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000'; + var baseConfig = { node: { + network: bitcore.Networks.testnet + }, + spawn: { datadir: 'testdir', - network: { - name: 'regtest' - } + exec: 'testpath' } }; + describe('@constructor', function() { it('will create an instance', function() { var bitcoind = new BitcoinService(baseConfig); @@ -30,34 +48,330 @@ describe('Bitcoin Service', function() { var bitcoind = BitcoinService(baseConfig); should.exist(bitcoind); }); + it('will init caches', function() { + var bitcoind = new BitcoinService(baseConfig); + should.exist(bitcoind.utxosCache); + should.exist(bitcoind.txidsCache); + should.exist(bitcoind.balanceCache); + should.exist(bitcoind.summaryCache); + should.exist(bitcoind.transactionDetailedCache); + + should.exist(bitcoind.transactionCache); + should.exist(bitcoind.rawTransactionCache); + should.exist(bitcoind.blockCache); + should.exist(bitcoind.rawBlockCache); + should.exist(bitcoind.blockHeaderCache); + should.exist(bitcoind.zmqKnownTransactions); + should.exist(bitcoind.zmqKnownBlocks); + should.exist(bitcoind.lastTip); + should.exist(bitcoind.lastTipTimeout); + }); + it('will init clients', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.should.deep.equal([]); + bitcoind.nodesIndex.should.equal(0); + bitcoind.nodes.push({client: sinon.stub()}); + should.exist(bitcoind.client); + }); + it('will set subscriptions', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.subscriptions.should.deep.equal({ + address: {}, + rawtransaction: [], + hashblock: [] + }); + }); }); + describe('@dependencies', function() { it('will have no dependencies', function() { BitcoinService.dependencies.should.deep.equal([]); }); }); - describe('#_loadConfiguration', function() { + + describe('#getAPIMethods', function() { + it('will return spec', function() { + var bitcoind = new BitcoinService(baseConfig); + var methods = bitcoind.getAPIMethods(); + should.exist(methods); + methods.length.should.equal(21); + }); + }); + + describe('#getPublishEvents', function() { + it('will return spec', function() { + var bitcoind = new BitcoinService(baseConfig); + var events = bitcoind.getPublishEvents(); + should.exist(events); + events.length.should.equal(3); + events[0].name.should.equal('bitcoind/rawtransaction'); + events[0].scope.should.equal(bitcoind); + events[0].subscribe.should.be.a('function'); + events[0].unsubscribe.should.be.a('function'); + events[1].name.should.equal('bitcoind/hashblock'); + events[1].scope.should.equal(bitcoind); + events[1].subscribe.should.be.a('function'); + events[1].unsubscribe.should.be.a('function'); + events[2].name.should.equal('bitcoind/addresstxid'); + events[2].scope.should.equal(bitcoind); + events[2].subscribe.should.be.a('function'); + events[2].unsubscribe.should.be.a('function'); + }); + it('will call subscribe/unsubscribe with correct args', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.subscribe = sinon.stub(); + bitcoind.unsubscribe = sinon.stub(); + var events = bitcoind.getPublishEvents(); + + events[0].subscribe('test'); + bitcoind.subscribe.args[0][0].should.equal('rawtransaction'); + bitcoind.subscribe.args[0][1].should.equal('test'); + + events[0].unsubscribe('test'); + bitcoind.unsubscribe.args[0][0].should.equal('rawtransaction'); + bitcoind.unsubscribe.args[0][1].should.equal('test'); + + events[1].subscribe('test'); + bitcoind.subscribe.args[1][0].should.equal('hashblock'); + bitcoind.subscribe.args[1][1].should.equal('test'); + + events[1].unsubscribe('test'); + bitcoind.unsubscribe.args[1][0].should.equal('hashblock'); + bitcoind.unsubscribe.args[1][1].should.equal('test'); + }); + }); + + describe('#subscribe', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will push to subscriptions', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter = {}; + bitcoind.subscribe('hashblock', emitter); + bitcoind.subscriptions.hashblock[0].should.equal(emitter); + + var emitter2 = {}; + bitcoind.subscribe('rawtransaction', emitter2); + bitcoind.subscriptions.rawtransaction[0].should.equal(emitter2); + }); + }); + + describe('#unsubscribe', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will remove item from subscriptions', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = {}; + var emitter2 = {}; + var emitter3 = {}; + var emitter4 = {}; + var emitter5 = {}; + bitcoind.subscribe('hashblock', emitter1); + bitcoind.subscribe('hashblock', emitter2); + bitcoind.subscribe('hashblock', emitter3); + bitcoind.subscribe('hashblock', emitter4); + bitcoind.subscribe('hashblock', emitter5); + bitcoind.subscriptions.hashblock.length.should.equal(5); + + bitcoind.unsubscribe('hashblock', emitter3); + bitcoind.subscriptions.hashblock.length.should.equal(4); + bitcoind.subscriptions.hashblock[0].should.equal(emitter1); + bitcoind.subscriptions.hashblock[1].should.equal(emitter2); + bitcoind.subscriptions.hashblock[2].should.equal(emitter4); + bitcoind.subscriptions.hashblock[3].should.equal(emitter5); + }); + it('will not remove item an already unsubscribed item', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = {}; + var emitter3 = {}; + bitcoind.subscriptions.hashblock= [emitter1]; + bitcoind.unsubscribe('hashblock', emitter3); + bitcoind.subscriptions.hashblock.length.should.equal(1); + bitcoind.subscriptions.hashblock[0].should.equal(emitter1); + }); + }); + + describe('#subscribeAddress', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will not an invalid address', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter = new EventEmitter(); + bitcoind.subscribeAddress(emitter, ['invalidaddress']); + should.not.exist(bitcoind.subscriptions.address['invalidaddress']); + }); + it('will add a valid address', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter = new EventEmitter(); + bitcoind.subscribeAddress(emitter, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + should.exist(bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + }); + it('will handle multiple address subscribers', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + var emitter2 = new EventEmitter(); + bitcoind.subscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.subscribeAddress(emitter2, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + should.exist(bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(2); + }); + it('will not add the same emitter twice', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + bitcoind.subscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.subscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + should.exist(bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1); + }); + }); + + describe('#unsubscribeAddress', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('it will remove a subscription', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + var emitter2 = new EventEmitter(); + bitcoind.subscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.subscribeAddress(emitter2, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + should.exist(bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(2); + bitcoind.unsubscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1); + }); + it('will unsubscribe subscriptions for an emitter', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + var emitter2 = new EventEmitter(); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2]; + bitcoind.unsubscribeAddress(emitter1); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1); + }); + it('will NOT unsubscribe subscription with missing address', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + var emitter2 = new EventEmitter(); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2]; + bitcoind.unsubscribeAddress(emitter1, ['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo']); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(2); + }); + it('will NOT unsubscribe subscription with missing emitter', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + var emitter2 = new EventEmitter(); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter2]; + bitcoind.unsubscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'][0].should.equal(emitter2); + }); + it('will remove empty addresses', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + var emitter2 = new EventEmitter(); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2]; + bitcoind.unsubscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + bitcoind.unsubscribeAddress(emitter2, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + should.not.exist(bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']); + }); + it('will unsubscribe emitter for all addresses', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + var emitter2 = new EventEmitter(); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2]; + bitcoind.subscriptions.address['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'] = [emitter1, emitter2]; + sinon.spy(bitcoind, 'unsubscribeAddressAll'); + bitcoind.unsubscribeAddress(emitter1); + bitcoind.unsubscribeAddressAll.callCount.should.equal(1); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1); + bitcoind.subscriptions.address['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'].length.should.equal(1); + }); + }); + + describe('#unsubscribeAddressAll', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will unsubscribe emitter for all addresses', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = new EventEmitter(); + var emitter2 = new EventEmitter(); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2]; + bitcoind.subscriptions.address['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'] = [emitter1, emitter2]; + bitcoind.subscriptions.address['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'] = [emitter2]; + bitcoind.subscriptions.address['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou'] = [emitter1]; + bitcoind.unsubscribeAddress(emitter1); + bitcoind.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1); + bitcoind.subscriptions.address['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'].length.should.equal(1); + bitcoind.subscriptions.address['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].length.should.equal(1); + should.not.exist(bitcoind.subscriptions.address['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou']); + }); + }); + + describe('#_getDefaultConfig', function() { + it('will generate config file from defaults', function() { + var bitcoind = new BitcoinService(baseConfig); + var config = bitcoind._getDefaultConfig(); + config.should.equal(defaultBitcoinConf); + }); + }); + + describe('#_loadSpawnConfiguration', function() { it('will parse a bitcoin.conf file', function() { var TestBitcoin = proxyquire('../../lib/services/bitcoind', { fs: { readFileSync: readFileSync, - existsSync: sinon.stub().returns(true) + existsSync: sinon.stub().returns(true), + writeFileSync: sinon.stub() }, mkdirp: { sync: sinon.stub() } }); var bitcoind = new TestBitcoin(baseConfig); - bitcoind._loadConfiguration({datadir: process.env.HOME + '/.bitcoin'}); - should.exist(bitcoind.configuration); - bitcoind.configuration.should.deep.equal({ - server: 1, - whitelist: '127.0.0.1', - txindex: 1, + bitcoind._loadSpawnConfiguration({datadir: process.env.HOME + '/.bitcoin'}); + should.exist(bitcoind.spawn.config); + bitcoind.spawn.config.should.deep.equal({ + addressindex: 1, + checkblocks: 144, + dbcache: 8192, + maxuploadtarget: 1024, port: 20000, + rpcport: 50001, rpcallowip: '127.0.0.1', rpcuser: 'bitcoin', - rpcpassword: 'local321' + rpcpassword: 'local321', + server: 1, + spentindex: 1, + timestampindex: 1, + txindex: 1, + upnp: 0, + whitelist: '127.0.0.1', + zmqpubhashblock: 'tcp://127.0.0.1:28332', + zmqpubrawtx: 'tcp://127.0.0.1:28332' }); }); it('should throw an exception if txindex isn\'t enabled in the configuration', function() { @@ -72,12 +386,12 @@ describe('Bitcoin Service', function() { }); var bitcoind = new TestBitcoin(baseConfig); (function() { - bitcoind._loadConfiguration({datadir: './test'}); - }).should.throw('Txindex option'); + bitcoind._loadSpawnConfiguration({datadir: './test'}); + }).should.throw(bitcore.errors.InvalidState); }); - it('should set https options if node https options are set', function() { + it('should NOT set https options if node https options are set', function() { var writeFileSync = function(path, config) { - config.should.equal('whitelist=127.0.0.1\ntxindex=1\nrpcssl=1\nrpcsslprivatekeyfile=key.pem\nrpcsslcertificatechainfile=cert.pem\n'); + config.should.equal(defaultBitcoinConf); }; var TestBitcoin = proxyquire('../../lib/services/bitcoind', { fs: { @@ -91,7 +405,6 @@ describe('Bitcoin Service', function() { }); var config = { node: { - datadir: 'testdir', network: { name: 'regtest' }, @@ -100,347 +413,4327 @@ describe('Bitcoin Service', function() { key: 'key.pem', cert: 'cert.pem' } + }, + spawn: { + datadir: 'testdir', + exec: 'testexec' } }; var bitcoind = new TestBitcoin(config); - bitcoind._loadConfiguration({datadir: process.env.HOME + '/.bitcoin'}); - }); - describe('reindex', function() { - var log = require('../../lib/').log; - var stub; - beforeEach(function() { - stub = sinon.stub(log, 'warn'); - }); - after(function() { - stub.restore(); - }); - it('should warn the user if reindex is set to 1 in the bitcoin.conf file', function() { - var readFileSync = function() { - return "txindex=1\nreindex=1"; - }; - var testbitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync, - existsSync: sinon.stub().returns(true) - }, - mkdirp: { - sync: sinon.stub() - }, - }); - var bitcoind = new testbitcoin(baseConfig); - bitcoind._loadConfiguration(); - stub.callCount.should.equal(1); - }); + bitcoind._loadSpawnConfiguration({datadir: process.env.HOME + '/.bitcoin'}); }); }); - describe('#_registerEventHandlers', function() { - it('will emit tx with transactions from bindings', function(done) { - var transaction = {}; - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - onTipUpdate: sinon.stub(), - startTxMon: sinon.stub().callsArgWith(0, [transaction]), - startTxMonLeave: sinon.stub().callsArgWith(0, [transaction]) - }; + + describe('#_checkConfigIndexes', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'warn'); + }); + after(function() { + sandbox.restore(); + }); + it('should warn the user if reindex is set to 1 in the bitcoin.conf file', function() { + var bitcoind = new BitcoinService(baseConfig); + var config = { + txindex: 1, + addressindex: 1, + spentindex: 1, + server: 1, + zmqpubrawtx: 1, + zmqpubhashblock: 1, + reindex: 1 + }; + var node = {}; + bitcoind._checkConfigIndexes(config, node); + log.warn.callCount.should.equal(1); + node._reindex.should.equal(true); + }); + }); + + describe('#_resetCaches', function() { + it('will reset LRU caches', function() { + var bitcoind = new BitcoinService(baseConfig); + var keys = []; + for (var i = 0; i < 10; i++) { + keys.push(crypto.randomBytes(32)); + bitcoind.transactionDetailedCache.set(keys[i], {}); + bitcoind.utxosCache.set(keys[i], {}); + bitcoind.txidsCache.set(keys[i], {}); + bitcoind.balanceCache.set(keys[i], {}); + bitcoind.summaryCache.set(keys[i], {}); + } + bitcoind._resetCaches(); + should.equal(bitcoind.transactionDetailedCache.get(keys[0]), undefined); + should.equal(bitcoind.utxosCache.get(keys[0]), undefined); + should.equal(bitcoind.txidsCache.get(keys[0]), undefined); + should.equal(bitcoind.balanceCache.get(keys[0]), undefined); + should.equal(bitcoind.summaryCache.get(keys[0]), undefined); + }); + }); + + describe('#_tryAll', function() { + it('will retry the number of bitcoind nodes', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.tryAllInterval = 1; + bitcoind.nodes.push({}); + bitcoind.nodes.push({}); + bitcoind.nodes.push({}); + var count = 0; + var func = function(callback) { + count++; + if (count <= 2) { + callback(new Error('test')); + } else { + callback(); } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind.on('tx', function(tx) { - tx.should.equal(transaction); + }; + bitcoind._tryAll(function(next) { + func(next); + }, function() { + count.should.equal(3); done(); }); - bitcoind._registerEventHandlers(); }); - it('will emit tip from bindings', function(done) { - var height = 1; - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - syncPercentage: function() { - return height * 10; - }, - onTipUpdate: function(callback) { - if (height >= 10) { - return callback(undefined); - } - setImmediate(function() { - callback(height++); - }); - }, - startTxMon: sinon.stub(), - startTxMonLeave: sinon.stub() - }; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - var tipCallCount = 0; - bitcoind.on('tip', function(height) { - should.exist(height); - tipCallCount++; - if (height === 9) { - tipCallCount.should.equal(9); - done(); - } + it('will get error if all fail', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.tryAllInterval = 1; + bitcoind.nodes.push({}); + bitcoind.nodes.push({}); + bitcoind.nodes.push({}); + var count = 0; + var func = function(callback) { + count++; + callback(new Error('test')); + }; + bitcoind._tryAll(function(next) { + func(next); + }, function(err) { + should.exist(err); + err.message.should.equal('test'); + count.should.equal(3); + done(); }); - bitcoind._registerEventHandlers(); }); }); - describe('#_onReady', function(done) { - var genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); - it('will emit ready and set the height and genesisBuffer', function(done) { - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - onTipUpdate: sinon.stub(), - startTxMon: sinon.stub(), - getInfo: sinon.stub().returns({ - blocks: 101 - }), - getBlock: sinon.stub().callsArgWith(1, null, genesisBuffer) - }; + + describe('#_wrapRPCError', function() { + it('will convert bitcoind-rpc error object into JavaScript error', function() { + var bitcoind = new BitcoinService(baseConfig); + var error = bitcoind._wrapRPCError({message: 'Test error', code: -1}); + error.should.be.an.instanceof(errors.RPCError); + error.code.should.equal(-1); + error.message.should.equal('Test error'); + }); + }); + + describe('#_initChain', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will set height and genesis buffer', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var genesisBuffer = new Buffer([]); + bitcoind.getRawBlock = sinon.stub().callsArgWith(1, null, genesisBuffer); + bitcoind.nodes.push({ + client: { + getBestBlockHash: function(callback) { + callback(null, { + result: 'bestblockhash' + }); + }, + getBlock: function(hash, callback) { + if (hash === 'bestblockhash') { + callback(null, { + result: { + height: 5000 + } + }); + } + }, + getBlockHash: function(num, callback) { + callback(null, { + result: 'genesishash' + }); + } } }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._registerEventHandlers = sinon.stub(); - var result = {}; - var readyCallCount = 0; - bitcoind.on('ready', function() { - readyCallCount++; - }); - bitcoind._onReady(result, function(err) { - if (err) { - throw err; - } - bitcoind._registerEventHandlers.callCount.should.equal(1); - readyCallCount.should.equal(1); + bitcoind._initChain(function() { + log.info.callCount.should.equal(1); + bitcoind.getRawBlock.callCount.should.equal(1); + bitcoind.getRawBlock.args[0][0].should.equal('genesishash'); + bitcoind.height.should.equal(5000); bitcoind.genesisBuffer.should.equal(genesisBuffer); - bitcoind.height.should.equal(101); done(); }); }); - }); - describe('#start', function() { - it('call bindings start with the correct arguments', function(done) { - var startCallCount = 0; - var start = function(obj, cb) { - startCallCount++; - obj.datadir.should.equal('testdir'); - obj.network.should.equal('regtest'); - cb(); - }; - var onBlocksReady = sinon.stub().callsArg(0); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - start: start, - onBlocksReady: onBlocksReady - }; + it('it will handle error from getBestBlockHash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, {code: -1, message: 'error'}); + bitcoind.nodes.push({ + client: { + getBestBlockHash: getBestBlockHash } }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._loadConfiguration = sinon.stub(); - bitcoind._onReady = sinon.stub().callsArg(1); - bitcoind.start(function(err) { - should.not.exist(err); - bitcoind._loadConfiguration.callCount.should.equal(1); - startCallCount.should.equal(1); - onBlocksReady.callCount.should.equal(1); - bitcoind._onReady.callCount.should.equal(1); + bitcoind._initChain(function(err) { + err.should.be.instanceOf(Error); done(); }); }); - it('will give an error from bindings.start', function(done) { - var start = sinon.stub().callsArgWith(1, new Error('test')); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - start: start - }; + it('it will handle error from getBlock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, {}); + var getBlock = sinon.stub().callsArgWith(1, {code: -1, message: 'error'}); + bitcoind.nodes.push({ + client: { + getBestBlockHash: getBestBlockHash, + getBlock: getBlock } }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._loadConfiguration = sinon.stub(); - bitcoind.start(function(err) { - should.exist(err); - err.message.should.equal('test'); + bitcoind._initChain(function(err) { + err.should.be.instanceOf(Error); done(); }); }); - it('will give an error from bindings.onBlocksReady', function(done) { - var start = sinon.stub().callsArgWith(1, null); - var onBlocksReady = sinon.stub().callsArgWith(0, new Error('test')); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - start: start, - onBlocksReady: onBlocksReady - }; + it('it will handle error from getBlockHash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, {}); + var getBlock = sinon.stub().callsArgWith(1, null, { + result: { + height: 10 } }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._onReady = sinon.stub().callsArg(1); - bitcoind._loadConfiguration = sinon.stub(); - bitcoind.start(function(err) { - should.exist(err); - err.message.should.equal('test'); + var getBlockHash = sinon.stub().callsArgWith(1, {code: -1, message: 'error'}); + bitcoind.nodes.push({ + client: { + getBestBlockHash: getBestBlockHash, + getBlock: getBlock, + getBlockHash: getBlockHash + } + }); + bitcoind._initChain(function(err) { + err.should.be.instanceOf(Error); done(); }); }); - describe('reindex', function() { - var log = require('../../lib/').log; - var info; - beforeEach(function() { - info = sinon.stub(log, 'info'); + it('it will handle error from getRawBlock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, {}); + var getBlock = sinon.stub().callsArgWith(1, null, { + result: { + height: 10 + } }); - afterEach(function() { - info.restore(); + var getBlockHash = sinon.stub().callsArgWith(1, null, {}); + bitcoind.nodes.push({ + client: { + getBestBlockHash: getBestBlockHash, + getBlock: getBlock, + getBlockHash: getBlockHash + } }); - it('will wait for a reindex to complete before calling the callback.', function(done) { - var start = sinon.stub().callsArgWith(1, null); - var onBlocksReady = sinon.stub().callsArg(0); - var percentage = 98; - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - return { - start: start, - onBlocksReady: onBlocksReady, - syncPercentage: function() { - return percentage; - } - }; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._reindex = true; - bitcoind._reindexWait = 1; - bitcoind._onReady = sinon.stub().callsArg(1); - bitcoind._loadConfiguration = sinon.stub(); - bitcoind.start(function() { - info.callCount.should.be.within(2,3); - bitcoind._reindex.should.be.false; - done(); - }); - setTimeout(function() { - percentage = 100; - }, 2); + bitcoind.getRawBlock = sinon.stub().callsArgWith(1, new Error('test')); + bitcoind._initChain(function(err) { + err.should.be.instanceOf(Error); + done(); }); }); }); - describe('#stop', function() { - it('will call bindings stop', function() { - var stop = sinon.stub().callsArgWith(0, null, 'status'); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync + + describe('#_getDefaultConf', function() { + afterEach(function() { + bitcore.Networks.disableRegtest(); + baseConfig.node.network = bitcore.Networks.testnet; + }); + it('will get default rpc port for livenet', function() { + var config = { + node: { + network: bitcore.Networks.livenet }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - stop: stop - }; + spawn: { + datadir: 'testdir', + exec: 'testpath' } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind.stop(function(err, status) { - stop.callCount.should.equal(1); - should.not.exist(err); - }); + }; + var bitcoind = new BitcoinService(config); + bitcoind._getDefaultConf().rpcport.should.equal(8332); }); - it('will give an error from bindings stop', function() { - var stop = sinon.stub().callsArgWith(0, new Error('test')); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync + it('will get default rpc port for testnet', function() { + var config = { + node: { + network: bitcore.Networks.testnet }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - stop: stop - }; + spawn: { + datadir: 'testdir', + exec: 'testpath' } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind.stop(function(err) { - stop.callCount.should.equal(1); - should.exist(err); - err.message.should.equal('test'); - }); + }; + var bitcoind = new BitcoinService(config); + bitcoind._getDefaultConf().rpcport.should.equal(18332); + }); + it('will get default rpc port for regtest', function() { + bitcore.Networks.enableRegtest(); + var config = { + node: { + network: bitcore.Networks.testnet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var bitcoind = new BitcoinService(config); + bitcoind._getDefaultConf().rpcport.should.equal(18332); }); }); - describe('proxy methods', function() { - - var proxyMethods = [ - ['isSynced', 0], - ['syncPercentage', 0], - ['getBlock', 2], - ['isSpent', 2], - ['getBlockIndex', 1], - ['isMainChain', 1], - ['estimateFee', 1], - ['sendTransaction', 2], - ['getTransaction', 3], - ['getTransactionWithBlockInfo', 3], - ['getMempoolTransactions', 0], - ['addMempoolUncheckedTransaction', 1], - ['getBestBlockHash', 0], - ['getNextBlockHash', 1], - ['getInfo', 0] - ]; - - proxyMethods.forEach(function(x) { - it('pass ' + x[1] + ' argument(s) to ' + x[0], function() { - - var stub = sinon.stub(); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - var methods = {}; - methods[x[0]] = stub; - return methods; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - var args = []; - for (var i = 0; i < x[1]; i++) { - args.push(i); - } + describe('#_getNetworkConfigPath', function() { + afterEach(function() { + bitcore.Networks.disableRegtest(); + baseConfig.node.network = bitcore.Networks.testnet; + }); + it('will get default config path for livenet', function() { + var config = { + node: { + network: bitcore.Networks.livenet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var bitcoind = new BitcoinService(config); + should.equal(bitcoind._getNetworkConfigPath(), undefined); + }); + it('will get default rpc port for testnet', function() { + var config = { + node: { + network: bitcore.Networks.testnet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var bitcoind = new BitcoinService(config); + bitcoind._getNetworkConfigPath().should.equal('testnet3/bitcoin.conf'); + }); + it('will get default rpc port for regtest', function() { + bitcore.Networks.enableRegtest(); + var config = { + node: { + network: bitcore.Networks.testnet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var bitcoind = new BitcoinService(config); + bitcoind._getNetworkConfigPath().should.equal('regtest/bitcoin.conf'); + }); + }); + + describe('#_getNetworkOption', function() { + afterEach(function() { + bitcore.Networks.disableRegtest(); + baseConfig.node.network = bitcore.Networks.testnet; + }); + it('return --testnet for testnet', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.node.network = bitcore.Networks.testnet; + bitcoind._getNetworkOption().should.equal('--testnet'); + }); + it('return --regtest for testnet', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.node.network = bitcore.Networks.testnet; + bitcore.Networks.enableRegtest(); + bitcoind._getNetworkOption().should.equal('--regtest'); + }); + it('return undefined for livenet', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.node.network = bitcore.Networks.livenet; + bitcore.Networks.enableRegtest(); + should.equal(bitcoind._getNetworkOption(), undefined); + }); + }); + + describe('#_zmqBlockHandler', function() { + it('will emit block', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + bitcoind._rapidProtectedUpdateTip = sinon.stub(); + bitcoind.on('block', function(block) { + block.should.equal(message); + done(); + }); + bitcoind._zmqBlockHandler(node, message); + }); + it('will not emit same block twice', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + bitcoind._rapidProtectedUpdateTip = sinon.stub(); + bitcoind.on('block', function(block) { + block.should.equal(message); + done(); + }); + bitcoind._zmqBlockHandler(node, message); + bitcoind._zmqBlockHandler(node, message); + }); + it('will call function to update tip', function() { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + bitcoind._rapidProtectedUpdateTip = sinon.stub(); + bitcoind._zmqBlockHandler(node, message); + bitcoind._rapidProtectedUpdateTip.callCount.should.equal(1); + bitcoind._rapidProtectedUpdateTip.args[0][0].should.equal(node); + bitcoind._rapidProtectedUpdateTip.args[0][1].should.equal(message); + }); + it('will emit to subscribers', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + bitcoind._rapidProtectedUpdateTip = sinon.stub(); + var emitter = new EventEmitter(); + bitcoind.subscriptions.hashblock.push(emitter); + emitter.on('bitcoind/hashblock', function(blockHash) { + blockHash.should.equal(message.toString('hex')); + done(); + }); + bitcoind._zmqBlockHandler(node, message); + }); + }); + + describe('#_rapidProtectedUpdateTip', function() { + it('will limit tip updates with rapid calls', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var callCount = 0; + bitcoind._updateTip = function() { + callCount++; + callCount.should.be.within(1, 2); + if (callCount > 1) { + done(); + } + }; + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + var count = 0; + function repeat() { + bitcoind._rapidProtectedUpdateTip(node, message); + count++; + if (count < 50) { + repeat(); + } + } + repeat(); + }); + }); + + describe('#_updateTip', function() { + var sandbox = sinon.sandbox.create(); + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + beforeEach(function() { + sandbox.stub(log, 'error'); + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('log and emit rpc error from get block', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub(); + bitcoind.on('error', function(err) { + err.code.should.equal(-1); + err.message.should.equal('Test error'); + log.error.callCount.should.equal(1); + done(); + }); + var node = { + client: { + getBlock: sinon.stub().callsArgWith(1, {message: 'Test error', code: -1}) + } + }; + bitcoind._updateTip(node, message); + }); + it('emit synced if percentage is 100', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 100); + bitcoind.on('synced', function() { + done(); + }); + var node = { + client: { + getBlock: sinon.stub() + } + }; + bitcoind._updateTip(node, message); + }); + it('NOT emit synced if percentage is less than 100', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 99); + bitcoind.on('synced', function() { + throw new Error('Synced called'); + }); + var node = { + client: { + getBlock: sinon.stub() + } + }; + bitcoind._updateTip(node, message); + log.info.callCount.should.equal(1); + done(); + }); + it('log and emit error from syncPercentage', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, new Error('test')); + bitcoind.on('error', function(err) { + log.error.callCount.should.equal(1); + err.message.should.equal('test'); + done(); + }); + var node = { + client: { + getBlock: sinon.stub() + } + }; + bitcoind._updateTip(node, message); + }); + it('reset caches and set height', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub(); + bitcoind._resetCaches = sinon.stub(); + bitcoind.on('tip', function(height) { + bitcoind._resetCaches.callCount.should.equal(1); + height.should.equal(10); + bitcoind.height.should.equal(10); + done(); + }); + var node = { + client: { + getBlock: sinon.stub().callsArgWith(1, null, { + result: { + height: 10 + } + }) + } + }; + bitcoind._updateTip(node, message); + }); + it('will NOT update twice for the same hash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub(); + bitcoind._resetCaches = sinon.stub(); + bitcoind.on('tip', function() { + done(); + }); + var node = { + client: { + getBlock: sinon.stub().callsArgWith(1, null, { + result: { + height: 10 + } + }) + } + }; + bitcoind._updateTip(node, message); + bitcoind._updateTip(node, message); + }); + it('will not call syncPercentage if node is stopping', function(done) { + var config = { + node: { + network: bitcore.Networks.testnet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var bitcoind = new BitcoinService(config); + bitcoind.syncPercentage = sinon.stub(); + bitcoind._resetCaches = sinon.stub(); + bitcoind.node.stopping = true; + var node = { + client: { + getBlock: sinon.stub().callsArgWith(1, null, { + result: { + height: 10 + } + }) + } + }; + bitcoind.on('tip', function() { + bitcoind.syncPercentage.callCount.should.equal(0); + done(); + }); + bitcoind._updateTip(node, message); + }); + }); + + describe('#_getAddressesFromTransaction', function() { + it('will get results using bitcore.Transaction', function() { + var bitcoind = new BitcoinService(baseConfig); + var wif = 'L2Gkw3kKJ6N24QcDuH4XDqt9cTqsKTVNDGz1CRZhk9cq4auDUbJy'; + var privkey = bitcore.PrivateKey.fromWIF(wif); + var inputAddress = privkey.toAddress(bitcore.Networks.testnet); + var outputAddress = bitcore.Address('2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'); + var tx = bitcore.Transaction(); + tx.from({ + txid: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', + outputIndex: 0, + script: bitcore.Script(inputAddress), + address: inputAddress.toString(), + satoshis: 5000000000 + }); + tx.to(outputAddress, 5000000000); + tx.sign(privkey); + var addresses = bitcoind._getAddressesFromTransaction(tx); + addresses.length.should.equal(2); + addresses[0].should.equal(inputAddress.toString()); + addresses[1].should.equal(outputAddress.toString()); + }); + it('will handle non-standard script types', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = bitcore.Transaction(); + tx.addInput(bitcore.Transaction.Input({ + prevTxId: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', + script: bitcore.Script('OP_TRUE'), + outputIndex: 1, + output: { + script: bitcore.Script('OP_TRUE'), + satoshis: 5000000000 + } + })); + tx.addOutput(bitcore.Transaction.Output({ + script: bitcore.Script('OP_TRUE'), + satoshis: 5000000000 + })); + var addresses = bitcoind._getAddressesFromTransaction(tx); + addresses.length.should.equal(0); + }); + it('will handle unparsable script types or missing input script', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = bitcore.Transaction(); + tx.addOutput(bitcore.Transaction.Output({ + script: new Buffer('4c', 'hex'), + satoshis: 5000000000 + })); + var addresses = bitcoind._getAddressesFromTransaction(tx); + addresses.length.should.equal(0); + }); + it('will return unique values', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = bitcore.Transaction(); + var address = bitcore.Address('2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'); + tx.addOutput(bitcore.Transaction.Output({ + script: bitcore.Script(address), + satoshis: 5000000000 + })); + tx.addOutput(bitcore.Transaction.Output({ + script: bitcore.Script(address), + satoshis: 5000000000 + })); + var addresses = bitcoind._getAddressesFromTransaction(tx); + addresses.length.should.equal(1); + }); + }); + + describe('#_notifyAddressTxidSubscribers', function() { + it('will emit event if matching addresses', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind._getAddressesFromTransaction = sinon.stub().returns([address]); + var emitter = new EventEmitter(); + bitcoind.subscriptions.address[address] = [emitter]; + var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; + var transaction = {}; + emitter.on('bitcoind/addresstxid', function(data) { + data.address.should.equal(address); + data.txid.should.equal(txid); + done(); + }); + sinon.spy(emitter, 'emit'); + bitcoind._notifyAddressTxidSubscribers(txid, transaction); + emitter.emit.callCount.should.equal(1); + }); + it('will NOT emit event without matching addresses', function() { + var bitcoind = new BitcoinService(baseConfig); + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind._getAddressesFromTransaction = sinon.stub().returns([address]); + var emitter = new EventEmitter(); + var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; + var transaction = {}; + emitter.emit = sinon.stub(); + bitcoind._notifyAddressTxidSubscribers(txid, transaction); + emitter.emit.callCount.should.equal(0); + }); + }); + + describe('#_zmqTransactionHandler', function() { + it('will emit to subscribers', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedBuffer = new Buffer(txhex, 'hex'); + var emitter = new EventEmitter(); + bitcoind.subscriptions.rawtransaction.push(emitter); + emitter.on('bitcoind/rawtransaction', function(hex) { + hex.should.be.a('string'); + hex.should.equal(expectedBuffer.toString('hex')); + done(); + }); + var node = {}; + bitcoind._zmqTransactionHandler(node, expectedBuffer); + }); + it('will NOT emit to subscribers more than once for the same tx', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedBuffer = new Buffer(txhex, 'hex'); + var emitter = new EventEmitter(); + bitcoind.subscriptions.rawtransaction.push(emitter); + emitter.on('bitcoind/rawtransaction', function() { + done(); + }); + var node = {}; + bitcoind._zmqTransactionHandler(node, expectedBuffer); + bitcoind._zmqTransactionHandler(node, expectedBuffer); + }); + it('will emit "tx" event', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedBuffer = new Buffer(txhex, 'hex'); + bitcoind.on('tx', function(buffer) { + buffer.should.be.instanceof(Buffer); + buffer.toString('hex').should.equal(expectedBuffer.toString('hex')); + done(); + }); + var node = {}; + bitcoind._zmqTransactionHandler(node, expectedBuffer); + }); + it('will NOT emit "tx" event more than once for the same tx', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedBuffer = new Buffer(txhex, 'hex'); + bitcoind.on('tx', function() { + done(); + }); + var node = {}; + bitcoind._zmqTransactionHandler(node, expectedBuffer); + bitcoind._zmqTransactionHandler(node, expectedBuffer); + }); + }); + + describe('#_checkSyncedAndSubscribeZmqEvents', function() { + var sandbox = sinon.sandbox.create(); + before(function() { + sandbox.stub(log, 'error'); + }); + after(function() { + sandbox.restore(); + }); + it('log errors, update tip and subscribe to zmq events', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._updateTip = sinon.stub(); + bitcoind._subscribeZmqEvents = sinon.stub(); + var blockEvents = 0; + bitcoind.on('block', function() { + blockEvents++; + }); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, { + result: '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45' + }); + getBestBlockHash.onCall(0).callsArgWith(0, {code: -1 , message: 'Test error'}); + var progress = 0.90; + function getProgress() { + progress = progress + 0.01; + return progress; + } + var info = {}; + Object.defineProperty(info, 'result', { + get: function() { + return { + verificationprogress: getProgress() + }; + } + }); + var getBlockchainInfo = sinon.stub().callsArgWith(0, null, info); + getBlockchainInfo.onCall(0).callsArgWith(0, {code: -1, message: 'Test error'}); + var node = { + _reindex: true, + _reindexWait: 1, + _tipUpdateInterval: 1, + client: { + getBestBlockHash: getBestBlockHash, + getBlockchainInfo: getBlockchainInfo + } + }; + bitcoind._checkSyncedAndSubscribeZmqEvents(node); + setTimeout(function() { + log.error.callCount.should.equal(2); + blockEvents.should.equal(11); + bitcoind._updateTip.callCount.should.equal(11); + bitcoind._subscribeZmqEvents.callCount.should.equal(1); + done(); + }, 200); + }); + it('it will clear interval if node is stopping', function(done) { + var config = { + node: { + network: bitcore.Networks.testnet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var bitcoind = new BitcoinService(config); + var getBestBlockHash = sinon.stub().callsArgWith(0, {code: -1, message: 'error'}); + var node = { + _tipUpdateInterval: 1, + client: { + getBestBlockHash: getBestBlockHash + } + }; + bitcoind._checkSyncedAndSubscribeZmqEvents(node); + setTimeout(function() { + bitcoind.node.stopping = true; + var count = getBestBlockHash.callCount; + setTimeout(function() { + getBestBlockHash.callCount.should.equal(count); + done(); + }, 100); + }, 100); + }); + it('will not set interval if synced is true', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._updateTip = sinon.stub(); + bitcoind._subscribeZmqEvents = sinon.stub(); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, { + result: '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45' + }); + var info = { + result: { + verificationprogress: 1.00 + } + }; + var getBlockchainInfo = sinon.stub().callsArgWith(0, null, info); + var node = { + _tipUpdateInterval: 1, + client: { + getBestBlockHash: getBestBlockHash, + getBlockchainInfo: getBlockchainInfo + } + }; + bitcoind._checkSyncedAndSubscribeZmqEvents(node); + setTimeout(function() { + getBestBlockHash.callCount.should.equal(1); + getBlockchainInfo.callCount.should.equal(1); + done(); + }, 200); + }); + }); + + describe('#_subscribeZmqEvents', function() { + it('will call subscribe on zmq socket', function() { + var bitcoind = new BitcoinService(baseConfig); + var node = { + zmqSubSocket: { + subscribe: sinon.stub(), + on: sinon.stub() + } + }; + bitcoind._subscribeZmqEvents(node); + node.zmqSubSocket.subscribe.callCount.should.equal(2); + node.zmqSubSocket.subscribe.args[0][0].should.equal('hashblock'); + node.zmqSubSocket.subscribe.args[1][0].should.equal('rawtx'); + }); + it('will call relevant handler for rawtx topics', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._zmqTransactionHandler = sinon.stub(); + var node = { + zmqSubSocket: new EventEmitter() + }; + node.zmqSubSocket.subscribe = sinon.stub(); + bitcoind._subscribeZmqEvents(node); + node.zmqSubSocket.on('message', function() { + bitcoind._zmqTransactionHandler.callCount.should.equal(1); + done(); + }); + var topic = new Buffer('rawtx', 'utf8'); + var message = new Buffer('abcdef', 'hex'); + node.zmqSubSocket.emit('message', topic, message); + }); + it('will call relevant handler for hashblock topics', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._zmqBlockHandler = sinon.stub(); + var node = { + zmqSubSocket: new EventEmitter() + }; + node.zmqSubSocket.subscribe = sinon.stub(); + bitcoind._subscribeZmqEvents(node); + node.zmqSubSocket.on('message', function() { + bitcoind._zmqBlockHandler.callCount.should.equal(1); + done(); + }); + var topic = new Buffer('hashblock', 'utf8'); + var message = new Buffer('abcdef', 'hex'); + node.zmqSubSocket.emit('message', topic, message); + }); + it('will ignore unknown topic types', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._zmqBlockHandler = sinon.stub(); + bitcoind._zmqTransactionHandler = sinon.stub(); + var node = { + zmqSubSocket: new EventEmitter() + }; + node.zmqSubSocket.subscribe = sinon.stub(); + bitcoind._subscribeZmqEvents(node); + node.zmqSubSocket.on('message', function() { + bitcoind._zmqBlockHandler.callCount.should.equal(0); + bitcoind._zmqTransactionHandler.callCount.should.equal(0); + done(); + }); + var topic = new Buffer('unknown', 'utf8'); + var message = new Buffer('abcdef', 'hex'); + node.zmqSubSocket.emit('message', topic, message); + }); + }); + + describe('#_initZmqSubSocket', function() { + it('will setup zmq socket', function() { + var socket = new EventEmitter(); + socket.monitor = sinon.stub(); + socket.connect = sinon.stub(); + var socketFunc = function() { + return socket; + }; + var BitcoinService = proxyquire('../../lib/services/bitcoind', { + zmq: { + socket: socketFunc + } + }); + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + bitcoind._initZmqSubSocket(node, 'url'); + node.zmqSubSocket.should.equal(socket); + socket.connect.callCount.should.equal(1); + socket.connect.args[0][0].should.equal('url'); + socket.monitor.callCount.should.equal(1); + socket.monitor.args[0][0].should.equal(500); + socket.monitor.args[0][1].should.equal(0); + }); + }); + + describe('#_checkReindex', function() { + var sandbox = sinon.sandbox.create(); + before(function() { + sandbox.stub(log, 'info'); + }); + after(function() { + sandbox.restore(); + }); + it('give error from client getblockchaininfo', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = { + _reindex: true, + _reindexWait: 1, + client: { + getBlockchainInfo: sinon.stub().callsArgWith(0, {code: -1 , message: 'Test error'}) + } + }; + bitcoind._checkReindex(node, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('will wait until sync is 100 percent', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var percent = 0.89; + var node = { + _reindex: true, + _reindexWait: 1, + client: { + getBlockchainInfo: function(callback) { + percent += 0.01; + callback(null, { + result: { + verificationprogress: percent + } + }); + } + } + }; + bitcoind._checkReindex(node, function() { + node._reindex.should.equal(false); + log.info.callCount.should.equal(11); + done(); + }); + }); + it('will call callback if reindex is not enabled', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = { + _reindex: false + }; + bitcoind._checkReindex(node, function() { + node._reindex.should.equal(false); + done(); + }); + }); + }); + + describe('#_loadTipFromNode', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'warn'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will give rpc from client getbestblockhash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, {code: -1, message: 'Test error'}); + var node = { + client: { + getBestBlockHash: getBestBlockHash + } + }; + bitcoind._loadTipFromNode(node, function(err) { + err.should.be.instanceof(Error); + log.warn.callCount.should.equal(0); + done(); + }); + }); + it('will give rpc from client getblock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, { + result: '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45' + }); + var getBlock = sinon.stub().callsArgWith(1, new Error('Test error')); + var node = { + client: { + getBestBlockHash: getBestBlockHash, + getBlock: getBlock + } + }; + bitcoind._loadTipFromNode(node, function(err) { + getBlock.args[0][0].should.equal('00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45'); + err.should.be.instanceof(Error); + log.warn.callCount.should.equal(0); + done(); + }); + }); + it('will log when error is RPC_IN_WARMUP', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, {code: -28, message: 'Verifying blocks...'}); + var node = { + client: { + getBestBlockHash: getBestBlockHash + } + }; + bitcoind._loadTipFromNode(node, function(err) { + err.should.be.instanceof(Error); + log.warn.callCount.should.equal(1); + done(); + }); + }); + it('will set height and emit tip', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, { + result: '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45' + }); + var getBlock = sinon.stub().callsArgWith(1, null, { + result: { + height: 100 + } + }); + var node = { + client: { + getBestBlockHash: getBestBlockHash, + getBlock: getBlock + } + }; + bitcoind.on('tip', function(height) { + height.should.equal(100); + bitcoind.height.should.equal(100); + done(); + }); + bitcoind._loadTipFromNode(node, function(err) { + if (err) { + return done(err); + } + }); + }); + }); + + describe('#_stopSpawnedProcess', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'warn'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('it will kill process and resume', function(done) { + var readFile = sandbox.stub(); + readFile.onCall(0).callsArgWith(2, null, '4321'); + var error = new Error('Test error'); + error.code = 'ENOENT'; + readFile.onCall(1).callsArgWith(2, error); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFile: readFile + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + bitcoind.spawnStopTime = 1; + bitcoind._process = {}; + bitcoind._process.kill = sinon.stub(); + bitcoind._stopSpawnedBitcoin(function(err) { + if (err) { + return done(err); + } + bitcoind._process.kill.callCount.should.equal(1); + log.warn.callCount.should.equal(1); + done(); + }); + }); + it('it will attempt to kill process and resume', function(done) { + var readFile = sandbox.stub(); + readFile.onCall(0).callsArgWith(2, null, '4321'); + var error = new Error('Test error'); + error.code = 'ENOENT'; + readFile.onCall(1).callsArgWith(2, error); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFile: readFile + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + bitcoind.spawnStopTime = 1; + bitcoind._process = {}; + var error2 = new Error('Test error'); + error2.code = 'ESRCH'; + bitcoind._process.kill = sinon.stub().throws(error2); + bitcoind._stopSpawnedBitcoin(function(err) { + if (err) { + return done(err); + } + bitcoind._process.kill.callCount.should.equal(1); + log.warn.callCount.should.equal(2); + done(); + }); + }); + it('it will attempt to kill process with NaN', function(done) { + var readFile = sandbox.stub(); + readFile.onCall(0).callsArgWith(2, null, ' '); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFile: readFile + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + bitcoind.spawnStopTime = 1; + bitcoind._process = {}; + bitcoind._process.kill = sinon.stub(); + bitcoind._stopSpawnedBitcoin(function(err) { + if (err) { + return done(err); + } + done(); + }); + }); + it('it will attempt to kill process without pid', function(done) { + var readFile = sandbox.stub(); + readFile.onCall(0).callsArgWith(2, null, ''); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFile: readFile + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + bitcoind.spawnStopTime = 1; + bitcoind._process = {}; + bitcoind._process.kill = sinon.stub(); + bitcoind._stopSpawnedBitcoin(function(err) { + if (err) { + return done(err); + } + done(); + }); + }); + }); + + describe('#_spawnChildProcess', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + sandbox.stub(log, 'warn'); + sandbox.stub(log, 'error'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will give error from spawn config', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind._loadSpawnConfiguration = sinon.stub().throws(new Error('test')); + bitcoind._spawnChildProcess(function(err) { + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will give error from stopSpawnedBitcoin', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind._stopSpawnedBitcoin = sinon.stub().callsArgWith(0, new Error('test')); + bitcoind._spawnChildProcess(function(err) { + err.should.be.instanceOf(Error); + err.message.should.equal('test'); + }); + }); + it('will exit spawn if shutdown', function() { + var config = { + node: { + network: bitcore.Networks.testnet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var process = new EventEmitter(); + var spawn = sinon.stub().returns(process); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFileSync: readFileSync + }, + child_process: { + spawn: spawn + } + }); + var bitcoind = new TestBitcoinService(config); + bitcoind.spawn = {}; + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind._stopSpawnedBitcoin = sinon.stub().callsArgWith(0, null); + bitcoind.node.stopping = true; + bitcoind._spawnChildProcess(function(err) { + err.should.be.instanceOf(Error); + err.message.should.match(/Stopping while trying to spawn/); + }); + }); + it('will include network with spawn command and init zmq/rpc on node', function(done) { + var process = new EventEmitter(); + var spawn = sinon.stub().returns(process); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFileSync: readFileSync + }, + child_process: { + spawn: spawn + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind.spawn = {}; + bitcoind.spawn.exec = 'testexec'; + bitcoind.spawn.configPath = 'testdir/bitcoin.conf'; + bitcoind.spawn.datadir = 'testdir'; + bitcoind.spawn.config = {}; + bitcoind.spawn.config.rpcport = 20001; + bitcoind.spawn.config.rpcuser = 'bitcoin'; + bitcoind.spawn.config.rpcpassword = 'password'; + bitcoind.spawn.config.zmqpubrawtx = 'tcp://127.0.0.1:30001'; + + bitcoind._loadTipFromNode = sinon.stub().callsArgWith(1, null); + bitcoind._initZmqSubSocket = sinon.stub(); + bitcoind._checkSyncedAndSubscribeZmqEvents = sinon.stub(); + bitcoind._checkReindex = sinon.stub().callsArgWith(1, null); + bitcoind._spawnChildProcess(function(err, node) { + should.not.exist(err); + spawn.callCount.should.equal(1); + spawn.args[0][0].should.equal('testexec'); + spawn.args[0][1].should.deep.equal([ + '--conf=testdir/bitcoin.conf', + '--datadir=testdir', + '--testnet' + ]); + spawn.args[0][2].should.deep.equal({ + stdio: 'inherit' + }); + bitcoind._loadTipFromNode.callCount.should.equal(1); + bitcoind._initZmqSubSocket.callCount.should.equal(1); + should.exist(bitcoind._initZmqSubSocket.args[0][0].client); + bitcoind._initZmqSubSocket.args[0][1].should.equal('tcp://127.0.0.1:30001'); + bitcoind._checkSyncedAndSubscribeZmqEvents.callCount.should.equal(1); + should.exist(bitcoind._checkSyncedAndSubscribeZmqEvents.args[0][0].client); + should.exist(node); + should.exist(node.client); + done(); + }); + }); + it('will respawn bitcoind spawned process', function(done) { + var process = new EventEmitter(); + var spawn = sinon.stub().returns(process); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFileSync: readFileSync + }, + child_process: { + spawn: spawn + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind.spawn = {}; + bitcoind.spawn.exec = 'bitcoind'; + bitcoind.spawn.datadir = '/tmp/bitcoin'; + bitcoind.spawn.configPath = '/tmp/bitcoin/bitcoin.conf'; + bitcoind.spawn.config = {}; + bitcoind.spawnRestartTime = 1; + bitcoind._loadTipFromNode = sinon.stub().callsArg(1); + bitcoind._initZmqSubSocket = sinon.stub(); + bitcoind._checkReindex = sinon.stub().callsArg(1); + bitcoind._checkSyncedAndSubscribeZmqEvents = sinon.stub(); + bitcoind._stopSpawnedBitcoin = sinon.stub().callsArg(0); + sinon.spy(bitcoind, '_spawnChildProcess'); + bitcoind._spawnChildProcess(function(err) { + if (err) { + return done(err); + } + process.once('exit', function() { + setTimeout(function() { + bitcoind._spawnChildProcess.callCount.should.equal(2); + done(); + }, 5); + }); + process.emit('exit', 1); + }); + }); + it('will emit error during respawn', function(done) { + var process = new EventEmitter(); + var spawn = sinon.stub().returns(process); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFileSync: readFileSync + }, + child_process: { + spawn: spawn + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind.spawn = {}; + bitcoind.spawn.exec = 'bitcoind'; + bitcoind.spawn.datadir = '/tmp/bitcoin'; + bitcoind.spawn.configPath = '/tmp/bitcoin/bitcoin.conf'; + bitcoind.spawn.config = {}; + bitcoind.spawnRestartTime = 1; + bitcoind._loadTipFromNode = sinon.stub().callsArg(1); + bitcoind._initZmqSubSocket = sinon.stub(); + bitcoind._checkReindex = sinon.stub().callsArg(1); + bitcoind._checkSyncedAndSubscribeZmqEvents = sinon.stub(); + bitcoind._stopSpawnedBitcoin = sinon.stub().callsArg(0); + sinon.spy(bitcoind, '_spawnChildProcess'); + bitcoind._spawnChildProcess(function(err) { + if (err) { + return done(err); + } + bitcoind._spawnChildProcess = sinon.stub().callsArgWith(0, new Error('test')); + bitcoind.on('error', function(err) { + err.should.be.instanceOf(Error); + err.message.should.equal('test'); + done(); + }); + process.emit('exit', 1); + }); + }); + it('will NOT respawn bitcoind spawned process if shutting down', function(done) { + var process = new EventEmitter(); + var spawn = sinon.stub().returns(process); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFileSync: readFileSync + }, + child_process: { + spawn: spawn + } + }); + var config = { + node: { + network: bitcore.Networks.testnet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var bitcoind = new TestBitcoinService(config); + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind.spawn = {}; + bitcoind.spawn.exec = 'bitcoind'; + bitcoind.spawn.datadir = '/tmp/bitcoin'; + bitcoind.spawn.configPath = '/tmp/bitcoin/bitcoin.conf'; + bitcoind.spawn.config = {}; + bitcoind.spawnRestartTime = 1; + bitcoind._loadTipFromNode = sinon.stub().callsArg(1); + bitcoind._initZmqSubSocket = sinon.stub(); + bitcoind._checkReindex = sinon.stub().callsArg(1); + bitcoind._checkSyncedAndSubscribeZmqEvents = sinon.stub(); + bitcoind._stopSpawnedBitcoin = sinon.stub().callsArg(0); + sinon.spy(bitcoind, '_spawnChildProcess'); + bitcoind._spawnChildProcess(function(err) { + if (err) { + return done(err); + } + bitcoind.node.stopping = true; + process.once('exit', function() { + setTimeout(function() { + bitcoind._spawnChildProcess.callCount.should.equal(1); + done(); + }, 5); + }); + process.emit('exit', 1); + }); + }); + it('will give error after 60 retries', function(done) { + var process = new EventEmitter(); + var spawn = sinon.stub().returns(process); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFileSync: readFileSync + }, + child_process: { + spawn: spawn + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + bitcoind.startRetryInterval = 1; + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind.spawn = {}; + bitcoind.spawn.exec = 'testexec'; + bitcoind.spawn.configPath = 'testdir/bitcoin.conf'; + bitcoind.spawn.datadir = 'testdir'; + bitcoind.spawn.config = {}; + bitcoind.spawn.config.rpcport = 20001; + bitcoind.spawn.config.rpcuser = 'bitcoin'; + bitcoind.spawn.config.rpcpassword = 'password'; + bitcoind.spawn.config.zmqpubrawtx = 'tcp://127.0.0.1:30001'; + bitcoind._loadTipFromNode = sinon.stub().callsArgWith(1, new Error('test')); + bitcoind._spawnChildProcess(function(err) { + bitcoind._loadTipFromNode.callCount.should.equal(60); + err.should.be.instanceof(Error); + done(); + }); + }); + it('will give error from check reindex', function(done) { + var process = new EventEmitter(); + var spawn = sinon.stub().returns(process); + var TestBitcoinService = proxyquire('../../lib/services/bitcoind', { + fs: { + readFileSync: readFileSync + }, + child_process: { + spawn: spawn + } + }); + var bitcoind = new TestBitcoinService(baseConfig); + + bitcoind._loadSpawnConfiguration = sinon.stub(); + bitcoind.spawn = {}; + bitcoind.spawn.exec = 'testexec'; + bitcoind.spawn.configPath = 'testdir/bitcoin.conf'; + bitcoind.spawn.datadir = 'testdir'; + bitcoind.spawn.config = {}; + bitcoind.spawn.config.rpcport = 20001; + bitcoind.spawn.config.rpcuser = 'bitcoin'; + bitcoind.spawn.config.rpcpassword = 'password'; + bitcoind.spawn.config.zmqpubrawtx = 'tcp://127.0.0.1:30001'; + + bitcoind._loadTipFromNode = sinon.stub().callsArgWith(1, null); + bitcoind._initZmqSubSocket = sinon.stub(); + bitcoind._checkSyncedAndSubscribeZmqEvents = sinon.stub(); + bitcoind._checkReindex = sinon.stub().callsArgWith(1, new Error('test')); + + bitcoind._spawnChildProcess(function(err) { + err.should.be.instanceof(Error); + done(); + }); + }); + }); + + describe('#_connectProcess', function() { + it('will give error if connecting while shutting down', function(done) { + var config = { + node: { + network: bitcore.Networks.testnet + }, + spawn: { + datadir: 'testdir', + exec: 'testpath' + } + }; + var bitcoind = new BitcoinService(config); + bitcoind.node.stopping = true; + bitcoind.startRetryInterval = 100; + bitcoind._loadTipFromNode = sinon.stub(); + bitcoind._connectProcess({}, function(err) { + err.should.be.instanceof(Error); + err.message.should.match(/Stopping while trying to connect/); + bitcoind._loadTipFromNode.callCount.should.equal(0); + done(); + }); + }); + it('will give error from loadTipFromNode after 60 retries', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._loadTipFromNode = sinon.stub().callsArgWith(1, new Error('test')); + bitcoind.startRetryInterval = 1; + var config = {}; + bitcoind._connectProcess(config, function(err) { + err.should.be.instanceof(Error); + bitcoind._loadTipFromNode.callCount.should.equal(60); + done(); + }); + }); + it('will init zmq/rpc on node', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._initZmqSubSocket = sinon.stub(); + bitcoind._subscribeZmqEvents = sinon.stub(); + bitcoind._loadTipFromNode = sinon.stub().callsArgWith(1, null); + var config = {}; + bitcoind._connectProcess(config, function(err, node) { + should.not.exist(err); + bitcoind._loadTipFromNode.callCount.should.equal(1); + bitcoind._initZmqSubSocket.callCount.should.equal(1); + bitcoind._loadTipFromNode.callCount.should.equal(1); + should.exist(node); + should.exist(node.client); + done(); + }); + }); + }); + + describe('#start', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will give error if "spawn" and "connect" are both not configured', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.options = {}; + bitcoind.start(function(err) { + err.should.be.instanceof(Error); + err.message.should.match(/Bitcoin configuration options/); + }); + done(); + }); + it('will give error from spawnChildProcess', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._spawnChildProcess = sinon.stub().callsArgWith(0, new Error('test')); + bitcoind.options = { + spawn: {} + }; + bitcoind.start(function(err) { + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will give error from connectProcess', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._connectProcess = sinon.stub().callsArgWith(1, new Error('test')); + bitcoind.options = { + connect: [ + {} + ] + }; + bitcoind.start(function(err) { + bitcoind._connectProcess.callCount.should.equal(1); + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will push node from spawnChildProcess', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + bitcoind._initChain = sinon.stub().callsArg(0); + bitcoind._spawnChildProcess = sinon.stub().callsArgWith(0, null, node); + bitcoind.options = { + spawn: {} + }; + bitcoind.start(function(err) { + should.not.exist(err); + bitcoind.nodes.length.should.equal(1); + done(); + }); + }); + it('will push node from connectProcess', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._initChain = sinon.stub().callsArg(0); + var nodes = [{}]; + bitcoind._connectProcess = sinon.stub().callsArgWith(1, null, nodes); + bitcoind.options = { + connect: [ + {} + ] + }; + bitcoind.start(function(err) { + should.not.exist(err); + bitcoind._connectProcess.callCount.should.equal(1); + bitcoind.nodes.length.should.equal(1); + done(); + }); + }); + }); + + describe('#isSynced', function() { + it('will give error from syncPercentage', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, new Error('test')); + bitcoind.isSynced(function(err) { + should.exist(err); + err.message.should.equal('test'); + done(); + }); + }); + it('will give "true" if percentage is 100.00', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 100.00); + bitcoind.isSynced(function(err, synced) { + if (err) { + return done(err); + } + synced.should.equal(true); + done(); + }); + }); + it('will give "true" if percentage is 99.98', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 99.98); + bitcoind.isSynced(function(err, synced) { + if (err) { + return done(err); + } + synced.should.equal(true); + done(); + }); + }); + it('will give "false" if percentage is 99.49', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 99.49); + bitcoind.isSynced(function(err, synced) { + if (err) { + return done(err); + } + synced.should.equal(false); + done(); + }); + }); + it('will give "false" if percentage is 1', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 1); + bitcoind.isSynced(function(err, synced) { + if (err) { + return done(err); + } + synced.should.equal(false); + done(); + }); + }); + }); + + describe('#syncPercentage', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockchainInfo = sinon.stub().callsArgWith(0, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getBlockchainInfo: getBlockchainInfo + } + }); + bitcoind.syncPercentage(function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client getInfo and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockchainInfo = sinon.stub().callsArgWith(0, null, { + result: { + verificationprogress: '0.983821387' + } + }); + bitcoind.nodes.push({ + client: { + getBlockchainInfo: getBlockchainInfo + } + }); + bitcoind.syncPercentage(function(err, percentage) { + if (err) { + return done(err); + } + percentage.should.equal(98.3821387); + done(); + }); + }); + }); + + describe('#_normalizeAddressArg', function() { + it('will turn single address into array', function() { + var bitcoind = new BitcoinService(baseConfig); + var args = bitcoind._normalizeAddressArg('address'); + args.should.deep.equal(['address']); + }); + it('will keep an array as an array', function() { + var bitcoind = new BitcoinService(baseConfig); + var args = bitcoind._normalizeAddressArg(['address', 'address']); + args.should.deep.equal(['address', 'address']); + }); + }); + + describe('#getAddressBalance', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressBalance: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + var options = {}; + bitcoind.getAddressBalance(address, options, function(err) { + err.should.be.instanceof(Error); + done(); + }); + }); + it('will give balance', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getAddressBalance = sinon.stub().callsArgWith(1, null, { + result: { + received: 100000, + balance: 10000 + } + }); + bitcoind.nodes.push({ + client: { + getAddressBalance: getAddressBalance + } + }); + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + var options = {}; + bitcoind.getAddressBalance(address, options, function(err, data) { + if (err) { + return done(err); + } + data.balance.should.equal(10000); + data.received.should.equal(100000); + bitcoind.getAddressBalance(address, options, function(err, data2) { + if (err) { + return done(err); + } + data2.balance.should.equal(10000); + data2.received.should.equal(100000); + getAddressBalance.callCount.should.equal(1); + done(); + }); + }); + }); + }); + + describe('#getAddressUnspentOutputs', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressUtxos: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('will give results from client getaddressutxos', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 1, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 7679241, + height: 207111 + } + ]; + bitcoind.nodes.push({ + client: { + getAddressUtxos: sinon.stub().callsArgWith(1, null, { + result: expectedUtxos + }) + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(1); + utxos.should.deep.equal(expectedUtxos); + done(); + }); + }); + it('will use cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 1, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 7679241, + height: 207111 + } + ]; + var getAddressUtxos = sinon.stub().callsArgWith(1, null, { + result: expectedUtxos + }); + bitcoind.nodes.push({ + client: { + getAddressUtxos: getAddressUtxos + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(1); + utxos.should.deep.equal(expectedUtxos); + getAddressUtxos.callCount.should.equal(1); + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(1); + utxos.should.deep.equal(expectedUtxos); + getAddressUtxos.callCount.should.equal(1); + done(); + }); + }); + }); + it('will update with mempool results', function(done) { + var deltas = [ + { + txid: 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + satoshis: -7679241, + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + index: 0, + timestamp: 1461342707725, + prevtxid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + prevout: 1 + }, + { + txid: 'f637384e9f81f18767ea50e00bce58fc9848b6588a1130529eebba22a410155f', + satoshis: 100000, + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + index: 0, + timestamp: 1461342833133 + }, + { + txid: 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345', + satoshis: 400000, + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + index: 1, + timestamp: 1461342954813 + } + ]; + var bitcoind = new BitcoinService(baseConfig); + var confirmedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 1, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 7679241, + height: 207111 + } + ]; + var expectedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + outputIndex: 1, + satoshis: 400000, + script: '76a914809dc14496f99b6deb722cf46d89d22f4beb8efd88ac', + timestamp: 1461342954813, + txid: 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345' + }, + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + outputIndex: 0, + satoshis: 100000, + script: '76a914809dc14496f99b6deb722cf46d89d22f4beb8efd88ac', + timestamp: 1461342833133, + txid: 'f637384e9f81f18767ea50e00bce58fc9848b6588a1130529eebba22a410155f' + } + ]; + bitcoind.nodes.push({ + client: { + getAddressUtxos: sinon.stub().callsArgWith(1, null, { + result: confirmedUtxos + }), + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: deltas + }) + } + }); + var options = { + queryMempool: true + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(2); + utxos.should.deep.equal(expectedUtxos); + done(); + }); + }); + it('will update with mempool results with multiple outputs', function(done) { + var deltas = [ + { + txid: 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + satoshis: -7679241, + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + index: 0, + timestamp: 1461342707725, + prevtxid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + prevout: 1 + }, + { + txid: 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + satoshis: -7679241, + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + index: 1, + timestamp: 1461342707725, + prevtxid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + prevout: 2 + } + ]; + var bitcoind = new BitcoinService(baseConfig); + var confirmedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 1, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 7679241, + height: 207111 + }, + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 2, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 7679241, + height: 207111 + } + ]; + bitcoind.nodes.push({ + client: { + getAddressUtxos: sinon.stub().callsArgWith(1, null, { + result: confirmedUtxos + }), + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: deltas + }) + } + }); + var options = { + queryMempool: true + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(0); + done(); + }); + }); + it('will update with mempool results spending zero value output (likely never to happen)', function(done) { + var deltas = [ + { + txid: 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + satoshis: 0, + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + index: 0, + timestamp: 1461342707725, + prevtxid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + prevout: 1 + } + ]; + var bitcoind = new BitcoinService(baseConfig); + var confirmedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 1, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 0, + height: 207111 + } + ]; + bitcoind.nodes.push({ + client: { + getAddressUtxos: sinon.stub().callsArgWith(1, null, { + result: confirmedUtxos + }), + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: deltas + }) + } + }); + var options = { + queryMempool: true + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(0); + done(); + }); + }); + it('will not filter results if mempool is not spending', function(done) { + var deltas = [ + { + txid: 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + satoshis: 10000, + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + index: 0, + timestamp: 1461342707725 + } + ]; + var bitcoind = new BitcoinService(baseConfig); + var confirmedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 1, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 0, + height: 207111 + } + ]; + bitcoind.nodes.push({ + client: { + getAddressUtxos: sinon.stub().callsArgWith(1, null, { + result: confirmedUtxos + }), + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: deltas + }) + } + }); + var options = { + queryMempool: true + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(2); + done(); + }); + }); + it('it will handle error from getAddressMempool', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, {code: -1, message: 'test'}) + } + }); + var options = { + queryMempool: true + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err) { + err.should.be.instanceOf(Error); + done(); + }); + }); + it('should set query mempool if undefined', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getAddressMempool = sinon.stub().callsArgWith(1, {code: -1, message: 'test'}); + bitcoind.nodes.push({ + client: { + getAddressMempool: getAddressMempool + } + }); + var options = {}; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err) { + getAddressMempool.callCount.should.equal(1); + done(); + }); + }); + }); + + describe('#_getBalanceFromMempool', function() { + it('will sum satoshis', function() { + var bitcoind = new BitcoinService(baseConfig); + var deltas = [ + { + satoshis: -1000, + }, + { + satoshis: 2000, + }, + { + satoshis: -10, + } + ]; + var sum = bitcoind._getBalanceFromMempool(deltas); + sum.should.equal(990); + }); + }); + + describe('#_getTxidsFromMempool', function() { + it('will filter to txids', function() { + var bitcoind = new BitcoinService(baseConfig); + var deltas = [ + { + txid: 'txid0', + }, + { + txid: 'txid1', + }, + { + txid: 'txid2', + } + ]; + var txids = bitcoind._getTxidsFromMempool(deltas); + txids.length.should.equal(3); + txids[0].should.equal('txid0'); + txids[1].should.equal('txid1'); + txids[2].should.equal('txid2'); + }); + it('will not include duplicates', function() { + var bitcoind = new BitcoinService(baseConfig); + var deltas = [ + { + txid: 'txid0', + }, + { + txid: 'txid0', + }, + { + txid: 'txid1', + } + ]; + var txids = bitcoind._getTxidsFromMempool(deltas); + txids.length.should.equal(2); + txids[0].should.equal('txid0'); + txids[1].should.equal('txid1'); + }); + }); + + describe('#_getHeightRangeQuery', function() { + it('will detect range query', function() { + var bitcoind = new BitcoinService(baseConfig); + var options = { + start: 20, + end: 0 + }; + var rangeQuery = bitcoind._getHeightRangeQuery(options); + rangeQuery.should.equal(true); + }); + it('will get range properties', function() { + var bitcoind = new BitcoinService(baseConfig); + var options = { + start: 20, + end: 0 + }; + var clone = {}; + bitcoind._getHeightRangeQuery(options, clone); + clone.end.should.equal(20); + clone.start.should.equal(0); + }); + it('will throw error with invalid range', function() { + var bitcoind = new BitcoinService(baseConfig); + var options = { + start: 0, + end: 20 + }; + (function() { + bitcoind._getHeightRangeQuery(options); + }).should.throw('"end" is expected'); + }); + }); + + describe('#getAddressTxids', function() { + it('will give error from _getHeightRangeQuery', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._getHeightRangeQuery = sinon.stub().throws(new Error('test')); + bitcoind.getAddressTxids('address', {}, function(err) { + err.should.be.instanceOf(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will give rpc error from mempool query', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + var options = {}; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + }); + }); + it('will give rpc error from txids query', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressTxids: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + }); + }); + it('will get txid results', function(done) { + var expectedTxids = [ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + 'f637384e9f81f18767ea50e00bce58fc9848b6588a1130529eebba22a410155f', + 'f3c1ba3ef86a0420d6102e40e2cfc8682632ab95d09d86a27f5d466b9fa9da47', + '56fafeb01961831b926558d040c246b97709fd700adcaa916541270583e8e579', + 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2', + 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345', + 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9', + 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a', + 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693', + 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681' + ]; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressTxids: sinon.stub().callsArgWith(1, null, { + result: expectedTxids.reverse() + }) + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + txids.length.should.equal(expectedTxids.length); + txids.should.deep.equal(expectedTxids); + done(); + }); + }); + it('will get txid results from cache', function(done) { + var expectedTxids = [ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce' + ]; + var bitcoind = new BitcoinService(baseConfig); + var getAddressTxids = sinon.stub().callsArgWith(1, null, { + result: expectedTxids.reverse() + }); + bitcoind.nodes.push({ + client: { + getAddressTxids: getAddressTxids + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + txids.should.deep.equal(expectedTxids); + + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + txids.should.deep.equal(expectedTxids); + done(); + }); + }); + }); + it('will get txid results WITHOUT cache if rangeQuery and exclude mempool', function(done) { + var expectedTxids = [ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce' + ]; + var bitcoind = new BitcoinService(baseConfig); + var getAddressMempool = sinon.stub(); + var getAddressTxids = sinon.stub().callsArgWith(1, null, { + result: expectedTxids.reverse() + }); + bitcoind.nodes.push({ + client: { + getAddressTxids: getAddressTxids, + getAddressMempool: getAddressMempool + } + }); + var options = { + queryMempool: true, // start and end will exclude mempool + start: 4, + end: 2 + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + getAddressMempool.callCount.should.equal(0); + txids.should.deep.equal(expectedTxids); + + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(2); + getAddressMempool.callCount.should.equal(0); + txids.should.deep.equal(expectedTxids); + done(); + }); + }); + }); + it('will get txid results from cache and live mempool', function(done) { + var expectedTxids = [ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce' + ]; + var bitcoind = new BitcoinService(baseConfig); + var getAddressTxids = sinon.stub().callsArgWith(1, null, { + result: expectedTxids.reverse() + }); + var getAddressMempool = sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2' + }, + { + txid: 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345' + }, + { + txid: 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9' + } + ] + }); + bitcoind.nodes.push({ + client: { + getAddressTxids: getAddressTxids, + getAddressMempool: getAddressMempool + } + }); + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, {queryMempool: false}, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + txids.should.deep.equal(expectedTxids); + + bitcoind.getAddressTxids(address, {queryMempool: true}, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + txids.should.deep.equal([ + 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9', // mempool + 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345', // mempool + 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2', // mempool + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce' // confirmed + ]); + done(); + }); + }); + }); + }); + + describe('#_getConfirmationDetail', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'warn'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('should get 0 confirmation', function() { + var tx = new Transaction(txhex); + tx.height = -1; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.height = 10; + var confirmations = bitcoind._getConfirmationsDetail(tx); + confirmations.should.equal(0); + }); + it('should get 1 confirmation', function() { + var tx = new Transaction(txhex); + tx.height = 10; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.height = 10; + var confirmations = bitcoind._getConfirmationsDetail(tx); + confirmations.should.equal(1); + }); + it('should get 2 confirmation', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = new Transaction(txhex); + bitcoind.height = 11; + tx.height = 10; + var confirmations = bitcoind._getConfirmationsDetail(tx); + confirmations.should.equal(2); + }); + it('should get 0 confirmation with overflow', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = new Transaction(txhex); + bitcoind.height = 3; + tx.height = 10; + var confirmations = bitcoind._getConfirmationsDetail(tx); + log.warn.callCount.should.equal(1); + confirmations.should.equal(0); + }); + it('should get 1000 confirmation', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = new Transaction(txhex); + bitcoind.height = 1000; + tx.height = 1; + var confirmations = bitcoind._getConfirmationsDetail(tx); + confirmations.should.equal(1000); + }); + }); + + describe('#_getAddressDetailsForInput', function() { + it('will return if missing an address', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = {}; + bitcoind._getAddressDetailsForInput({}, 0, result, []); + should.not.exist(result.addresses); + should.not.exist(result.satoshis); + }); + it('will only add address if it matches', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = {}; + bitcoind._getAddressDetailsForInput({ + address: 'address1' + }, 0, result, ['address2']); + should.not.exist(result.addresses); + should.not.exist(result.satoshis); + }); + it('will instantiate if outputIndexes not defined', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = { + addresses: {} + }; + bitcoind._getAddressDetailsForInput({ + address: 'address1' + }, 0, result, ['address1']); + should.exist(result.addresses); + result.addresses['address1'].inputIndexes.should.deep.equal([0]); + result.addresses['address1'].outputIndexes.should.deep.equal([]); + }); + it('will push to inputIndexes', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = { + addresses: { + 'address1': { + inputIndexes: [1] + } + } + }; + bitcoind._getAddressDetailsForInput({ + address: 'address1' + }, 2, result, ['address1']); + should.exist(result.addresses); + result.addresses['address1'].inputIndexes.should.deep.equal([1, 2]); + }); + }); + + describe('#_getAddressDetailsForOutput', function() { + it('will return if missing an address', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = {}; + bitcoind._getAddressDetailsForOutput({}, 0, result, []); + should.not.exist(result.addresses); + should.not.exist(result.satoshis); + }); + it('will only add address if it matches', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = {}; + bitcoind._getAddressDetailsForOutput({ + address: 'address1' + }, 0, result, ['address2']); + should.not.exist(result.addresses); + should.not.exist(result.satoshis); + }); + it('will instantiate if outputIndexes not defined', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = { + addresses: {} + }; + bitcoind._getAddressDetailsForOutput({ + address: 'address1' + }, 0, result, ['address1']); + should.exist(result.addresses); + result.addresses['address1'].inputIndexes.should.deep.equal([]); + result.addresses['address1'].outputIndexes.should.deep.equal([0]); + }); + it('will push if outputIndexes defined', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = { + addresses: { + 'address1': { + outputIndexes: [0] + } + } + }; + bitcoind._getAddressDetailsForOutput({ + address: 'address1' + }, 1, result, ['address1']); + should.exist(result.addresses); + result.addresses['address1'].outputIndexes.should.deep.equal([0, 1]); + }); + }); + + describe('#_getAddressDetailsForTransaction', function() { + it('will calculate details for the transaction', function(done) { + /* jshint sub:true */ + var tx = { + inputs: [ + { + satoshis: 1000000000, + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW' + } + ], + outputs: [ + { + satoshis: 100000000, + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW' + }, + { + satoshis: 200000000, + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW' + }, + { + satoshis: 50000000, + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW' + }, + { + satoshis: 300000000, + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW' + }, + { + satoshis: 349990000, + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW' + } + ], + locktime: 0 + }; + var bitcoind = new BitcoinService(baseConfig); + var addresses = ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']; + var details = bitcoind._getAddressDetailsForTransaction(tx, addresses); + should.exist(details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']); + details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].inputIndexes.should.deep.equal([0]); + details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].outputIndexes.should.deep.equal([ + 0, 1, 2, 3, 4 + ]); + details.satoshis.should.equal(-10000); + done(); + }); + }); + + describe('#_getAddressDetailedTransaction', function() { + it('will get detailed transaction info', function(done) { + var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; + var tx = { + height: 20, + }; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getDetailedTransaction = sinon.stub().callsArgWith(1, null, tx); + bitcoind.height = 300; + var addresses = {}; + bitcoind._getAddressDetailsForTransaction = sinon.stub().returns({ + addresses: addresses, + satoshis: 1000, + }); + bitcoind._getAddressDetailedTransaction(txid, {}, function(err, details) { + if (err) { + return done(err); + } + details.addresses.should.equal(addresses); + details.satoshis.should.equal(1000); + details.confirmations.should.equal(281); + details.tx.should.equal(tx); + done(); + }); + }); + it('give error from getDetailedTransaction', function(done) { + var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getDetailedTransaction = sinon.stub().callsArgWith(1, new Error('test')); + bitcoind._getAddressDetailedTransaction(txid, {}, function(err) { + err.should.be.instanceof(Error); + done(); + }); + }); + }); + + describe('#_getAddressStrings', function() { + it('will get address strings from bitcore addresses', function() { + var addresses = [ + bitcore.Address('1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i'), + bitcore.Address('3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou'), + ]; + var bitcoind = new BitcoinService(baseConfig); + var strings = bitcoind._getAddressStrings(addresses); + strings[0].should.equal('1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i'); + strings[1].should.equal('3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou'); + }); + it('will get address strings from strings', function() { + var addresses = [ + '1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i', + '3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', + ]; + var bitcoind = new BitcoinService(baseConfig); + var strings = bitcoind._getAddressStrings(addresses); + strings[0].should.equal('1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i'); + strings[1].should.equal('3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou'); + }); + it('will get address strings from mixture of types', function() { + var addresses = [ + bitcore.Address('1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i'), + '3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', + ]; + var bitcoind = new BitcoinService(baseConfig); + var strings = bitcoind._getAddressStrings(addresses); + strings[0].should.equal('1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i'); + strings[1].should.equal('3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou'); + }); + it('will give error with unknown', function() { + var addresses = [ + bitcore.Address('1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i'), + 0, + ]; + var bitcoind = new BitcoinService(baseConfig); + (function() { + bitcoind._getAddressStrings(addresses); + }).should.throw(TypeError); + }); + }); + + describe('#_paginateTxids', function() { + it('slice txids based on "from" and "to" (3 to 13)', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var paginated = bitcoind._paginateTxids(txids, 3, 13); + paginated.should.deep.equal([3, 4, 5, 6, 7, 8, 9, 10]); + }); + it('slice txids based on "from" and "to" (0 to 3)', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var paginated = bitcoind._paginateTxids(txids, 0, 3); + paginated.should.deep.equal([0, 1, 2]); + }); + it('slice txids based on "from" and "to" (0 to 1)', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var paginated = bitcoind._paginateTxids(txids, 0, 1); + paginated.should.deep.equal([0]); + }); + it('will throw error if "from" is greater than "to"', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + (function() { + bitcoind._paginateTxids(txids, 1, 0); + }).should.throw('"from" (1) is expected to be less than "to"'); + }); + it('will handle string numbers', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var paginated = bitcoind._paginateTxids(txids, '1', '3'); + paginated.should.deep.equal([1, 2]); + }); + }); + + describe('#getAddressHistory', function() { + var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; + it('will give error with "from" and "to" range that exceeds max size', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getAddressHistory(address, {from: 0, to: 51}, function(err) { + should.exist(err); + err.message.match(/^\"from/); + done(); + }); + }); + it('will give error with "from" and "to" order is reversed', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, []); + bitcoind.getAddressHistory(address, {from: 51, to: 0}, function(err) { + should.exist(err); + err.message.match(/^\"from/); + done(); + }); + }); + it('will give error from _getAddressDetailedTransaction', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, ['txid']); + bitcoind._getAddressDetailedTransaction = sinon.stub().callsArgWith(2, new Error('test')); + bitcoind.getAddressHistory(address, {}, function(err) { + should.exist(err); + err.message.should.equal('test'); + done(); + }); + }); + it('will give an error if length of addresses is too long', function(done) { + var addresses = []; + for (var i = 0; i < 101; i++) { + addresses.push(address); + } + var bitcoind = new BitcoinService(baseConfig); + bitcoind.maxAddressesQuery = 100; + bitcoind.getAddressHistory(addresses, {}, function(err) { + should.exist(err); + err.message.match(/Maximum/); + done(); + }); + }); + it('give error from getAddressTxids', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, new Error('test')); + bitcoind.getAddressHistory('address', {}, function(err) { + should.exist(err); + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will paginate', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._getAddressDetailedTransaction = function(txid, options, callback) { + callback(null, txid); + }; + var txids = ['one', 'two', 'three', 'four']; + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, txids); + bitcoind.getAddressHistory('address', {from: 1, to: 3}, function(err, data) { + if (err) { + return done(err); + } + data.items.length.should.equal(2); + data.items.should.deep.equal(['two', 'three']); + done(); + }); + }); + }); + + describe('#getAddressSummary', function() { + var txid1 = '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8'; + var txid2 = '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d'; + var txid3 = '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247'; + var memtxid1 = 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92'; + var memtxid2 = 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce'; + it('will handle error from getAddressTxids', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', + } + ] + }) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, new Error('test')); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, {}); + var address = ''; + var options = {}; + bitcoind.getAddressSummary(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will handle error from getAddressBalance', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', + } + ] + }) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, {}); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, new Error('test'), {}); + var address = ''; + var options = {}; + bitcoind.getAddressSummary(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will handle error from client getAddressMempool', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, {}); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, {}); + var address = ''; + var options = {}; + bitcoind.getAddressSummary(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(Error); + err.message.should.equal('Test error'); + done(); + }); + }); + it('should set all properties', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: memtxid1, + satoshis: -1000000 + }, + { + txid: memtxid2, + satoshis: 99999 + } + ] + }) + } + }); + sinon.spy(bitcoind, '_paginateTxids'); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, [txid1, txid2, txid3]); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, { + received: 30 * 1e8, + balance: 20 * 1e8 + }); + var address = '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj'; + var options = {}; + bitcoind.getAddressSummary(address, options, function(err, summary) { + bitcoind._paginateTxids.callCount.should.equal(1); + bitcoind._paginateTxids.args[0][1].should.equal(0); + bitcoind._paginateTxids.args[0][2].should.equal(1000); + summary.appearances.should.equal(3); + summary.totalReceived.should.equal(3000000000); + summary.totalSpent.should.equal(1000000000); + summary.balance.should.equal(2000000000); + summary.unconfirmedAppearances.should.equal(2); + summary.unconfirmedBalance.should.equal(-900001); + summary.txids.should.deep.equal([ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92', + '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', + '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d', + '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247' + ]); + done(); + }); + }); + it('will give error with "from" and "to" range that exceeds max size', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: memtxid1, + satoshis: -1000000 + }, + { + txid: memtxid2, + satoshis: 99999 + } + ] + }) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, [txid1, txid2, txid3]); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, { + received: 30 * 1e8, + balance: 20 * 1e8 + }); + var address = '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj'; + var options = { + from: 0, + to: 1001 + }; + bitcoind.getAddressSummary(address, options, function(err) { + should.exist(err); + err.message.match(/^\"from/); + done(); + }); + }); + it('will get from cache with noTxList', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: memtxid1, + satoshis: -1000000 + }, + { + txid: memtxid2, + satoshis: 99999 + } + ] + }) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, [txid1, txid2, txid3]); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, { + received: 30 * 1e8, + balance: 20 * 1e8 + }); + var address = '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj'; + var options = { + noTxList: true + }; + function checkSummary(summary) { + summary.appearances.should.equal(3); + summary.totalReceived.should.equal(3000000000); + summary.totalSpent.should.equal(1000000000); + summary.balance.should.equal(2000000000); + summary.unconfirmedAppearances.should.equal(2); + summary.unconfirmedBalance.should.equal(-900001); + should.not.exist(summary.txids); + } + bitcoind.getAddressSummary(address, options, function(err, summary) { + checkSummary(summary); + bitcoind.getAddressTxids.callCount.should.equal(1); + bitcoind.getAddressBalance.callCount.should.equal(1); + bitcoind.getAddressSummary(address, options, function(err, summary) { + checkSummary(summary); + bitcoind.getAddressTxids.callCount.should.equal(1); + bitcoind.getAddressBalance.callCount.should.equal(1); + done(); + }); + }); + }); + it('will skip querying the mempool with queryMempool set to false', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getAddressMempool = sinon.stub(); + bitcoind.nodes.push({ + client: { + getAddressMempool: getAddressMempool + } + }); + sinon.spy(bitcoind, '_paginateTxids'); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, [txid1, txid2, txid3]); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, { + received: 30 * 1e8, + balance: 20 * 1e8 + }); + var address = '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj'; + var options = { + queryMempool: false + }; + bitcoind.getAddressSummary(address, options, function() { + getAddressMempool.callCount.should.equal(0); + done(); + }); + }); + it('will give error from _paginateTxids', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getAddressMempool = sinon.stub(); + bitcoind.nodes.push({ + client: { + getAddressMempool: getAddressMempool + } + }); + sinon.spy(bitcoind, '_paginateTxids'); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, [txid1, txid2, txid3]); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, { + received: 30 * 1e8, + balance: 20 * 1e8 + }); + bitcoind._paginateTxids = sinon.stub().throws(new Error('test')); + var address = '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj'; + var options = { + queryMempool: false + }; + bitcoind.getAddressSummary(address, options, function(err) { + err.should.be.instanceOf(Error); + err.message.should.equal('test'); + done(); + }); + }); + }); + + describe('#getRawBlock', function() { + var blockhash = '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'; + var blockhex = '0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; + it('will give rcp error from client getblockhash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getBlockHash: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + bitcoind.getRawBlock(10, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('will give rcp error from client getblock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getBlock: sinon.stub().callsArgWith(2, {code: -1, message: 'Test error'}) + } + }); + bitcoind.getRawBlock(blockhash, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('will try all nodes for getblock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockWithError = sinon.stub().callsArgWith(2, {code: -1, message: 'Test error'}); + bitcoind.tryAllInterval = 1; + bitcoind.nodes.push({ + client: { + getBlock: getBlockWithError + } + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlockWithError + } + }); + bitcoind.nodes.push({ + client: { + getBlock: sinon.stub().callsArgWith(2, null, { + result: blockhex + }) + } + }); + bitcoind.getRawBlock(blockhash, function(err, buffer) { + if (err) { + return done(err); + } + buffer.should.be.instanceof(Buffer); + getBlockWithError.callCount.should.equal(2); + done(); + }); + }); + it('will get block from cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockhex + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlock + } + }); + bitcoind.getRawBlock(blockhash, function(err, buffer) { + if (err) { + return done(err); + } + buffer.should.be.instanceof(Buffer); + getBlock.callCount.should.equal(1); + bitcoind.getRawBlock(blockhash, function(err, buffer) { + if (err) { + return done(err); + } + buffer.should.be.instanceof(Buffer); + getBlock.callCount.should.equal(1); + done(); + }); + }); + }); + it('will get block by height', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockhex + }); + var getBlockHash = sinon.stub().callsArgWith(1, null, { + result: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlock, + getBlockHash: getBlockHash + } + }); + bitcoind.getRawBlock(0, function(err, buffer) { + if (err) { + return done(err); + } + buffer.should.be.instanceof(Buffer); + getBlock.callCount.should.equal(1); + getBlockHash.callCount.should.equal(1); + done(); + }); + }); + }); + + describe('#getBlock', function() { + var blockhex = '0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; + it('will give an rpc error from client getblock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, {code: -1, message: 'Test error'}); + var getBlockHash = sinon.stub().callsArgWith(1, null, {}); + bitcoind.nodes.push({ + client: { + getBlock: getBlock, + getBlockHash: getBlockHash + } + }); + bitcoind.getBlock(0, function(err) { + err.should.be.instanceof(Error); + done(); + }); + }); + it('will give an rpc error from client getblockhash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHash = sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}); + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind.getBlock(0, function(err) { + err.should.be.instanceof(Error); + done(); + }); + }); + it('will getblock as bitcore object from height', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockhex + }); + var getBlockHash = sinon.stub().callsArgWith(1, null, { + result: '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b' + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlock, + getBlockHash: getBlockHash + } + }); + bitcoind.getBlock(0, function(err, block) { + should.not.exist(err); + getBlock.args[0][0].should.equal('00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'); + getBlock.args[0][1].should.equal(false); + block.should.be.instanceof(bitcore.Block); + done(); + }); + }); + it('will getblock as bitcore object', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockhex + }); + var getBlockHash = sinon.stub(); + bitcoind.nodes.push({ + client: { + getBlock: getBlock, + getBlockHash: getBlockHash + } + }); + bitcoind.getBlock('00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b', function(err, block) { + should.not.exist(err); + getBlockHash.callCount.should.equal(0); + getBlock.callCount.should.equal(1); + getBlock.args[0][0].should.equal('00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'); + getBlock.args[0][1].should.equal(false); + block.should.be.instanceof(bitcore.Block); + done(); + }); + }); + it('will get block from cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockhex + }); + var getBlockHash = sinon.stub(); + bitcoind.nodes.push({ + client: { + getBlock: getBlock, + getBlockHash: getBlockHash + } + }); + var hash = '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'; + bitcoind.getBlock(hash, function(err, block) { + should.not.exist(err); + getBlockHash.callCount.should.equal(0); + getBlock.callCount.should.equal(1); + block.should.be.instanceof(bitcore.Block); + bitcoind.getBlock(hash, function(err, block) { + should.not.exist(err); + getBlockHash.callCount.should.equal(0); + getBlock.callCount.should.equal(1); + block.should.be.instanceof(bitcore.Block); + done(); + }); + }); + }); + it('will get block from cache with height (but not height)', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockhex + }); + var getBlockHash = sinon.stub().callsArgWith(1, null, { + result: '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b' + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlock, + getBlockHash: getBlockHash + } + }); + bitcoind.getBlock(0, function(err, block) { + should.not.exist(err); + getBlockHash.callCount.should.equal(1); + getBlock.callCount.should.equal(1); + block.should.be.instanceof(bitcore.Block); + bitcoind.getBlock(0, function(err, block) { + should.not.exist(err); + getBlockHash.callCount.should.equal(2); + getBlock.callCount.should.equal(1); + block.should.be.instanceof(bitcore.Block); + done(); + }); + }); + }); + }); + + describe('#getBlockHashesByTimestamp', function() { + it('should give an rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHashes = sinon.stub().callsArgWith(2, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getBlockHashes: getBlockHashes + } + }); + bitcoind.getBlockHashesByTimestamp(1441911000, 1441914000, function(err, hashes) { + should.exist(err); + err.message.should.equal('error'); + done(); + }); + }); + it('should get the correct block hashes', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var block1 = '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'; + var block2 = '000000000383752a55a0b2891ce018fd0fdc0b6352502772b034ec282b4a1bf6'; + var getBlockHashes = sinon.stub().callsArgWith(2, null, { + result: [block2, block1] + }); + bitcoind.nodes.push({ + client: { + getBlockHashes: getBlockHashes + } + }); + bitcoind.getBlockHashesByTimestamp(1441914000, 1441911000, function(err, hashes) { + should.not.exist(err); + hashes.should.deep.equal([block2, block1]); + done(); + }); + }); + }); + + describe('#getBlockHeader', function() { + var blockhash = '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'; + it('will give error from getBlockHash', function() { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHash = sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}); + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind.getBlockHeader(10, function(err) { + err.should.be.instanceof(Error); + }); + }); + it('it will give rpc error from client getblockheader', function() { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHeader = sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}); + bitcoind.nodes.push({ + client: { + getBlockHeader: getBlockHeader + } + }); + bitcoind.getBlockHeader(blockhash, function(err) { + err.should.be.instanceof(Error); + }); + }); + it('it will give rpc error from client getblockhash', function() { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHeader = sinon.stub(); + var getBlockHash = sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}); + bitcoind.nodes.push({ + client: { + getBlockHeader: getBlockHeader, + getBlockHash: getBlockHash + } + }); + bitcoind.getBlockHeader(0, function(err) { + err.should.be.instanceof(Error); + }); + }); + it('will give result from client getblockheader (from height)', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = { + hash: '0000000000000a817cd3a74aec2f2246b59eb2cbb1ad730213e6c4a1d68ec2f6', + version: 536870912, + confirmations: 5, + height: 828781, + chainWork: '00000000000000000000000000000000000000000000000ad467352c93bc6a3b', + prevHash: '0000000000000504235b2aff578a48470dbf6b94dafa9b3703bbf0ed554c9dd9', + nextHash: '00000000000000eedd967ec155f237f033686f0924d574b946caf1b0e89551b8', + merkleRoot: '124e0f3fb5aa268f102b0447002dd9700988fc570efcb3e0b5b396ac7db437a9', + time: 1462979126, + medianTime: 1462976771, + nonce: 2981820714, + bits: '1a13ca10', + difficulty: 847779.0710240941 + }; + var getBlockHeader = sinon.stub().callsArgWith(1, null, { + result: { + hash: '0000000000000a817cd3a74aec2f2246b59eb2cbb1ad730213e6c4a1d68ec2f6', + version: 536870912, + confirmations: 5, + height: 828781, + chainwork: '00000000000000000000000000000000000000000000000ad467352c93bc6a3b', + previousblockhash: '0000000000000504235b2aff578a48470dbf6b94dafa9b3703bbf0ed554c9dd9', + nextblockhash: '00000000000000eedd967ec155f237f033686f0924d574b946caf1b0e89551b8', + merkleroot: '124e0f3fb5aa268f102b0447002dd9700988fc570efcb3e0b5b396ac7db437a9', + time: 1462979126, + mediantime: 1462976771, + nonce: 2981820714, + bits: '1a13ca10', + difficulty: 847779.0710240941 + } + }); + var getBlockHash = sinon.stub().callsArgWith(1, null, { + result: blockhash + }); + bitcoind.nodes.push({ + client: { + getBlockHeader: getBlockHeader, + getBlockHash: getBlockHash + } + }); + bitcoind.getBlockHeader(0, function(err, blockHeader) { + should.not.exist(err); + getBlockHeader.args[0][0].should.equal(blockhash); + blockHeader.should.deep.equal(result); + }); + }); + it('will give result from client getblockheader (from hash)', function() { + var bitcoind = new BitcoinService(baseConfig); + var result = { + hash: '0000000000000a817cd3a74aec2f2246b59eb2cbb1ad730213e6c4a1d68ec2f6', + version: 536870912, + confirmations: 5, + height: 828781, + chainWork: '00000000000000000000000000000000000000000000000ad467352c93bc6a3b', + prevHash: '0000000000000504235b2aff578a48470dbf6b94dafa9b3703bbf0ed554c9dd9', + nextHash: '00000000000000eedd967ec155f237f033686f0924d574b946caf1b0e89551b8', + merkleRoot: '124e0f3fb5aa268f102b0447002dd9700988fc570efcb3e0b5b396ac7db437a9', + time: 1462979126, + medianTime: 1462976771, + nonce: 2981820714, + bits: '1a13ca10', + difficulty: 847779.0710240941 + }; + var getBlockHeader = sinon.stub().callsArgWith(1, null, { + result: { + hash: '0000000000000a817cd3a74aec2f2246b59eb2cbb1ad730213e6c4a1d68ec2f6', + version: 536870912, + confirmations: 5, + height: 828781, + chainwork: '00000000000000000000000000000000000000000000000ad467352c93bc6a3b', + previousblockhash: '0000000000000504235b2aff578a48470dbf6b94dafa9b3703bbf0ed554c9dd9', + nextblockhash: '00000000000000eedd967ec155f237f033686f0924d574b946caf1b0e89551b8', + merkleroot: '124e0f3fb5aa268f102b0447002dd9700988fc570efcb3e0b5b396ac7db437a9', + time: 1462979126, + mediantime: 1462976771, + nonce: 2981820714, + bits: '1a13ca10', + difficulty: 847779.0710240941 + } + }); + var getBlockHash = sinon.stub(); + bitcoind.nodes.push({ + client: { + getBlockHeader: getBlockHeader, + getBlockHash: getBlockHash + } + }); + bitcoind.getBlockHeader(blockhash, function(err, blockHeader) { + should.not.exist(err); + getBlockHash.callCount.should.equal(0); + blockHeader.should.deep.equal(result); + }); + }); + }); + + describe('#_maybeGetBlockHash', function() { + it('will not get block hash with an address', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHash = sinon.stub(); + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind._maybeGetBlockHash('2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br', function(err, hash) { + if (err) { + return done(err); + } + getBlockHash.callCount.should.equal(0); + hash.should.equal('2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'); + done(); + }); + }); + it('will not get block hash with non zero-nine numeric string', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHash = sinon.stub(); + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind._maybeGetBlockHash('109a', function(err, hash) { + if (err) { + return done(err); + } + getBlockHash.callCount.should.equal(0); + hash.should.equal('109a'); + done(); + }); + }); + it('will get the block hash if argument is a number', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHash = sinon.stub().callsArgWith(1, null, { + result: 'blockhash' + }); + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind._maybeGetBlockHash(10, function(err, hash) { + if (err) { + return done(err); + } + hash.should.equal('blockhash'); + getBlockHash.callCount.should.equal(1); + done(); + }); + }); + it('will get the block hash if argument is a number (as string)', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHash = sinon.stub().callsArgWith(1, null, { + result: 'blockhash' + }); + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind._maybeGetBlockHash('10', function(err, hash) { + if (err) { + return done(err); + } + hash.should.equal('blockhash'); + getBlockHash.callCount.should.equal(1); + done(); + }); + }); + it('will try multiple nodes if one fails', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHash = sinon.stub().callsArgWith(1, null, { + result: 'blockhash' + }); + getBlockHash.onCall(0).callsArgWith(1, {code: -1, message: 'test'}); + bitcoind.tryAllInterval = 1; + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind._maybeGetBlockHash(10, function(err, hash) { + if (err) { + return done(err); + } + hash.should.equal('blockhash'); + getBlockHash.callCount.should.equal(2); + done(); + }); + }); + it('will give error from getBlockHash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHash = sinon.stub().callsArgWith(1, {code: -1, message: 'test'}); + bitcoind.tryAllInterval = 1; + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind.nodes.push({ + client: { + getBlockHash: getBlockHash + } + }); + bitcoind._maybeGetBlockHash(10, function(err, hash) { + getBlockHash.callCount.should.equal(2); + err.should.be.instanceOf(Error); + err.message.should.equal('test'); + err.code.should.equal(-1); + done(); + }); + }); + }); + + describe('#getBlockOverview', function() { + var blockhash = '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'; + it('will handle error from maybeGetBlockHash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._maybeGetBlockHash = sinon.stub().callsArgWith(1, new Error('test')); + bitcoind.getBlockOverview(blockhash, function(err) { + err.should.be.instanceOf(Error); + done(); + }); + }); + it('will give error from client.getBlock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, {code: -1, message: 'test'}); + bitcoind.nodes.push({ + client: { + getBlock: getBlock + } + }); + bitcoind.getBlockOverview(blockhash, function(err) { + err.should.be.instanceOf(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will give expected result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var blockResult = { + hash: blockhash, + version: 536870912, + confirmations: 5, + height: 828781, + chainwork: '00000000000000000000000000000000000000000000000ad467352c93bc6a3b', + previousblockhash: '0000000000000504235b2aff578a48470dbf6b94dafa9b3703bbf0ed554c9dd9', + nextblockhash: '00000000000000eedd967ec155f237f033686f0924d574b946caf1b0e89551b8', + merkleroot: '124e0f3fb5aa268f102b0447002dd9700988fc570efcb3e0b5b396ac7db437a9', + time: 1462979126, + mediantime: 1462976771, + nonce: 2981820714, + bits: '1a13ca10', + difficulty: 847779.0710240941 + }; + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockResult + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlock + } + }); + function checkBlock(blockOverview) { + blockOverview.hash.should.equal('00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'); + blockOverview.version.should.equal(536870912); + blockOverview.confirmations.should.equal(5); + blockOverview.height.should.equal(828781); + blockOverview.chainWork.should.equal('00000000000000000000000000000000000000000000000ad467352c93bc6a3b'); + blockOverview.prevHash.should.equal('0000000000000504235b2aff578a48470dbf6b94dafa9b3703bbf0ed554c9dd9'); + blockOverview.nextHash.should.equal('00000000000000eedd967ec155f237f033686f0924d574b946caf1b0e89551b8'); + blockOverview.merkleRoot.should.equal('124e0f3fb5aa268f102b0447002dd9700988fc570efcb3e0b5b396ac7db437a9'); + blockOverview.time.should.equal(1462979126); + blockOverview.medianTime.should.equal(1462976771); + blockOverview.nonce.should.equal(2981820714); + blockOverview.bits.should.equal('1a13ca10'); + blockOverview.difficulty.should.equal(847779.0710240941); + } + bitcoind.getBlockOverview(blockhash, function(err, blockOverview) { + if (err) { + return done(err); + } + checkBlock(blockOverview); + bitcoind.getBlockOverview(blockhash, function(err, blockOverview) { + checkBlock(blockOverview); + getBlock.callCount.should.equal(1); + done(); + }); + }); + }); + }); + + describe('#estimateFee', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var estimateFee = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + estimateFee: estimateFee + } + }); + bitcoind.estimateFee(1, function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client estimateFee and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var estimateFee = sinon.stub().callsArgWith(1, null, { + result: -1 + }); + bitcoind.nodes.push({ + client: { + estimateFee: estimateFee + } + }); + bitcoind.estimateFee(1, function(err, feesPerKb) { + if (err) { + return done(err); + } + feesPerKb.should.equal(-1); + done(); + }); + }); + }); + + describe('#sendTransaction', function(done) { + var tx = bitcore.Transaction(txhex); + it('will give rpc error', function() { + var bitcoind = new BitcoinService(baseConfig); + var sendRawTransaction = sinon.stub().callsArgWith(2, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + sendRawTransaction: sendRawTransaction + } + }); + bitcoind.sendTransaction(txhex, function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + }); + }); + it('will send to client and get hash', function() { + var bitcoind = new BitcoinService(baseConfig); + var sendRawTransaction = sinon.stub().callsArgWith(2, null, { + result: tx.hash + }); + bitcoind.nodes.push({ + client: { + sendRawTransaction: sendRawTransaction + } + }); + bitcoind.sendTransaction(txhex, function(err, hash) { + if (err) { + return done(err); + } + hash.should.equal(tx.hash); + }); + }); + it('will send to client with absurd fees and get hash', function() { + var bitcoind = new BitcoinService(baseConfig); + var sendRawTransaction = sinon.stub().callsArgWith(2, null, { + result: tx.hash + }); + bitcoind.nodes.push({ + client: { + sendRawTransaction: sendRawTransaction + } + }); + bitcoind.sendTransaction(txhex, {allowAbsurdFees: true}, function(err, hash) { + if (err) { + return done(err); + } + hash.should.equal(tx.hash); + }); + }); + it('missing callback will throw error', function() { + var bitcoind = new BitcoinService(baseConfig); + var sendRawTransaction = sinon.stub().callsArgWith(2, null, { + result: tx.hash + }); + bitcoind.nodes.push({ + client: { + sendRawTransaction: sendRawTransaction + } + }); + var transaction = bitcore.Transaction(); + (function() { + bitcoind.sendTransaction(transaction); + }).should.throw(Error); + }); + }); + + describe('#getRawTransaction', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getRawTransaction('txid', function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will try all nodes', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.tryAllInterval = 1; + var getRawTransactionWithError = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + var getRawTransaction = sinon.stub().callsArgWith(1, null, { + result: txhex + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransactionWithError + } + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransactionWithError + } + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getRawTransaction('txid', function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx); + tx.should.be.an.instanceof(Buffer); + done(); + }); + }); + it('will get from cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(1, null, { + result: txhex + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getRawTransaction('txid', function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx); + tx.should.be.an.instanceof(Buffer); + + bitcoind.getRawTransaction('txid', function(err, tx) { + should.exist(tx); + tx.should.be.an.instanceof(Buffer); + getRawTransaction.callCount.should.equal(1); + done(); + }); + }); + }); + }); + + describe('#getTransaction', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getTransaction('txid', function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will try all nodes', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.tryAllInterval = 1; + var getRawTransactionWithError = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + var getRawTransaction = sinon.stub().callsArgWith(1, null, { + result: txhex + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransactionWithError + } + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransactionWithError + } + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getTransaction('txid', function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx); + tx.should.be.an.instanceof(bitcore.Transaction); + done(); + }); + }); + it('will get from cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(1, null, { + result: txhex + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getTransaction('txid', function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx); + tx.should.be.an.instanceof(bitcore.Transaction); + + bitcoind.getTransaction('txid', function(err, tx) { + should.exist(tx); + tx.should.be.an.instanceof(bitcore.Transaction); + getRawTransaction.callCount.should.equal(1); + done(); + }); + + }); + }); + }); + + describe('#getDetailedTransaction', function() { + var txBuffer = new Buffer('01000000016f95980911e01c2c664b3e78299527a47933aac61a515930a8fe0213d1ac9abe01000000da0047304402200e71cda1f71e087c018759ba3427eb968a9ea0b1decd24147f91544629b17b4f0220555ee111ed0fc0f751ffebf097bdf40da0154466eb044e72b6b3dcd5f06807fa01483045022100c86d6c8b417bff6cc3bbf4854c16bba0aaca957e8f73e19f37216e2b06bb7bf802205a37be2f57a83a1b5a8cc511dc61466c11e9ba053c363302e7b99674be6a49fc0147522102632178d046673c9729d828cfee388e121f497707f810c131e0d3fc0fe0bd66d62103a0951ec7d3a9da9de171617026442fcd30f34d66100fab539853b43f508787d452aeffffffff0240420f000000000017a9148a31d53a448c18996e81ce67811e5fb7da21e4468738c9d6f90000000017a9148ce5408cfeaddb7ccb2545ded41ef478109454848700000000', 'hex'); + var info = { + blockHash: '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6', + height: 530482, + timestamp: 1439559434000, + buffer: txBuffer + }; + var rpcRawTransaction = { + hex: txBuffer.toString('hex'), + blockhash: info.blockHash, + height: info.height, + version: 1, + locktime: 411451, + time: info.timestamp, + vin: [ + { + valueSat: 110, + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW', + txid: '3d003413c13eec3fa8ea1fe8bbff6f40718c66facffe2544d7516c9e2900cac2', + sequence: 0xFFFFFFFF, + vout: 0, + scriptSig: { + hex: 'scriptSigHex', + asm: 'scriptSigAsm' + } + } + ], + vout: [ + { + spentTxId: '4316b98e7504073acd19308b4b8c9f4eeb5e811455c54c0ebfe276c0b1eb6315', + spentIndex: 2, + spentHeight: 100, + valueSat: 100, + scriptPubKey: { + hex: '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac', + asm: 'OP_DUP OP_HASH160 0b2f0a0c31bfe0406b0ccc1381fdbe311946dadc OP_EQUALVERIFY OP_CHECKSIG', + addresses: ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'] + } + } + ] + }; + it('should give a transaction with height and timestamp', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, {code: -1, message: 'Test error'}) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getDetailedTransaction(txid, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('should give a transaction with all properties', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(2, null, { + result: rpcRawTransaction + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + function checkTx(tx) { + /* jshint maxstatements: 30 */ + should.exist(tx); + should.not.exist(tx.coinbase); + should.equal(tx.hex, txBuffer.toString('hex')); + should.equal(tx.blockHash, '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6'); + should.equal(tx.height, 530482); + should.equal(tx.blockTimestamp, 1439559434000); + should.equal(tx.version, 1); + should.equal(tx.locktime, 411451); + should.equal(tx.feeSatoshis, 10); + should.equal(tx.inputSatoshis, 110); + should.equal(tx.outputSatoshis, 100); + should.equal(tx.hash, txid); + var input = tx.inputs[0]; + should.equal(input.prevTxId, '3d003413c13eec3fa8ea1fe8bbff6f40718c66facffe2544d7516c9e2900cac2'); + should.equal(input.outputIndex, 0); + should.equal(input.satoshis, 110); + should.equal(input.sequence, 0xFFFFFFFF); + should.equal(input.script, 'scriptSigHex'); + should.equal(input.scriptAsm, 'scriptSigAsm'); + should.equal(input.address, 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'); + var output = tx.outputs[0]; + should.equal(output.satoshis, 100); + should.equal(output.script, '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'); + should.equal(output.scriptAsm, 'OP_DUP OP_HASH160 0b2f0a0c31bfe0406b0ccc1381fdbe311946dadc OP_EQUALVERIFY OP_CHECKSIG'); + should.equal(output.address, 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'); + should.equal(output.spentTxId, '4316b98e7504073acd19308b4b8c9f4eeb5e811455c54c0ebfe276c0b1eb6315'); + should.equal(output.spentIndex, 2); + should.equal(output.spentHeight, 100); + } + bitcoind.getDetailedTransaction(txid, function(err, tx) { + if (err) { + return done(err); + } + checkTx(tx); + bitcoind.getDetailedTransaction(txid, function(err, tx) { + if (err) { + return done(err); + } + checkTx(tx); + getRawTransaction.callCount.should.equal(1); + done(); + }); + }); + }); + it('should set coinbase to true', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var rawTransaction = JSON.parse((JSON.stringify(rpcRawTransaction))); + delete rawTransaction.vin[0]; + rawTransaction.vin = [ + { + coinbase: 'abcdef' + } + ]; + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, null, { + result: rawTransaction + }) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getDetailedTransaction(txid, function(err, tx) { + should.exist(tx); + should.equal(tx.coinbase, true); + done(); + }); + }); + it('will not include address if address length is zero', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var rawTransaction = JSON.parse((JSON.stringify(rpcRawTransaction))); + rawTransaction.vout[0].scriptPubKey.addresses = []; + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, null, { + result: rawTransaction + }) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getDetailedTransaction(txid, function(err, tx) { + should.exist(tx); + should.equal(tx.outputs[0].address, null); + done(); + }); + }); + it('will not include address if address length is greater than 1', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var rawTransaction = JSON.parse((JSON.stringify(rpcRawTransaction))); + rawTransaction.vout[0].scriptPubKey.addresses = ['one', 'two']; + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, null, { + result: rawTransaction + }) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getDetailedTransaction(txid, function(err, tx) { + should.exist(tx); + should.equal(tx.outputs[0].address, null); + done(); + }); + }); + it('will handle scriptPubKey.addresses not being set', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var rawTransaction = JSON.parse((JSON.stringify(rpcRawTransaction))); + delete rawTransaction.vout[0].scriptPubKey['addresses']; + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, null, { + result: rawTransaction + }) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getDetailedTransaction(txid, function(err, tx) { + should.exist(tx); + should.equal(tx.outputs[0].address, null); + done(); + }); + }); + it('will not include script if input missing scriptSig or coinbase', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var rawTransaction = JSON.parse((JSON.stringify(rpcRawTransaction))); + delete rawTransaction.vin[0].scriptSig; + delete rawTransaction.vin[0].coinbase; + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, null, { + result: rawTransaction + }) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getDetailedTransaction(txid, function(err, tx) { + should.exist(tx); + should.equal(tx.inputs[0].script, null); + done(); + }); + }); + it('will set height to -1 if missing height', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var rawTransaction = JSON.parse((JSON.stringify(rpcRawTransaction))); + delete rawTransaction.height; + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, null, { + result: rawTransaction + }) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getDetailedTransaction(txid, function(err, tx) { + should.exist(tx); + should.equal(tx.height, -1); + done(); + }); + }); + }); + + describe('#getBestBlockHash', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getBestBlockHash: getBestBlockHash + } + }); + bitcoind.getBestBlockHash(function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client getInfo and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, { + result: 'besthash' + }); + bitcoind.nodes.push({ + client: { + getBestBlockHash: getBestBlockHash + } + }); + bitcoind.getBestBlockHash(function(err, hash) { + if (err) { + return done(err); + } + should.exist(hash); + hash.should.equal('besthash'); + done(); + }); + }); + }); + + describe('#getSpentInfo', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getSpentInfo = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getSpentInfo: getSpentInfo + } + }); + bitcoind.getSpentInfo({}, function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will empty object when not found', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getSpentInfo = sinon.stub().callsArgWith(1, {message: 'test', code: -5}); + bitcoind.nodes.push({ + client: { + getSpentInfo: getSpentInfo + } + }); + bitcoind.getSpentInfo({}, function(err, info) { + should.not.exist(err); + info.should.deep.equal({}); + done(); + }); + }); + it('will call client getSpentInfo and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getSpentInfo = sinon.stub().callsArgWith(1, null, { + result: { + txid: 'txid', + index: 10, + height: 101 + } + }); + bitcoind.nodes.push({ + client: { + getSpentInfo: getSpentInfo + } + }); + bitcoind.getSpentInfo({}, function(err, info) { + if (err) { + return done(err); + } + info.txid.should.equal('txid'); + info.index.should.equal(10); + info.height.should.equal(101); + done(); + }); + }); + }); + + describe('#getInfo', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getInfo = sinon.stub().callsArgWith(0, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getInfo: getInfo + } + }); + bitcoind.getInfo(function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client getInfo and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.node.getNetworkName = sinon.stub().returns('testnet'); + var getInfo = sinon.stub().callsArgWith(0, null, { + result: { + version: 1, + protocolversion: 1, + blocks: 1, + timeoffset: 1, + connections: 1, + proxy: '', + difficulty: 1, + testnet: true, + relayfee: 10, + errors: '' + } + }); + bitcoind.nodes.push({ + client: { + getInfo: getInfo + } + }); + bitcoind.getInfo(function(err, info) { + if (err) { + return done(err); + } + should.exist(info); + should.equal(info.version, 1); + should.equal(info.protocolVersion, 1); + should.equal(info.blocks, 1); + should.equal(info.timeOffset, 1); + should.equal(info.connections, 1); + should.equal(info.proxy, ''); + should.equal(info.difficulty, 1); + should.equal(info.testnet, true); + should.equal(info.relayFee, 10); + should.equal(info.errors, ''); + info.network.should.equal('testnet'); + done(); + }); + }); + }); + + describe('#generateBlock', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var generate = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + generate: generate + } + }); + bitcoind.generateBlock(10, function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client generate and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var generate = sinon.stub().callsArgWith(1, null, { + result: ['hash'] + }); + bitcoind.nodes.push({ + client: { + generate: generate + } + }); + bitcoind.generateBlock(10, function(err, hashes) { + if (err) { + return done(err); + } + hashes.length.should.equal(1); + hashes[0].should.equal('hash'); + done(); + }); + }); + }); - bitcoind[x[0]].apply(bitcoind, args); - stub.callCount.should.equal(1); - stub.args[0].length.should.equal(x[1]); + describe('#stop', function() { + it('will callback if spawn is not set', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.stop(done); + }); + it('will exit spawned process', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.spawn = {}; + bitcoind.spawn.process = new EventEmitter(); + bitcoind.spawn.process.kill = sinon.stub(); + bitcoind.stop(done); + bitcoind.spawn.process.kill.callCount.should.equal(1); + bitcoind.spawn.process.kill.args[0][0].should.equal('SIGINT'); + bitcoind.spawn.process.emit('exit', 0); + }); + it('will give error with non-zero exit status code', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.spawn = {}; + bitcoind.spawn.process = new EventEmitter(); + bitcoind.spawn.process.kill = sinon.stub(); + bitcoind.stop(function(err) { + err.should.be.instanceof(Error); + err.code.should.equal(1); + done(); + }); + bitcoind.spawn.process.kill.callCount.should.equal(1); + bitcoind.spawn.process.kill.args[0][0].should.equal('SIGINT'); + bitcoind.spawn.process.emit('exit', 1); + }); + it('will stop after timeout', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.shutdownTimeout = 300; + bitcoind.spawn = {}; + bitcoind.spawn.process = new EventEmitter(); + bitcoind.spawn.process.kill = sinon.stub(); + bitcoind.stop(function(err) { + err.should.be.instanceof(Error); + done(); }); + bitcoind.spawn.process.kill.callCount.should.equal(1); + bitcoind.spawn.process.kill.args[0][0].should.equal('SIGINT'); }); }); diff --git a/test/services/db.unit.js b/test/services/db.unit.js deleted file mode 100644 index 60568cce6..000000000 --- a/test/services/db.unit.js +++ /dev/null @@ -1,1038 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var sinon = require('sinon'); -var EventEmitter = require('events').EventEmitter; -var proxyquire = require('proxyquire'); -var index = require('../../'); -var DB = index.services.DB; -var blockData = require('../data/livenet-345003.json'); -var bitcore = require('bitcore-lib'); -var Networks = bitcore.Networks; -var Block = bitcore.Block; -var BufferUtil = bitcore.util.buffer; -var Transaction = bitcore.Transaction; -var transactionData = require('../data/bitcoin-transactions.json'); -var chainHashes = require('../data/hashes.json'); -var chainData = require('../data/testnet-blocks.json'); -var errors = index.errors; -var memdown = require('memdown'); -var levelup = require('levelup'); - -describe('DB Service', function() { - - function hexlebuf(hexString){ - return BufferUtil.reverse(new Buffer(hexString, 'hex')); - } - - function lebufhex(buf) { - return BufferUtil.reverse(buf).toString('hex'); - } - - var baseConfig = { - node: { - network: Networks.testnet, - datadir: 'testdir' - }, - store: memdown - }; - - var genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); - - - describe('#_setDataPath', function() { - it('should set the database path', function() { - var config = { - node: { - network: Networks.livenet, - datadir: process.env.HOME + '/.bitcoin' - }, - store: memdown - }; - var db = new DB(config); - db.dataPath.should.equal(process.env.HOME + '/.bitcoin/bitcore-node.db'); - }); - it('should load the db for testnet', function() { - var config = { - node: { - network: Networks.testnet, - datadir: process.env.HOME + '/.bitcoin' - }, - store: memdown - }; - var db = new DB(config); - db.dataPath.should.equal(process.env.HOME + '/.bitcoin/testnet3/bitcore-node.db'); - }); - it('error with unknown network', function() { - var config = { - node: { - network: 'unknown', - datadir: process.env.HOME + '/.bitcoin' - }, - store: memdown - }; - (function() { - var db = new DB(config); - }).should.throw('Unknown network'); - }); - it('should load the db with regtest', function() { - // Switch to use regtest - Networks.enableRegtest(); - var regtest = Networks.get('regtest'); - var config = { - node: { - network: regtest, - datadir: process.env.HOME + '/.bitcoin' - }, - store: memdown - }; - var db = new DB(config); - db.dataPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-node.db'); - Networks.disableRegtest(); - }); - }); - - describe('#_checkVersion', function() { - var config = { - node: { - network: Networks.get('testnet'), - datadir: 'testdir' - }, - store: memdown - }; - it('will handle an error while retrieving the tip', function() { - var db = new DB(config); - db.store = {}; - db.store.get = sinon.stub().callsArgWith(2, new Error('test')); - db._checkVersion(function(err) { - should.exist(err); - err.message.should.equal('test'); - }); - }); - it('will handle an error while retrieving the version', function() { - var db = new DB(config); - db.store = {}; - db.store.get = function() {}; - var callCount = 0; - sinon.stub(db.store, 'get', function(key, options, callback) { - if (callCount === 1) { - return callback(new Error('test')); - } - callCount++; - setImmediate(callback); - }); - db._checkVersion(function(err) { - should.exist(err); - err.message.should.equal('test'); - }); - }); - it('will NOT check the version if a tip is not found', function(done) { - var db = new DB(config); - db.store = {}; - db.store.get = sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()); - db._checkVersion(done); - }); - it('will NOT give an error if the versions match', function(done) { - var db = new DB(config); - db.store = {}; - db.store.get = function() {}; - var callCount = 0; - sinon.stub(db.store, 'get', function(key, options, callback) { - if (callCount === 1) { - var versionBuffer = new Buffer(new Array(4)); - versionBuffer.writeUInt32BE(2); - return callback(null, versionBuffer); - } - callCount++; - setImmediate(callback); - }); - db.version = 2; - db._checkVersion(done); - }); - it('will give an error if the versions do NOT match', function(done) { - var db = new DB(config); - db.store = {}; - db.store.get = function() {}; - var callCount = 0; - sinon.stub(db.store, 'get', function(key, options, callback) { - if (callCount === 1) { - var versionBuffer = new Buffer(new Array(4)); - versionBuffer.writeUInt32BE(2); - return callback(null, versionBuffer); - } - callCount++; - setImmediate(callback); - }); - db.version = 3; - db._checkVersion(function(err) { - should.exist(err); - err.message.should.match(/^The version of the database/); - done(); - }); - }); - it('will default to version 1 if the version is NOT found', function(done) { - var db = new DB(config); - db.store = {}; - db.store.get = function() {}; - var callCount = 0; - sinon.stub(db.store, 'get', function(key, options, callback) { - if (callCount === 1) { - return callback(new levelup.errors.NotFoundError()); - } - callCount++; - setImmediate(callback); - }); - db.version = 1; - db._checkVersion(done); - }); - }); - - describe('#_setVersion', function() { - var config = { - node: { - network: Networks.get('testnet'), - datadir: 'testdir' - }, - store: memdown - }; - it('will give an error from the store', function(done) { - var db = new DB(config); - db.store = {}; - db.store.put = sinon.stub().callsArgWith(2, new Error('test')); - db._setVersion(function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - it('will set the version', function(done) { - var db = new DB(config); - db.store = {}; - db.store.put = sinon.stub().callsArgWith(2, null); - db.version = 5; - db._setVersion(function(err) { - if (err) { - return done(err); - } - db.store.put.args[0][0].should.deep.equal(new Buffer('ff', 'hex')); - db.store.put.args[0][1].should.deep.equal(new Buffer('00000005', 'hex')); - done(); - }); - }); - }); - - describe('#start', function() { - var TestDB; - - before(function() { - TestDB = proxyquire('../../lib/services/db', { - fs: { - existsSync: sinon.stub().returns(true) - }, - levelup: sinon.stub() - }); - }); - - it('should emit ready', function(done) { - var db = new TestDB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - on: sinon.spy(), - genesisBuffer: genesisBuffer - }; - db.loadTip = sinon.stub().callsArg(0); - db.connectBlock = sinon.stub().callsArg(1); - db._checkVersion = sinon.stub().callsArg(0); - db._setVersion = sinon.stub().callsArg(0); - db.sync = sinon.stub(); - var readyFired = false; - db.on('ready', function() { - readyFired = true; - }); - db.start(function() { - readyFired.should.equal(true); - done(); - }); - }); - - it('will call sync when there is a new tip', function(done) { - var db = new TestDB(baseConfig); - db.node.services = {}; - db.node.services.bitcoind = new EventEmitter(); - db.node.services.bitcoind.genesisBuffer = genesisBuffer; - db.loadTip = sinon.stub().callsArg(0); - db.connectBlock = sinon.stub().callsArg(1); - db._checkVersion = sinon.stub().callsArg(0); - db._setVersion = sinon.stub().callsArg(0); - db.sync = sinon.stub(); - db.start(function() { - db.sync = function() { - done(); - }; - db.node.services.bitcoind.emit('tip', 10); - }); - }); - - it('will not call sync when there is a new tip and shutting down', function(done) { - var db = new TestDB(baseConfig); - db.node.services = {}; - db.node.services.bitcoind = new EventEmitter(); - db.node.services.bitcoind.syncPercentage = sinon.spy(); - db.node.services.bitcoind.genesisBuffer = genesisBuffer; - db.loadTip = sinon.stub().callsArg(0); - db.connectBlock = sinon.stub().callsArg(1); - db._checkVersion = sinon.stub().callsArg(0); - db._setVersion = sinon.stub().callsArg(0); - db.node.stopping = true; - db.sync = sinon.stub(); - db.start(function() { - db.sync.callCount.should.equal(1); - db.node.services.bitcoind.once('tip', function() { - db.sync.callCount.should.equal(1); - done(); - }); - db.node.services.bitcoind.emit('tip', 10); - }); - }); - - }); - - describe('#stop', function() { - it('should wait until db has stopped syncing before closing leveldb', function(done) { - var db = new DB(baseConfig); - db.store = { - close: sinon.stub().callsArg(0) - }; - db.bitcoindSyncing = true; - - db.stop(function(err) { - should.not.exist(err); - done(); - }); - - setTimeout(function() { - db.bitcoindSyncing = false; - }, 15); - }); - }); - - describe('#getTransaction', function() { - it('will return a NotFound error', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransaction: sinon.stub().callsArgWith(2, null, null) - }; - var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; - db.getTransaction(txid, true, function(err) { - err.should.be.instanceof(errors.Transaction.NotFound); - done(); - }); - }); - it('will return an error from bitcoind', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransaction: sinon.stub().callsArgWith(2, new Error('test error')) - }; - var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; - db.getTransaction(txid, true, function(err) { - err.message.should.equal('test error'); - done(); - }); - }); - it('will return an error from bitcoind', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransaction: sinon.stub().callsArgWith(2, null, new Buffer(transactionData[0].hex, 'hex')) - }; - var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; - db.getTransaction(txid, true, function(err, tx) { - if (err) { - throw err; - } - should.exist(tx); - done(); - }); - }); - }); - - describe('#loadTip', function() { - it('genesis block if no metadata is found in the db', function(done) { - var db = new DB(baseConfig); - db.genesis = Block.fromBuffer(genesisBuffer); - db.store = { - get: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()) - }; - db.connectBlock = sinon.stub().callsArg(1); - db.sync = sinon.stub(); - db.loadTip(function() { - should.exist(db.tip); - db.tip.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'); - done(); - }); - }); - - it('tip from the database if it exists', function(done) { - var node = { - network: Networks.testnet, - datadir: 'testdir', - services: { - bitcoind: { - genesisBuffer: genesisBuffer, - on: sinon.stub(), - getBlockIndex: sinon.stub().returns({height: 1}) - } - } - }; - var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; - var tip = Block.fromBuffer(genesisBuffer); - var db = new DB({node: node}); - db.store = { - get: sinon.stub().callsArgWith(2, null, new Buffer(tipHash, 'hex')) - }; - db.getBlock = sinon.stub().callsArgWith(1, null, tip); - db.sync = sinon.stub(); - db.loadTip(function() { - should.exist(db.tip); - db.tip.hash.should.equal(tipHash); - db.tip.__height.should.equal(1); - done(); - }); - }); - - it('give error if levelup error', function(done) { - var node = { - network: Networks.testnet, - datadir: 'testdir', - services: { - bitcoind: { - genesisBuffer: genesisBuffer, - on: sinon.stub() - } - } - }; - var db = new DB({node: node}); - db.store = { - get: sinon.stub().callsArgWith(2, new Error('test')) - }; - db.loadTip(function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - - it('should try 3 times before giving error from getBlock', function(done) { - var node = { - network: Networks.testnet, - datadir: 'testdir', - services: { - bitcoind: { - genesisBuffer: genesisBuffer, - on: sinon.stub(), - getBlockIndex: sinon.stub().returns({height: 1}) - } - } - }; - var db = new DB({node: node}); - db.retryInterval = 10; - var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; - db.store = { - get: sinon.stub().callsArgWith(2, null, new Buffer(tipHash, 'hex')) - }; - db.getBlock = sinon.stub().callsArgWith(1, new Error('test')); - db.loadTip(function(err) { - should.exist(err); - db.getBlock.callCount.should.equal(3); - err.message.should.equal('test'); - done(); - }); - }); - }); - - describe('#getBlock', function() { - var db = new DB(baseConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var expectedBlock = Block.fromBuffer(blockBuffer); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer) - }; - - it('should get the block from bitcoin daemon', function(done) { - db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) { - should.not.exist(err); - block.hash.should.equal(expectedBlock.hash); - done(); - }); - }); - it('should give an error when bitcoind.js gives an error', function(done) { - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = {}; - db.node.services.bitcoind.getBlock = sinon.stub().callsArgWith(1, new Error('error')); - db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - }); - - describe('#getBlockHashesByTimestamp', function() { - it('should get the correct block hashes', function(done) { - var db = new DB(baseConfig); - var readStream = new EventEmitter(); - db.store = { - createReadStream: sinon.stub().returns(readStream) - }; - - var block1 = { - hash: '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b', - timestamp: 1441911909 - }; - - var block2 = { - hash: '000000000383752a55a0b2891ce018fd0fdc0b6352502772b034ec282b4a1bf6', - timestamp: 1441913112 - }; - - db.getBlockHashesByTimestamp(1441914000, 1441911000, function(err, hashes) { - should.not.exist(err); - hashes.should.deep.equal([block2.hash, block1.hash]); - done(); - }); - - readStream.emit('data', { - key: db._encodeBlockIndexKey(block2.timestamp), - value: db._encodeBlockIndexValue(block2.hash) - }); - - readStream.emit('data', { - key: db._encodeBlockIndexKey(block1.timestamp), - value: db._encodeBlockIndexValue(block1.hash) - }); - - readStream.emit('close'); - }); - - it('should give an error if the stream has an error', function(done) { - var db = new DB(baseConfig); - var readStream = new EventEmitter(); - db.store = { - createReadStream: sinon.stub().returns(readStream) - }; - - db.getBlockHashesByTimestamp(1441911000, 1441914000, function(err, hashes) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - - readStream.emit('error', new Error('error')); - - readStream.emit('close'); - }); - - it('should give an error if the timestamp is out of range', function(done) { - var db = new DB(baseConfig); - var readStream = new EventEmitter(); - db.store = { - createReadStream: sinon.stub().returns(readStream) - }; - - db.getBlockHashesByTimestamp(-1, -5, function(err, hashes) { - should.exist(err); - err.message.should.equal('Invalid Argument: timestamp out of bounds'); - done(); - }); - }); - }); - - describe('#getPrevHash', function() { - it('should return prevHash from bitcoind', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getBlockIndex: sinon.stub().returns({ - prevHash: 'prevhash' - }) - }; - - db.getPrevHash('hash', function(err, prevHash) { - should.not.exist(err); - prevHash.should.equal('prevhash'); - done(); - }); - }); - - it('should give an error if bitcoind could not find it', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getBlockIndex: sinon.stub().returns(null) - }; - - db.getPrevHash('hash', function(err, prevHash) { - should.exist(err); - done(); - }); - }); - }); - - describe('#getTransactionWithBlockInfo', function() { - it('should give a transaction with height and timestamp', function(done) { - var txBuffer = new Buffer('01000000016f95980911e01c2c664b3e78299527a47933aac61a515930a8fe0213d1ac9abe01000000da0047304402200e71cda1f71e087c018759ba3427eb968a9ea0b1decd24147f91544629b17b4f0220555ee111ed0fc0f751ffebf097bdf40da0154466eb044e72b6b3dcd5f06807fa01483045022100c86d6c8b417bff6cc3bbf4854c16bba0aaca957e8f73e19f37216e2b06bb7bf802205a37be2f57a83a1b5a8cc511dc61466c11e9ba053c363302e7b99674be6a49fc0147522102632178d046673c9729d828cfee388e121f497707f810c131e0d3fc0fe0bd66d62103a0951ec7d3a9da9de171617026442fcd30f34d66100fab539853b43f508787d452aeffffffff0240420f000000000017a9148a31d53a448c18996e81ce67811e5fb7da21e4468738c9d6f90000000017a9148ce5408cfeaddb7ccb2545ded41ef478109454848700000000', 'hex'); - var info = { - blockHash: '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6', - height: 530482, - timestamp: 1439559434000, - buffer: txBuffer - }; - - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, info) - }; - - db.getTransactionWithBlockInfo('2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f', true, function(err, tx) { - should.not.exist(err); - tx.__blockHash.should.equal(info.blockHash); - tx.__height.should.equal(info.height); - tx.__timestamp.should.equal(info.timestamp); - done(); - }); - }); - it('should give an error if one occurred', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('error')) - }; - - db.getTransactionWithBlockInfo('tx', true, function(err, tx) { - should.exist(err); - done(); - }); - }); - }); - - describe('#sendTransaction', function() { - it('should handle a basic serialized transaction hex string', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - sendTransaction: sinon.stub().returns('txid') - }; - - var tx = 'hexstring'; - db.sendTransaction(tx, function(err, txid) { - should.not.exist(err); - txid.should.equal('txid'); - done(); - }); - }); - it('should give the txid on success', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - sendTransaction: sinon.stub().returns('txid') - }; - - var tx = new Transaction(); - tx.serialize = sinon.stub().returns('txstring'); - db.sendTransaction(tx, function(err, txid) { - should.not.exist(err); - tx.serialize.callCount.should.equal(1); - txid.should.equal('txid'); - done(); - }); - }); - it('should give an error if bitcoind threw an error', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - sendTransaction: sinon.stub().throws(new Error('error')) - }; - - var tx = new Transaction(); - tx.serialize = sinon.stub().returns('txstring'); - db.sendTransaction(tx, function(err, txid) { - tx.serialize.callCount.should.equal(1); - should.exist(err); - done(); - }); - }); - }); - - describe('#estimateFee', function() { - it('should pass along the fee from bitcoind', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - estimateFee: sinon.stub().returns(1000) - }; - - db.estimateFee(5, function(err, fee) { - should.not.exist(err); - fee.should.equal(1000); - db.node.services.bitcoind.estimateFee.args[0][0].should.equal(5); - done(); - }); - }); - }); - - describe('#connectBlock', function() { - it('should remove block from mempool and call blockHandler with true', function(done) { - var db = new DB(baseConfig); - db.mempool = { - removeBlock: sinon.stub() - }; - db.runAllBlockHandlers = sinon.stub().callsArg(2); - db.connectBlock({hash: 'hash'}, function(err) { - should.not.exist(err); - db.runAllBlockHandlers.args[0][1].should.equal(true); - done(); - }); - }); - }); - - describe('#disconnectBlock', function() { - it('should call blockHandler with false', function(done) { - var db = new DB(baseConfig); - db.runAllBlockHandlers = sinon.stub().callsArg(2); - db.disconnectBlock({hash: 'hash'}, function(err) { - should.not.exist(err); - db.runAllBlockHandlers.args[0][1].should.equal(false); - done(); - }); - }); - }); - - describe('#runAllBlockHandlers', function() { - var db = new DB(baseConfig); - var Service1 = function() {}; - Service1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']); - var Service2 = function() {}; - Service2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']); - var Service3 = function() {}; - var Service4 = function() {}; - Service4.prototype.blockHandler = sinon.stub().callsArgWith(2, null, 'bad-value'); - db.node = {}; - db.node.services = { - service1: new Service1(), - service2: new Service2() - }; - db.store = { - batch: sinon.stub().callsArg(1) - }; - - var block = { - hash: '00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863', - header: { - timestamp: 1441906365 - } - }; - - it('should call blockHandler in all services and perform operations', function(done) { - db.runAllBlockHandlers(block, true, function(err) { - should.not.exist(err); - var tipOp = { - type: 'put', - key: DB.PREFIXES.TIP, - value: new Buffer('00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863', 'hex') - } - var blockOp = { - type: 'put', - key: db._encodeBlockIndexKey(1441906365), - value: db._encodeBlockIndexValue('00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863') - }; - db.store.batch.args[0][0].should.deep.equal([tipOp, blockOp, 'op1', 'op2', 'op3', 'op4', 'op5']); - done(); - }); - }); - - it('should give an error if one of the services gives an error', function(done) { - var Service3 = function() {}; - Service3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error')); - db.node.services.service3 = new Service3(); - - db.runAllBlockHandlers(block, true, function(err) { - should.exist(err); - done(); - }); - }); - - it('should not give an error if a service does not have blockHandler', function(done) { - db.node = {}; - db.node.services = { - service3: new Service3() - }; - - db.runAllBlockHandlers(block, true, function(err) { - should.not.exist(err); - done(); - }); - }); - - it('should throw an error if blockHandler gives unexpected result', function() { - db.node = {}; - db.node.services = { - service4: new Service4() - }; - - (function() { - db.runAllBlockHandlers(block, true, function(err) { - should.not.exist(err); - }); - }).should.throw('bitcore.ErrorInvalidArgument'); - }); - }); - - describe('#getAPIMethods', function() { - it('should return the correct db methods', function() { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - var methods = db.getAPIMethods(); - methods.length.should.equal(6); - }); - }); - - describe('#findCommonAncestor', function() { - it('will find an ancestor 6 deep', function(done) { - var db = new DB(baseConfig); - db.tip = { - hash: chainHashes[chainHashes.length - 1] - }; - - var expectedAncestor = chainHashes[chainHashes.length - 6]; - - var mainBlocks = {}; - for(var i = chainHashes.length - 1; i > chainHashes.length - 10; i--) { - var hash = chainHashes[i]; - var prevHash = hexlebuf(chainHashes[i - 1]); - mainBlocks[hash] = { - header: { - prevHash: prevHash - } - }; - } - - var forkedBlocks = { - 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': { - header: { - prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a') - }, - hash: 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82' - }, - '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': { - header: { - prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c') - }, - hash: '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a' - }, - 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': { - header: { - prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31') - }, - hash: 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c' - }, - '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': { - header: { - prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453') - }, - hash: '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31' - }, - 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': { - header: { - prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618') - }, - hash: 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453' - }, - '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': { - header: { - prevHash: hexlebuf(expectedAncestor) - }, - hash: '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618' - } - }; - db.node.services = {}; - db.node.services.bitcoind = { - getBlockIndex: function(hash) { - var forkedBlock = forkedBlocks[hash]; - var mainBlock = mainBlocks[hash]; - var prevHash; - if (forkedBlock && forkedBlock.header.prevHash) { - prevHash = BufferUtil.reverse(forkedBlock.header.prevHash).toString('hex'); - } else if (mainBlock && mainBlock.header.prevHash){ - prevHash = BufferUtil.reverse(mainBlock.header.prevHash).toString('hex'); - } else { - return null; - } - return { - prevHash: prevHash - }; - } - }; - var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82']; - db.findCommonAncestor(block, function(err, ancestorHash) { - if (err) { - throw err; - } - ancestorHash.should.equal(expectedAncestor); - done(); - }); - }); - }); - - describe('#syncRewind', function() { - it('will undo blocks 6 deep', function() { - var db = new DB(baseConfig); - var ancestorHash = chainHashes[chainHashes.length - 6]; - db.tip = { - __height: 10, - hash: chainHashes[chainHashes.length], - header: { - prevHash: hexlebuf(chainHashes[chainHashes.length - 1]) - } - }; - db.emit = sinon.stub(); - db.getBlock = function(hash, callback) { - setImmediate(function() { - for(var i = chainHashes.length; i > 0; i--) { - var block = { - hash: chainHashes[i], - header: { - prevHash: hexlebuf(chainHashes[i - 1]) - } - }; - if (chainHashes[i] === hash) { - callback(null, block); - } - } - }); - }; - db.node.services = {}; - db.disconnectBlock = function(block, callback) { - setImmediate(callback); - }; - db.findCommonAncestor = function(block, callback) { - setImmediate(function() { - callback(null, ancestorHash); - }); - }; - var forkedBlock = {}; - db.syncRewind(forkedBlock, function(err) { - if (err) { - throw err; - } - db.tip.__height.should.equal(4); - db.tip.hash.should.equal(ancestorHash); - }); - }); - }); - - describe('#sync', function() { - var node = new EventEmitter(); - var syncConfig = { - node: node, - store: memdown - }; - syncConfig.node.network = Networks.testnet; - syncConfig.node.datadir = 'testdir'; - it('will get and add block up to the tip height', function(done) { - var db = new DB(syncConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var block = Block.fromBuffer(blockBuffer); - db.node.services = {}; - db.node.services.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true), - height: 1 - }; - db.tip = { - __height: 0, - hash: lebufhex(block.header.prevHash) - }; - db.emit = sinon.stub(); - db.cache = { - hashes: {} - }; - db.connectBlock = function(block, callback) { - db.tip.__height += 1; - callback(); - }; - db.node.once('synced', function() { - done(); - }); - db.sync(); - }); - it('will exit and emit error with error from bitcoind.getBlock', function(done) { - var db = new DB(syncConfig); - db.node.services = {}; - db.node.services.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, new Error('test error')), - height: 1 - }; - db.tip = { - __height: 0 - }; - db.node.on('error', function(err) { - err.message.should.equal('test error'); - done(); - }); - db.sync(); - }); - it('will stop syncing when the node is stopping', function(done) { - var db = new DB(syncConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var block = Block.fromBuffer(blockBuffer); - db.node.services = {}; - db.node.services.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true), - height: 1 - }; - db.tip = { - __height: 0, - hash: block.prevHash - }; - db.emit = sinon.stub(); - db.cache = { - hashes: {} - }; - db.connectBlock = function(block, callback) { - db.tip.__height += 1; - callback(); - }; - db.node.stopping = true; - var synced = false; - db.node.once('synced', function() { - synced = true; - }); - db.sync(); - setTimeout(function() { - synced.should.equal(false); - done(); - }, 10); - }); - }); - -}); diff --git a/test/services/web.unit.js b/test/services/web.unit.js index 4bc5760f5..3d0ff593b 100644 --- a/test/services/web.unit.js +++ b/test/services/web.unit.js @@ -5,6 +5,9 @@ var sinon = require('sinon'); var EventEmitter = require('events').EventEmitter; var proxyquire = require('proxyquire'); +var index = require('../../lib'); +var log = index.log; + var httpStub = { createServer: sinon.spy() }; @@ -22,20 +25,29 @@ var fakeSocket = new EventEmitter(); fakeSocket.on('test/event1', function(data) { data.should.equal('testdata'); - done(); }); fakeSocketListener.emit('connection', fakeSocket); - fakeSocket.emit('subscribe', 'test/event1'); - - var WebService = proxyquire('../../lib/services/web', {http: httpStub, https: httpsStub, fs: fsStub}); describe('WebService', function() { var defaultNode = new EventEmitter(); + describe('@constructor', function() { + it('will set socket rpc settings', function() { + var web = new WebService({node: defaultNode, enableSocketRPC: false}); + web.enableSocketRPC.should.equal(false); + + var web2 = new WebService({node: defaultNode, enableSocketRPC: true}); + web2.enableSocketRPC.should.equal(true); + + var web3 = new WebService({node: defaultNode}); + web3.enableSocketRPC.should.equal(WebService.DEFAULT_SOCKET_RPC); + }); + }); + describe('#start', function() { beforeEach(function() { httpStub.createServer.reset(); @@ -201,8 +213,43 @@ describe('WebService', function() { }); }); + describe('#_getRemoteAddress', function() { + it('will get remote address from cloudflare header', function() { + var web = new WebService({node: defaultNode}); + var socket = {}; + socket.conn = {}; + socket.client = {}; + socket.client.request = {}; + socket.client.request.headers = { + 'cf-connecting-ip': '127.0.0.1' + }; + var remoteAddress = web._getRemoteAddress(socket); + remoteAddress.should.equal('127.0.0.1'); + }); + it('will get remote address from connection', function() { + var web = new WebService({node: defaultNode}); + var socket = {}; + socket.conn = {}; + socket.conn.remoteAddress = '127.0.0.1'; + socket.client = {}; + socket.client.request = {}; + socket.client.request.headers = {}; + var remoteAddress = web._getRemoteAddress(socket); + remoteAddress.should.equal('127.0.0.1'); + }); + }); + describe('#socketHandler', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + var bus = new EventEmitter(); + bus.remoteAddress = '127.0.0.1'; var Module1 = function() {}; Module1.prototype.getPublishEvents = function() { @@ -232,10 +279,33 @@ describe('WebService', function() { done(); }; socket = new EventEmitter(); + socket.conn = {}; + socket.conn.remoteAddress = '127.0.0.1'; + socket.client = {}; + socket.client.request = {}; + socket.client.request.headers = {}; web.socketHandler(socket); socket.emit('message', 'data'); }); + it('on message should NOT call socketMessageHandler if not enabled', function(done) { + web = new WebService({node: node, enableSocketRPC: false}); + web.eventNames = web.getEventNames(); + web.socketMessageHandler = sinon.stub(); + socket = new EventEmitter(); + socket.conn = {}; + socket.conn.remoteAddress = '127.0.0.1'; + socket.client = {}; + socket.client.request = {}; + socket.client.request.headers = {}; + web.socketHandler(socket); + socket.on('message', function() { + web.socketMessageHandler.callCount.should.equal(0); + done(); + }); + socket.emit('message', 'data'); + }); + it('on subscribe should call bus.subscribe', function(done) { bus.subscribe = function(param1) { param1.should.equal('data'); @@ -297,7 +367,7 @@ describe('WebService', function() { var message = { method: 'two', params: [1, 2] - } + }; web.socketMessageHandler(message, function(response) { should.exist(response.error); response.error.message.should.equal('Method Not Found'); diff --git a/test/transaction.unit.js b/test/transaction.unit.js deleted file mode 100644 index eee529a4c..000000000 --- a/test/transaction.unit.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var sinon = require('sinon'); -var bitcoinlib = require('../'); -var Transaction = bitcoinlib.Transaction; -var levelup = require('levelup'); - -describe('Bitcoin Transaction', function() { - describe('#populateInputs', function() { - it('will call _populateInput with transactions', function() { - var tx = new Transaction(); - tx.isCoinbase = sinon.stub().returns(false); - tx._populateInput = sinon.stub().callsArg(3); - tx.inputs = ['input']; - var transactions = []; - var db = {}; - tx.populateInputs(db, transactions, function(err) { - tx._populateInput.callCount.should.equal(1); - tx._populateInput.args[0][0].should.equal(db); - tx._populateInput.args[0][1].should.equal('input'); - tx._populateInput.args[0][2].should.equal(transactions); - }); - }); - }); - - describe('#_populateInput', function() { - var input = { - prevTxId: new Buffer('d6cffbb343a6a41eeaa199478c985493843bfe6a59d674a5c188787416cbcda3', 'hex'), - outputIndex: 0 - }; - it('should give an error if the input does not have a valid prevTxId', function(done) { - var badInput = { - prevTxId: 'bad' - }; - var tx = new Transaction(); - tx._populateInput({}, badInput, [], function(err) { - should.exist(err); - err.message.should.equal('Input is expected to have prevTxId as a buffer'); - done(); - }); - }); - it('if an error happened it should pass it along', function(done) { - var tx = new Transaction(); - var db = { - getTransaction: sinon.stub().callsArgWith(2, new Error('error')) - }; - tx._populateInput(db, input, [], function(err) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - it('should return an error if the transaction for the input does not exist', function(done) { - var tx = new Transaction(); - var db = { - getTransaction: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()) - }; - tx._populateInput(db, input, [], function(err) { - should.exist(err); - err.message.should.equal('Previous tx ' + input.prevTxId.toString('hex') + ' not found'); - done(); - }); - }); - it('should look through poolTransactions if database does not have transaction', function(done) { - var tx = new Transaction(); - var db = { - getTransaction: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()) - }; - var transactions = [ - { - hash: 'd6cffbb343a6a41eeaa199478c985493843bfe6a59d674a5c188787416cbcda3', - outputs: ['output'] - } - ]; - tx._populateInput(db, input, transactions, function(err) { - should.not.exist(err); - input.output.should.equal('output'); - done(); - }); - }); - it('should not return an error if an error did not occur', function(done) { - var prevTx = new Transaction(); - prevTx.outputs = ['output']; - var tx = new Transaction(); - var db = { - getTransaction: sinon.stub().callsArgWith(2, null, prevTx) - }; - tx._populateInput(db, input, [], function(err) { - should.not.exist(err); - input.output.should.equal('output'); - done(); - }); - }); - }); - - describe('#_checkSpent', function() { - it('should return an error if input was spent', function(done) { - var tx = new Transaction(); - var db = { - isSpentDB: sinon.stub().callsArgWith(1, true) - }; - tx._checkSpent(db, [], 'input', function(err) { - should.exist(err); - err.message.should.equal('Input already spent'); - done(); - }); - }); - it('should not return an error if input was unspent', function(done) { - var tx = new Transaction(); - var db = { - isSpentDB: sinon.stub().callsArgWith(1, false) - }; - tx._checkSpent(db, [], 'input', function(err) { - should.not.exist(err); - done(); - }); - }); - }); -});