Merge pull request #169 from braydonf/services

Services Architecture
This commit is contained in:
Patrick Nagurny 2015-08-31 13:41:16 -04:00
commit 01ef98ef5d
43 changed files with 2842 additions and 3311 deletions

29
LICENSE
View File

@ -1,30 +1,7 @@
bitcoind,js
--------------------------------------------------------------------------------
Copyright (c) 2014-2015 BitPay, Inc.
Copyright (c) 2014-2015, BitPay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
bcoin
--------------------------------------------------------------------------------
Copyright Fedor Indutny, 2014.
Parts of this software are based on Bitcoin Core
Copyright (c) 2009-2015 The Bitcoin Core developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

394
README.md
View File

@ -1,398 +1,47 @@
Bitcore Node
=======
============
A Node.js module that adds a native interface to Bitcoin Core for querying information about the Bitcoin blockchain. Bindings are linked to Bitcoin Core compiled as a static library.
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.
## Install
Here is how you can you install and start your node:
```bash
npm install -g bitcore-node@0.2.0-beta.4
npm install -g bitcore-node@0.2.0-beta.5
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. If you want to compile the project yourself, then please see the [Build & Install](#build--install) for full detailed instructions to build the project from source.
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.
## Configuration
Bitcore Node includes a Command Line Interface (CLI) for managing, configuring and interfacing with your Bitcore Node. At the minimum, your node can function with all of the features from Bitcoin Core running as a full node. However you can enable additional features to make your node more useful such as exposing new APIs, adding new indexes for addresses, running a block explorer and custom modules.
Bitcore Node includes a Command Line Interface (CLI) for managing, configuring and interfacing with your Bitcore Node.
```bash
bitcore-node create -d <bitcoin-data-dir> mynode "My Node"
cd mynode
bitcore-node add <module>
bitcore-node add <service>
bitcore-node add https://github.com/yourname/helloworld
```
This will create a directory with configuration files for your node and install the necessary dependencies. If you're interested in developing a module, please see the [Module Development Guide](#modules).
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).
## Build & Install
## Documentation
This includes a detailed instructions for compiling. There are two main parts of the build, compiling Bitcoin Core as a static library and the Node.js bindings.
- [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.
- [Build & Install](docs/build.md) - How to build and install from source
- [Testing & Development](docs/testing.md) - Developer guide for testing
- [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.
### Ubuntu 14.04 (Unix/Linux)
## Contributing
If git is not already installed, it can be installed by running:
```bash
sudo apt-get install git
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
```
If Node.js v0.12 isn't installed, it can be installed using "nvm", it can be done by following the installation script at https://github.com/creationix/nvm#install-script and then install version v0.12
```bash
nvm install v0.12
```
To build Bitcoin Core and bindings development packages are needed:
```bash
sudo apt-get install build-essential libtool autotools-dev automake autoconf pkg-config libssl-dev
```
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
```
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).
### Fedora
Later versions of Fedora (>= 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 and following the directions at 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 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
```
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).
## 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
```
## 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.
## Example Usage
```js
var BitcoinNode = require('bitcore-node').Node;
var configuration = {
datadir: '~/.bitcoin',
network: 'testnet'
};
var node = new BitcoinNode(configuration);
node.on('ready', function() {
console.log('Bitcoin Node Ready');
});
node.on('error', function(err) {
console.error(err);
});
node.chain.on('addblock', function(block) {
console.log('New Best Tip:', block.hash);
});
```
## API Documentation
Get Unspent Outputs
```js
var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2';
var includeMempool = true;
node.getUnspentOutputs(address, includeMempool, function(err, unspentOutputs) {
//...
});
```
View Balances
```js
var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2';
var includeMempool = true;
node.getBalance(address, includeMempool, function(err, balance) {
//...
});
```
Get Outputs
```js
var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2';
var includeMempool = true;
node.getOutputs(address, includeMempool, function(err, outputs) {
//...
});
```
Get Transaction
```js
var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e';
var includeMempool = true;
node.getTransaction(txid, includeMempool, function(err, transaction) {
//...
});
```
Get Block
```js
var blockHash = '00000000d17332a156a807b25bc5a2e041d2c730628ceb77e75841056082a2c2';
node.getBlock(blockHash, function(err, block) {
//...
});
```
You can log output from the daemon using:
``` bash
$ tail -f ~/.bitcoin/debug.log
```
^C (SIGINT) will call `StartShutdown()` in bitcoind on the node thread pool.
## Modules
Bitcore Node has a module system where additional information can be indexed and queried from
the blockchain. One built-in module is the address module which exposes the API methods for getting balances and outputs.
### Writing a Module
A new module can be created by inheriting from `Node.Module`, implementing the methods `blockHandler()`, `getAPIMethods()`, `getPublishEvents()` and any additional methods for querying the data. Here is an example:
```js
var inherits = require('util').inherits;
var Node = require('bitcore-node').Node;
var MyModule = function(options) {
Node.Module.call(this, options);
};
inherits(MyModule, Node.Module);
/**
* blockHandler
* @param {Block} block - the block being added or removed from the chain
* @param {Boolean} add - whether the block is being added or removed
* @param {Function} callback - call with the leveldb database operations to perform
*/
MyModule.prototype.blockHandler = function(block, add, callback) {
var transactions = block.transactions;
// loop through transactions and outputs
// call the callback with leveldb database operations
var operations = [];
if(add) {
operations.push({
type: 'put',
key: 'key',
value: 'value'
});
} else {
operations.push({
type: 'del',
key: 'key'
});
}
// If your function is not asynchronous, it is important to use setImmediate.
setImmediate(function() {
callback(null, operations);
});
};
/**
* the API methods to expose
* @return {Array} return array of methods
*/
MyModule.prototype.getAPIMethods = function() {
return [
['getData', this, this.getData, 1]
];
};
/**
* the bus events available for subscription
* @return {Array} array of events
*/
MyModule.prototype.getPublishEvents = function() {
return [
{
name: 'custom',
scope: this,
subscribe: this.subscribeCustom,
unsubscribe: this.unsubscribeCustom
}
]
};
/**
* Will keep track of event listeners to later publish and emit events.
*/
MyModule.prototype.subscribeCustom = function(emitter, param) {
if(!this.subscriptions[param]) {
this.subscriptions[param] = [];
}
this.subscriptions[param].push(emitter);
}
MyModule.prototype.getData = function(arg1, callback) {
// You can query the data by reading from the leveldb store on db
this.node.db.store.get(arg1, callback);
};
module.exports = MyModule;
```
The module can then be used when running a node:
```js
var configuration = {
datadir: process.env.BITCORENODE_DIR || '~/.bitcoin',
modules: [MyModule]
};
var node = new Node(configuration);
node.on('ready', function() {
node.getData('key', function(err, value) {
console.log(err || value);
});
});
```
Note that if you already have a bitcore-node database, and you want to query data from previous blocks in the blockchain, you will need to reindex. Reindexing right now means deleting your bitcore-node database and resyncing.
## Daemon Documentation
- `daemon.start([options], [callback])` - Start the JavaScript Bitcoin node.
- `daemon.getBlock(blockHash|blockHeight, callback)` - Get any block asynchronously by block hash or height as a node buffer.
- `daemon.isSpent(txid, outputIndex)` - Returns a boolean if a txid and outputIndex is already spent.
- `daemon.getBlockIndex(blockHash)` - Will return the block chain work and previous hash.
- `daemon.estimateFee(blocks)` - Estimates the fees required to have a transaction included in the number of blocks specified as the first argument.
- `daemon.sendTransaction(transaction, allowAbsurdFees)` - Will attempt to add a transaction to the mempool and broadcast to peers.
- `daemon.getTransaction(txid, queryMempool, callback)` - Get any tx asynchronously by reading it from disk, with an argument to optionally not include the mempool.
- `daemon.getTransactionWithBlockInfo(txid, queryMempool, callback)` - Similar to getTransaction but will also include the block timestamp and height.
- `daemon.getMempoolOutputs(address)` - Will return an array of outputs that match an address from the mempool.
- `daemon.getInfo()` - Basic information about the chain including total number of blocks.
- `daemon.isSynced()` - Returns a boolean if the daemon is fully synced (not the initial block download)
- `daemon.syncPercentage()` - Returns the current estimate of blockchain download as a percentage.
- `daemon.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.
Please send pull requests for bug fixes, code optimization, and ideas for improvement. For more information on how to contribute, please refer to our [CONTRIBUTING](https://github.com/bitpay/bitcore/blob/master/CONTRIBUTING.md) file.
## License
@ -401,4 +50,3 @@ Code released under [the MIT license](https://github.com/bitpay/bitcore-node/blo
Copyright 2013-2015 BitPay, Inc.
- bitcoin: Copyright (c) 2009-2015 Bitcoin Core Developers (MIT License)
- bcoin (some code borrowed temporarily): Copyright Fedor Indutny, 2014.

View File

@ -2,17 +2,21 @@
'use strict';
var chainlib = require('chainlib');
var log = chainlib.log;
var index = require('..');
var log = index.log;
process.title = 'libbitcoind';
/**
* daemon
*/
var daemon = require('../').daemon({
datadir: process.env.BITCORENODE_DIR || '~/.bitcoin',
network: process.env.BITCORENODE_NETWORK || 'livenet'
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() {
@ -54,4 +58,4 @@ function exitHandler(options, err) {
process.on('uncaughtException', exitHandler.bind(null, {exit:true}));
//catches ctrl+c event
process.on('SIGINT', exitHandler.bind(null, {sigint:true}));
process.on('SIGINT', exitHandler.bind(null, {sigint:true}));

92
docs/build.md Normal file
View File

@ -0,0 +1,92 @@
## Build & Install
This includes a detailed instructions for compiling. There are two main parts of the build, compiling Bitcoin Core as a static library and the Node.js bindings.
## Ubuntu 14.04 (Unix/Linux)
If git is not already installed, it can be installed by running:
```bash
sudo apt-get install git
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
```
If Node.js v0.12 isn't installed, it can be installed using "nvm", it can be done by following the installation script at https://github.com/creationix/nvm#install-script and then install version v0.12
```bash
nvm install v0.12
```
To build Bitcoin Core and bindings development packages are needed:
```bash
sudo apt-get install build-essential libtool autotools-dev automake autoconf pkg-config libssl-dev
```
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
```
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).
## Fedora
Later versions of Fedora (>= 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 and following the directions at 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 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
```
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).

32
docs/bus.md Normal file
View File

@ -0,0 +1,32 @@
# Bus
The bus provides a way to subscribe to events from any of the services running. It's implemented abstract from transport specific implementation. The primary use of the bus in Bitcore Node is for subscribing to events via a web socket.
## Opening/Closing
```javascript
// a node is needed to be able to open a bus
var node = new Node(configuration);
// will create a new bus that is ready to subscribe to events
var bus = node.openBus();
// will remove all event listeners
bus.close();
```
## Subscribing/Unsubscribing
```javascript
// subscribe to all transaction events
bus.subscribe('transaction');
// only subscribe to events relevant to a bitcoin address
bus.subscribe('address/transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']);
// unsubscribe
bus.unsubscribe('transaction');
```

17
docs/errors.md Normal file
View File

@ -0,0 +1,17 @@
# 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 appropriatly.
```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`.

41
docs/node.md Normal file
View File

@ -0,0 +1,41 @@
# Node
A node represents a collection of services that are loaded together. For more information about services, please see the [Services Documentation](services.md).
## API Documentation
- `start()` - Will start the node's services in the correct order based on the dependencies of a service.
- `stop()` - Will stop the node's services.
- `openBus()` - Will create a new event bus to subscribe to events.
- `getAllAPIMethods()` - Returns information about all of the API methods from the services.
- `getAllPublishEvents()` - Returns information about publish events.
- `getServiceOrder()` - Returns an array of service modules.
- `services.<service-name>.<method>` - Additional API methods exposed by each service. The services for the node are defined when the node instance is constructed.
## Example Usage
```js
var BitcoinNode = require('bitcore-node').Node;
var configuration = {
datadir: '~/.bitcoin',
network: 'testnet'
};
var node = new BitcoinNode(configuration);
node.on('ready', function() {
console.log('Bitcoin Node Ready');
});
node.on('error', function(err) {
console.error(err);
});
// shutdown the node
node.stop(function() {
// the shutdown is complete
});
```

7
docs/patch.md Normal file
View File

@ -0,0 +1,7 @@
# 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.

View File

@ -1,8 +1,8 @@
## Release Process
# 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
## How to Verify Signatures
```
cd build/Release
@ -15,7 +15,7 @@ To verify signatures, use the following PGP keys:
- @kleetus: https://pgp.mit.edu/pks/lookup?op=get&search=0x33195D27EF6BDB7F
- @pnagurny: https://pgp.mit.edu/pks/lookup?op=get&search=0x0909B33F0AA53013
### How to Release
## 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.

32
docs/scaffold.md Normal file
View File

@ -0,0 +1,32 @@
# Scaffold
A collection of functions for creating, managing, starting, stopping and interacting with a Bitcore Node.
## Create
This function will create a new directory and the initial configuration files/directories, including 'bitcore-node.json', 'package.json', 'bitcoin.conf', install the necessary Node.js modules, and create a data directory.
## Add
This function will add a service to a node by installing the necessary dependencies and modifying the `bitcore-node.json` configuration.
## Start
This function will load a configuration file `bitcore-node.json` and instantiate and start a node based on the configuration.
## Find Config
This function will recursively find a configuration `bitcore-node.json` file in parent directories and return the result.
## Default Config
This function will return a default configuration with the default services based on environment variables, and will default to using the standard `~/.bitcoin` data directory.
## Remove
This function will remove a service from a node by uninstalling the necessary dependencies and modifying the `bitcore-node.json` configuration.
## Call Method
This function will call an API method on a node via the JSON-RPC interface.

56
docs/services.md Normal file
View File

@ -0,0 +1,56 @@
# Services
## Available Services
- [Bitcoin Daemon](services/bitcoind.md)
- [DB](services/db.md)
- [Address](services/address.md)
## Overview
Bitcore Node has a service module system that can start up additional services that can include additional:
- Blockchain indexes (e.g. querying balances for addresses)
- API methods
- HTTP routes
- Event types to publish and subscribe
The `bitcore-node.json` file describes which services will load for a node:
```json
{
"services": [
"bitcoind", "db", "address", "insight-api"
]
}
```
Services correspond with a Node.js module as described in 'package.json', for example:
```json
{
"dependencies": {
"bitcore": "^0.13.1",
"bitcore-node": "^0.2.0",
"insight-api": "^3.0.0"
}
}
```
*Note:* If you already have a bitcore-node database, and you want to query data from previous blocks in the blockchain, you will need to reindex. Reindexing right now means deleting your bitcore-node database and resyncing.
## Writing a Service
A new service can be created by inheriting from `Node.Service` and implementing these methods and properties:
- `Service.dependencies` - An array of services that are needed, this will determine the order that services are started on the node.
- `Service.prototype.start()` - Called to start up the service.
- `Service.prototype.stop()` - Called to stop the service.
- `Service.prototype.blockHandler()` - Will be called when a block is added or removed from the chain, and is useful for updating a database view/index.
- `Service.prototype.getAPIMethods()` - Describes which API methods that this service includes, these methods can then be called over the JSON-RPC API, as well as the command-line utility.
- `Service.prototype.getPublishEvents()` - Describes which events can be subscribed to for this service, useful to subscribe to events over the included web socket API.
- `Service.prototype.setupRoutes()` - A service can extend HTTP routes on an express application by implementing this method.
The `package.json` for the service module can either export the `Node.Service` directly, or specify a specific module to load by including `"bitcoreNode": "lib/bitcore-node.js"`.
Please take a look at some of the existing services for implemenation specifics.

35
docs/services/address.md Normal file
View File

@ -0,0 +1,35 @@
# 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.
## API Documentation
Get Unspent Outputs
```js
var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2';
var includeMempool = true;
node.getUnspentOutputs(address, includeMempool, function(err, unspentOutputs) {
//...
});
```
View Balances
```js
var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2';
var includeMempool = true;
node.getBalance(address, includeMempool, function(err, balance) {
//...
});
```
Get Outputs
```js
var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2';
var includeMempool = true;
node.getOutputs(address, includeMempool, function(err, outputs) {
//...
});
```

20
docs/services/bitcoind.md Normal file
View File

@ -0,0 +1,20 @@
# Bitcoin Service
The bitcoin service adds a native interface to Bitcoin Core for querying information about the Bitcoin blockchain. Bindings are linked to Bitcoin Core compiled as a static library.
## API Documentation
- `bitcoind.start([options], [callback])` - Start the JavaScript Bitcoin node.
- `bitcoind.getBlock(blockHash|blockHeight, callback)` - Get any block asynchronously by block hash or height as a node buffer.
- `bitcoind.isSpent(txid, outputIndex)` - Returns a boolean if a txid and outputIndex is already spent.
- `bitcoind.getBlockIndex(blockHash)` - Will return the block chain work and previous hash.
- `bitcoind.estimateFee(blocks)` - Estimates the fees required to have a transaction included in the number of blocks specified as the first argument.
- `bitcoind.sendTransaction(transaction, allowAbsurdFees)` - Will attempt to add a transaction to the mempool and broadcast to peers.
- `bitcoind.getTransaction(txid, queryMempool, callback)` - Get any tx asynchronously by reading it from disk, with an argument to optionally not include the mempool.
- `bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback)` - Similar to getTransaction but will also include the block timestamp and height.
- `bitcoind.getMempoolOutputs(address)` - Will return an array of outputs that match an address from the mempool.
- `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.

34
docs/services/db.md Normal file
View File

@ -0,0 +1,34 @@
# Database Service
An extensible interface to the bitcoin block chain. The service builds on the [Bitcoin Service](bitcoind.md), and includes additional methods for working with the block chain.
## API Documentation
Get Transaction
```js
var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e';
var includeMempool = true;
node.getTransaction(txid, includeMempool, function(err, transaction) {
//...
});
```
Get Transaction with Block Info
```js
var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e';
var includeMempool = true;
node.getTransactionWithBlockInfo(txid, includeMempool, function(err, transaction) {
//...
});
```
Get Block
```js
var blockHash = '00000000d17332a156a807b25bc5a2e041d2c730628ceb77e75841056082a2c2';
node.getBlock(blockHash, function(err, block) {
//...
});
```

52
docs/testing.md Normal file
View File

@ -0,0 +1,52 @@
## 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
```

View File

@ -1,16 +1,15 @@
'use strict';
module.exports = require('./lib');
module.exports.daemon = require('./lib/daemon');
module.exports.Node = require('./lib/node');
module.exports.Chain = require('./lib/chain');
module.exports.DB = require('./lib/db');
module.exports.Transaction = require('./lib/transaction');
module.exports.Module = require('./lib/module');
module.exports.Service = require('./lib/service');
module.exports.errors = require('./lib/errors');
module.exports.modules = {};
module.exports.modules.AddressModule = require('./lib/modules/address');
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.scaffold = {};
module.exports.scaffold.create = require('./lib/scaffold/create');

View File

@ -23,7 +23,11 @@ var node;
var should = chai.should();
var BitcoinRPC = require('bitcoind-rpc');
var BitcoreNode = require('..').Node;
var index = require('..');
var BitcoreNode = index.Node;
var AddressService = index.services.Address;
var BitcoinService = index.services.Bitcoin;
var DBService = index.services.DB;
var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG';
var testKey;
var client;
@ -60,7 +64,24 @@ describe('Node Functionality', function() {
var configuration = {
datadir: datadir,
network: 'regtest'
network: 'regtest',
services: [
{
name: 'db',
module: DBService,
dependencies: DBService.dependencies
},
{
name: 'bitcoind',
module: BitcoinService,
dependencies: BitcoinService.dependencies
},
{
name: 'address',
module: AddressService,
dependencies: AddressService.dependencies
}
]
};
node = new BitcoreNode(configuration);
@ -81,7 +102,7 @@ describe('Node Functionality', function() {
});
var syncedHandler = function() {
if (node.chain.tip.__height === 150) {
if (node.services.db.tip.__height === 150) {
node.removeListener('synced', syncedHandler);
done();
}
@ -102,7 +123,10 @@ describe('Node Functionality', function() {
after(function(done) {
this.timeout(20000);
node.bitcoind.stop(function(err, result) {
node.stop(function(err, result) {
if(err) {
throw err;
}
done();
});
});
@ -154,18 +178,18 @@ describe('Node Functionality', function() {
blocksRemoved++;
};
node.chain.on('removeblock', removeBlock);
node.services.db.on('removeblock', removeBlock);
var addBlock = function() {
blocksAdded++;
if (blocksAdded === 2 && blocksRemoved === 1) {
node.chain.removeListener('addblock', addBlock);
node.chain.removeListener('removeblock', removeBlock);
node.services.db.removeListener('addblock', addBlock);
node.services.db.removeListener('removeblock', removeBlock);
done();
}
};
node.chain.on('addblock', addBlock);
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.

View File

@ -61,9 +61,13 @@ describe('Daemon Binding Functionality', function() {
throw err;
}
bitcoind = require('../').daemon({
datadir: datadir,
network: 'regtest'
bitcoind = require('../').services.Bitcoin({
node: {
datadir: datadir,
network: {
name: 'regtest'
}
}
});
bitcoind.on('error', function(err) {

View File

@ -11,11 +11,11 @@ function Bus(params) {
util.inherits(Bus, events.EventEmitter);
Bus.prototype.subscribe = function(name) {
var events = this.node.db.getPublishEvents();
var events = [];
for(var i in this.node.modules) {
var mod = this.node.modules[i];
events = events.concat(mod.getPublishEvents());
for(var i in this.node.services) {
var service = this.node.services[i];
events = events.concat(service.getPublishEvents());
}
for (var j = 0; j < events.length; j++) {
@ -29,11 +29,11 @@ Bus.prototype.subscribe = function(name) {
};
Bus.prototype.unsubscribe = function(name) {
var events = this.node.db.getPublishEvents();
var events = [];
for(var i in this.node.modules) {
var mod = this.node.modules[i];
events = events.concat(mod.getPublishEvents());
for(var i in this.node.services) {
var service = this.node.services[i];
events = events.concat(service.getPublishEvents());
}
for (var j = 0; j < events.length; j++) {
@ -47,11 +47,11 @@ Bus.prototype.unsubscribe = function(name) {
};
Bus.prototype.close = function() {
var events = this.node.db.getPublishEvents();
var events = [];
for(var i in this.node.modules) {
var mod = this.node.modules[i];
events = events.concat(mod.getPublishEvents());
for(var i in this.node.services) {
var service = this.node.services[i];
events = events.concat(service.getPublishEvents());
}
// Unsubscribe from all events

View File

@ -1,252 +0,0 @@
'use strict';
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var bitcore = require('bitcore');
var BN = bitcore.crypto.BN;
var $ = bitcore.util.preconditions;
var Block = bitcore.Block;
var index = require('./index');
var log = index.log;
var utils = require('./utils');
var MAX_STACK_DEPTH = 1000;
/**
* Will instantiate a new Chain instance
* @param {Object} options - The options for the chain
* @param {Number} options.minBits - The minimum number of bits
* @param {Number} options.maxBits - The maximum number of bits
* @param {BN|Number} options.targetTimespan - The number of milliseconds for difficulty retargeting
* @param {BN|Number} options.targetSpacing - The number of milliseconds between blocks
* @returns {Chain}
* @extends BaseChain
* @constructor
*/
function Chain(opts) {
/* jshint maxstatements: 30 */
if (!(this instanceof Chain)) {
return new Chain(opts);
}
var self = this;
if(!opts) {
opts = {};
}
this.genesis = opts.genesis;
this.genesisOptions = opts.genesisOptions;
this.genesisWeight = new BN(0);
this.tip = null;
this.overrideTip = opts.overrideTip;
this.cache = {
hashes: {}, // dictionary of hash -> prevHash
chainHashes: {}
};
this.lastSavedMetadata = null;
this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance
this.blockQueue = [];
this.processingBlockQueue = false;
this.builder = opts.builder || false;
this.ready = false;
this.on('initialized', function() {
self.initialized = true;
});
this.on('initialized', this._onInitialized.bind(this));
this.on('ready', function() {
log.debug('Chain is ready');
self.ready = true;
self.startBuilder();
});
this.minBits = opts.minBits || Chain.DEFAULTS.MIN_BITS;
this.maxBits = opts.maxBits || Chain.DEFAULTS.MAX_BITS;
this.maxHashes = opts.maxHashes || Chain.DEFAULTS.MAX_HASHES;
this.targetTimespan = opts.targetTimespan || Chain.DEFAULTS.TARGET_TIMESPAN;
this.targetSpacing = opts.targetSpacing || Chain.DEFAULTS.TARGET_SPACING;
this.node = opts.node;
return this;
}
util.inherits(Chain, EventEmitter);
Chain.DEFAULTS = {
MAX_HASHES: new BN('10000000000000000000000000000000000000000000000000000000000000000', 'hex'),
TARGET_TIMESPAN: 14 * 24 * 60 * 60 * 1000, // two weeks
TARGET_SPACING: 10 * 60 * 1000, // ten minutes
MAX_BITS: 0x1d00ffff,
MIN_BITS: 0x03000000
};
Chain.prototype._onInitialized = function() {
this.emit('ready');
};
Chain.prototype.start = function(callback) {
this.genesis = Block.fromBuffer(this.node.bitcoind.genesisBuffer);
this.once('initialized', callback);
this.initialize();
};
Chain.prototype.initialize = function() {
var self = this;
// Does our database already have a tip?
self.node.db.getMetadata(function getMetadataCallback(err, metadata) {
if(err) {
return self.emit('error', err);
} else if(!metadata || !metadata.tip) {
self.tip = self.genesis;
self.tip.__height = 0;
self.tip.__weight = self.genesisWeight;
self.node.db.putBlock(self.genesis, function putBlockCallback(err) {
if(err) {
return self.emit('error', err);
}
self.node.db._onChainAddBlock(self.genesis, function(err) {
if(err) {
return self.emit('error', err);
}
self.emit('addblock', self.genesis);
self.saveMetadata();
self.emit('initialized');
});
});
} else {
metadata.tip = metadata.tip;
self.node.db.getBlock(metadata.tip, function getBlockCallback(err, tip) {
if(err) {
return self.emit('error', err);
}
self.tip = tip;
self.tip.__height = metadata.tipHeight;
self.tip.__weight = new BN(metadata.tipWeight, 'hex');
self.cache = metadata.cache;
self.emit('initialized');
});
}
});
};
Chain.prototype.stop = function(callback) {
setImmediate(callback);
};
Chain.prototype._validateBlock = function(block, callback) {
// All validation is done by bitcoind
setImmediate(callback);
};
Chain.prototype.startBuilder = function() {
// Unused in bitcoind.js
};
Chain.prototype.getWeight = function getWeight(blockHash, callback) {
var self = this;
var blockIndex = self.node.bitcoind.getBlockIndex(blockHash);
setImmediate(function() {
if (blockIndex) {
callback(null, new BN(blockIndex.chainWork, 'hex'));
} else {
return callback(new Error('Weight not found for ' + blockHash));
}
});
};
/**
* Will get an array of hashes all the way to the genesis block for
* the chain based on "block hash" as the tip.
*
* @param {String} block hash - a block hash
* @param {Function} callback - A function that accepts: Error and Array of hashes
*/
Chain.prototype.getHashes = function getHashes(tipHash, callback) {
var self = this;
$.checkArgument(utils.isHash(tipHash));
var hashes = [];
var depth = 0;
getHashAndContinue(null, tipHash);
function getHashAndContinue(err, hash) {
if (err) {
return callback(err);
}
depth++;
hashes.unshift(hash);
if (hash === self.genesis.hash) {
// Stop at the genesis block
self.cache.chainHashes[tipHash] = hashes;
callback(null, hashes);
} else if(self.cache.chainHashes[hash]) {
hashes.shift();
hashes = self.cache.chainHashes[hash].concat(hashes);
delete self.cache.chainHashes[hash];
self.cache.chainHashes[tipHash] = hashes;
callback(null, hashes);
} else {
// Continue with the previous hash
// check cache first
var prevHash = self.cache.hashes[hash];
if(prevHash) {
// Don't let the stack get too deep. Otherwise we will crash.
if(depth >= MAX_STACK_DEPTH) {
depth = 0;
return setImmediate(function() {
getHashAndContinue(null, prevHash);
});
} else {
return getHashAndContinue(null, prevHash);
}
} else {
// do a db call if we don't have it
self.node.db.getPrevHash(hash, function(err, prevHash) {
if(err) {
return callback(err);
}
return getHashAndContinue(null, prevHash);
});
}
}
}
};
Chain.prototype.saveMetadata = function saveMetadata(callback) {
var self = this;
callback = callback || function() {};
if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) {
return callback();
}
var metadata = {
tip: self.tip ? self.tip.hash : null,
tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0,
tipWeight: self.tip && self.tip.__weight ? self.tip.__weight.toString(16) : '0',
cache: self.cache
};
self.lastSavedMetadata = new Date();
self.node.db.putMetadata(metadata, callback);
};
module.exports = Chain;

View File

@ -1,165 +0,0 @@
'use strict';
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var bitcoind = require('bindings')('bitcoind.node');
var index = require('./');
var log = index.log;
var bitcore = require('bitcore');
var $ = bitcore.util.preconditions;
function Daemon(options) {
var self = this;
if (!(this instanceof Daemon)) {
return new Daemon(options);
}
if (Object.keys(this.instances).length) {
throw new Error('Daemon cannot be instantiated more than once.');
}
EventEmitter.call(this);
$.checkArgument(options.datadir, 'Please specify a datadir');
this.options = options || {};
this.options.datadir = this.options.datadir.replace(/^~/, process.env.HOME);
this.datadir = this.options.datadir;
this.node = options.node;
this.config = this.datadir + '/bitcoin.conf';
Object.keys(exports).forEach(function(key) {
self[key] = exports[key];
});
}
util.inherits(Daemon, EventEmitter);
Daemon.instances = {};
Daemon.prototype.instances = Daemon.instances;
Daemon.__defineGetter__('global', function() {
return Daemon.instances[Object.keys(Daemon.instances)[0]];
});
Daemon.prototype.__defineGetter__('global', function() {
return Daemon.global;
});
Daemon.prototype.start = function(callback) {
var self = this;
if (this.instances[this.datadir]) {
return callback(new Error('Daemon already started'));
}
this.instances[this.datadir] = true;
bitcoind.start(this.options, function(err) {
if(err) {
return callback(err);
}
self._started = true;
bitcoind.onBlocksReady(function(err, result) {
function onTipUpdateListener(result) {
if (result) {
// Emit and event that the tip was updated
self.height = result;
self.emit('tip', result);
// Recursively wait until the next update
bitcoind.onTipUpdate(onTipUpdateListener);
}
}
bitcoind.onTipUpdate(onTipUpdateListener);
bitcoind.startTxMon(function(txs) {
for(var i = 0; i < txs.length; i++) {
self.emit('tx', txs[i]);
}
});
// Set the current chain height
var info = self.getInfo();
self.height = info.blocks;
// Get the genesis block
self.getBlock(0, function(err, block) {
self.genesisBuffer = block;
self.emit('ready', result);
setImmediate(callback);
});
});
});
};
Daemon.prototype.isSynced = function() {
return bitcoind.isSynced();
};
Daemon.prototype.syncPercentage = function() {
return bitcoind.syncPercentage();
};
Daemon.prototype.getBlock = function(blockhash, callback) {
return bitcoind.getBlock(blockhash, callback);
};
Daemon.prototype.isSpent = function(txid, outputIndex) {
return bitcoind.isSpent(txid, outputIndex);
};
Daemon.prototype.getBlockIndex = function(blockHash) {
return bitcoind.getBlockIndex(blockHash);
};
Daemon.prototype.estimateFee = function(blocks) {
return bitcoind.estimateFee(blocks);
};
Daemon.prototype.sendTransaction = function(transaction, allowAbsurdFees) {
return bitcoind.sendTransaction(transaction, allowAbsurdFees);
};
Daemon.prototype.getTransaction = function(txid, queryMempool, callback) {
return bitcoind.getTransaction(txid, queryMempool, callback);
};
Daemon.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) {
return bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback);
};
Daemon.prototype.getMempoolOutputs = function(address) {
return bitcoind.getMempoolOutputs(address);
};
Daemon.prototype.addMempoolUncheckedTransaction = function(txBuffer) {
return bitcoind.addMempoolUncheckedTransaction(txBuffer);
};
Daemon.prototype.getInfo = function() {
return bitcoind.getInfo();
};
Daemon.prototype.stop = function(callback) {
var self = this;
return bitcoind.stop(function(err, status) {
setImmediate(function() {
if (err) {
return callback(err);
} else {
log.info(status);
return callback();
}
});
});
};
module.exports = Daemon;

347
lib/db.js
View File

@ -1,347 +0,0 @@
'use strict';
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var async = require('async');
var levelup = require('levelup');
var leveldown = require('leveldown');
var bitcore = require('bitcore');
var Block = bitcore.Block;
var $ = bitcore.util.preconditions;
var index = require('./');
var errors = index.errors;
var log = index.log;
var Transaction = require('./transaction');
function DB(options) {
/* jshint maxstatements: 30 */
/* jshint maxcomplexity: 20 */
if (!(this instanceof DB)) {
return new DB(options);
}
if(!options) {
options = {};
}
this.coinbaseAmount = options.coinbaseAmount || 50 * 1e8;
var levelupStore = leveldown;
if(options.store) {
levelupStore = options.store;
} else if(!options.path) {
throw new Error('Please include database path in options');
}
this.store = levelup(options.path, { db: levelupStore });
this.txPrefix = options.txPrefix || DB.PREFIXES.TX;
this.prevHashPrefix = options.prevHashPrefix || DB.PREFIXES.PREV_HASH;
this.blockPrefix = options.blockPrefix || DB.PREFIXES.BLOCK;
this.dataPrefix = options.dataPrefix || DB.PREFIXES.DATA;
this.weightPrefix = options.weightPrefix || DB.PREFIXES.WEIGHT;
this.Transaction = Transaction;
this.coinbaseAddress = options.coinbaseAddress;
this.coinbaseAmount = options.coinbaseAmount || 50 * 1e8;
this.Transaction = Transaction;
this.network = bitcore.Networks.get(options.network) || bitcore.Networks.testnet;
this.node = options.node;
this.subscriptions = {
transaction: [],
block: []
};
}
DB.PREFIXES = {
TX: 'tx',
PREV_HASH: 'ph',
BLOCK: 'blk',
DATA: 'data',
WEIGHT: 'wt'
};
util.inherits(DB, EventEmitter);
DB.prototype.initialize = function() {
this.emit('ready');
};
DB.prototype.start = function(callback) {
this.node.bitcoind.on('tx', this.transactionHandler.bind(this));
this.emit('ready');
setImmediate(callback);
};
DB.prototype.stop = function(callback) {
// TODO Figure out how to call this.store.close() without issues
setImmediate(callback);
};
DB.prototype.getInfo = function(callback) {
var self = this;
setImmediate(function() {
var info = self.node.bitcoind.getInfo();
callback(null, info);
});
};
DB.prototype.getBlock = function(hash, callback) {
var self = this;
// get block from bitcoind
this.node.bitcoind.getBlock(hash, function(err, blockData) {
if(err) {
return callback(err);
}
callback(null, Block.fromBuffer(blockData));
});
};
DB.prototype.getPrevHash = function(blockHash, callback) {
var blockIndex = this.node.bitcoind.getBlockIndex(blockHash);
setImmediate(function() {
if (blockIndex) {
callback(null, blockIndex.prevHash);
} else {
callback(new Error('Could not get prevHash, block not found'));
}
});
};
DB.prototype.putBlock = function(block, callback) {
// block is already stored in bitcoind
setImmediate(callback);
};
DB.prototype.getTransaction = function(txid, queryMempool, callback) {
this.node.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));
});
};
DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) {
this.node.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) {
if(err) {
return callback(err);
}
var tx = Transaction().fromBuffer(obj.buffer);
tx.__height = obj.height;
tx.__timestamp = obj.timestamp;
callback(null, tx);
});
};
DB.prototype.sendTransaction = function(tx, callback) {
if(tx instanceof this.Transaction) {
tx = tx.toString();
}
$.checkArgument(typeof tx === 'string', 'Argument must be a hex string or Transaction');
try {
var txid = this.node.bitcoind.sendTransaction(tx);
return callback(null, txid);
} catch(err) {
return callback(err);
}
};
DB.prototype.estimateFee = function(blocks, callback) {
var self = this;
setImmediate(function() {
callback(null, self.node.bitcoind.estimateFee(blocks));
});
};
DB.prototype.validateBlockData = function(block, callback) {
// bitcoind does the validation
setImmediate(callback);
};
DB.prototype._updatePrevHashIndex = function(block, callback) {
// bitcoind has the previous hash for each block
setImmediate(callback);
};
DB.prototype._updateWeight = function(hash, weight, callback) {
// bitcoind has all work for each block
setImmediate(callback);
};
/**
* Saves metadata to the database
* @param {Object} metadata - The metadata
* @param {Function} callback - A function that accepts: Error
*/
DB.prototype.putMetadata = function(metadata, callback) {
this.store.put('metadata', JSON.stringify(metadata), {}, callback);
};
/**
* Retrieves metadata from the database
* @param {Function} callback - A function that accepts: Error and Object
*/
DB.prototype.getMetadata = function(callback) {
var self = this;
self.store.get('metadata', {}, function(err, data) {
if(err instanceof levelup.errors.NotFoundError) {
return callback(null, {});
} else if(err) {
return callback(err);
}
var metadata;
try {
metadata = JSON.parse(data);
} catch(e) {
return callback(new Error('Could not parse metadata'));
}
callback(null, metadata);
});
};
/**
* Closes the underlying store database
* @param {Function} callback - A function that accepts: Error
*/
DB.prototype.close = function(callback) {
this.store.close(callback);
};
DB.prototype.getOutputTotal = function(transactions, excludeCoinbase) {
var totals = transactions.map(function(tx) {
if(tx.isCoinbase() && excludeCoinbase) {
return 0;
} else {
return tx._getOutputAmount();
}
});
var grandTotal = totals.reduce(function(previousValue, currentValue) {
return previousValue + currentValue;
});
return grandTotal;
};
DB.prototype.getInputTotal = function(transactions) {
var totals = transactions.map(function(tx) {
if(tx.isCoinbase()) {
return 0;
} else {
return tx._getInputAmount();
}
});
var grandTotal = totals.reduce(function(previousValue, currentValue) {
return previousValue + currentValue;
});
return grandTotal;
};
DB.prototype._onChainAddBlock = function(block, callback) {
log.debug('DB handling new chain block');
this.blockHandler(block, true, callback);
};
DB.prototype._onChainRemoveBlock = function(block, callback) {
log.debug('DB removing chain block');
this.blockHandler(block, false, callback);
};
DB.prototype.blockHandler = 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('block', block.hash);
}
async.eachSeries(
this.node.modules,
function(bitcoreNodeModule, next) {
bitcoreNodeModule.blockHandler.call(bitcoreNodeModule, block, add, function(err, ops) {
if(err) {
return next(err);
}
operations = operations.concat(ops);
next();
});
},
function(err) {
if (err) {
return callback(err);
}
log.debug('Updating the database with operations', operations);
self.store.batch(operations, callback);
}
);
};
DB.prototype.getAPIMethods = function() {
var methods = [
['getInfo', this, this.getInfo, 0],
['getBlock', this, this.getBlock, 1],
['getTransaction', this, this.getTransaction, 2],
['sendTransaction', this, this.sendTransaction, 1],
['estimateFee', this, this.estimateFee, 1]
];
return methods;
};
DB.prototype.getPublishEvents = function() {
return [
{
name: 'transaction',
scope: this,
subscribe: this.subscribe.bind(this, 'transaction'),
unsubscribe: this.unsubscribe.bind(this, 'transaction')
},
{
name: '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);
}
};
DB.prototype.transactionHandler = function(txInfo) {
var tx = bitcore.Transaction().fromBuffer(txInfo.buffer);
for(var i = 0; i < this.subscriptions.transaction.length; i++) {
this.subscriptions.transaction[i].emit('transaction', {
rejected: !txInfo.mempool,
tx: tx
});
}
};
module.exports = DB;

View File

@ -1,354 +1,52 @@
'use strict';
var fs = require('fs');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var mkdirp = require('mkdirp');
var bitcore = require('bitcore');
var BufferUtil = bitcore.util.buffer;
var Networks = bitcore.Networks;
var _ = bitcore.deps._;
var $ = bitcore.util.preconditions;
var Block = bitcore.Block;
var Chain = require('./chain');
var DB = require('./db');
var index = require('./');
var log = index.log;
var daemon = require('./daemon');
var Bus = require('./bus');
var BaseModule = require('./module');
var BaseService = require('./service');
function Node(config) {
if(!(this instanceof Node)) {
return new Node(config);
}
this.db = null;
this.chain = null;
var self = this;
this.network = null;
this.services = {};
this._unloadedServices = [];
this.modules = {};
this._unloadedModules = [];
// TODO type check the arguments of config.modules
if (config.modules) {
$.checkArgument(Array.isArray(config.modules));
this._unloadedModules = config.modules;
// TODO type check the arguments of config.services
if (config.services) {
$.checkArgument(Array.isArray(config.services));
this._unloadedServices = config.services;
}
this._loadConfiguration(config);
this._initialize();
$.checkState(config.datadir, 'Node config expects "datadir"');
this.datadir = config.datadir;
this._setNetwork(config);
this.start(function(err) {
if(err) {
return self.emit('error', err);
}
self.emit('ready');
});
}
util.inherits(Node, EventEmitter);
Node.prototype.openBus = function() {
return new Bus({node: this});
};
util.inherits(Node, EventEmitter);
Node.prototype.addModule = function(service) {
var self = this;
var mod = new service.module({
node: this
});
$.checkState(
mod instanceof BaseModule,
'Unexpected module instance type for module:' + service.name
);
// include in loaded modules
this.modules[service.name] = mod;
// add API methods
var methodData = mod.getAPIMethods();
methodData.forEach(function(data) {
var name = data[0];
var instance = data[1];
var method = data[2];
if (self[name]) {
throw new Error('Existing API method exists:' + name);
} else {
self[name] = function() {
return method.apply(instance, arguments);
};
}
});
};
Node.prototype.getAllAPIMethods = function() {
var methods = this.db.getAPIMethods();
for(var i in this.modules) {
var mod = this.modules[i];
methods = methods.concat(mod.getAPIMethods());
}
return methods;
};
Node.prototype.getAllPublishEvents = function() {
var events = this.db.getPublishEvents();
for (var i in this.modules) {
var mod = this.modules[i];
events = events.concat(mod.getPublishEvents());
}
return events;
};
Node.prototype._loadConfiguration = function(config) {
this._loadBitcoinConf(config);
this._loadBitcoind(config);
this._loadNetwork(config);
this._loadDB(config);
this._loadAPI();
this._loadConsensus(config);
};
Node.DEFAULT_DAEMON_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n';
Node.prototype._loadBitcoinConf = function(config) {
$.checkArgument(config.datadir, 'Please specify "datadir" in configuration options');
var configPath = config.datadir + '/bitcoin.conf';
this.bitcoinConfiguration = {};
if (!fs.existsSync(config.datadir)) {
mkdirp.sync(config.datadir);
}
if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, Node.DEFAULT_DAEMON_CONFIG);
}
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.bitcoinConfiguration[option[0]] = value;
}
}
$.checkState((this.bitcoinConfiguration.txindex && this.bitcoinConfiguration.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');
};
Node.prototype._loadBitcoind = function(config) {
var bitcoindConfig = {};
bitcoindConfig.datadir = config.datadir;
bitcoindConfig.network = config.network;
bitcoindConfig.node = this;
// start the bitcoind daemon
this.bitcoind = daemon(bitcoindConfig);
};
/**
* This function will find the common ancestor between the current chain and a forked block,
* by moving backwards from the forked block until it meets the current chain.
* @param {Block} block - The new tip that forks the current chain.
* @param {Function} done - A callback function that is called when complete.
*/
Node.prototype._syncBitcoindAncestor = function(block, done) {
var self = this;
// The current chain of hashes will likely already be available in a cache.
self.chain.getHashes(self.chain.tip.hash, function(err, currentHashes) {
if (err) {
done(err);
}
// Create a hash map for faster lookups
var currentHashesMap = {};
var length = currentHashes.length;
for (var i = 0; i < length; i++) {
currentHashesMap[currentHashes[i]] = true;
}
// TODO: expose prevHash as a string from bitcore
var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex');
// We only need to go back until we meet the main chain for the forked block
// and thus don't need to find the entire chain of hashes.
while(ancestorHash && !currentHashesMap[ancestorHash]) {
var blockIndex = self.bitcoind.getBlockIndex(ancestorHash);
ancestorHash = blockIndex ? blockIndex.prevHash : null;
}
// Hash map is no-longer needed, quickly let
// scavenging garbage collection know to cleanup
currentHashesMap = null;
if (!ancestorHash) {
return done(new Error('Unknown common ancestor.'));
}
done(null, ancestorHash);
});
};
/**
* 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.
*/
Node.prototype._syncBitcoindRewind = function(block, done) {
var self = this;
self._syncBitcoindAncestor(block, function(err, ancestorHash) {
if (err) {
return done(err);
}
// Rewind the chain to the common ancestor
async.whilst(
function() {
// Wait until the tip equals the ancestor hash
return self.chain.tip.hash !== ancestorHash;
},
function(removeDone) {
var tip = self.chain.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.db._onChainRemoveBlock(tip, function(err) {
if (err) {
return removeDone(err);
}
// Set the new tip
previousTip.__height = self.chain.tip.__height - 1;
self.chain.tip = previousTip;
self.chain.saveMetadata();
self.chain.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.
*/
Node.prototype._syncBitcoind = function() {
var self = this;
if (self.bitcoindSyncing) {
return;
}
if (!self.chain.tip) {
return;
}
self.bitcoindSyncing = true;
self.chain.lastSavedMetadataThreshold = 30000;
var height;
async.whilst(function() {
height = self.chain.tip.__height;
return height < self.bitcoind.height && !self.stopping;
}, function(done) {
self.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.chain.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.chain.tip.__height + 1;
// Update chain.cache.hashes
self.chain.cache.hashes[block.hash] = prevHash;
// Update chain.cache.chainHashes
self.chain.getHashes(block.hash, function(err, hashes) {
if (err) {
return done(err);
}
// Create indexes
self.db._onChainAddBlock(block, function(err) {
if (err) {
return done(err);
}
self.chain.tip = block;
log.debug('Saving metadata');
self.chain.saveMetadata();
log.debug('Chain added block to main chain');
self.chain.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.
self._syncBitcoindRewind(block, done);
}
});
}, function(err) {
if (err) {
Error.captureStackTrace(err);
return self.emit('error', err);
}
if(self.stopping) {
return;
}
self.bitcoindSyncing = false;
self.chain.lastSavedMetadataThreshold = 0;
// If bitcoind is completely synced
if (self.bitcoind.isSynced()) {
self.emit('synced');
}
});
};
Node.prototype._loadNetwork = function(config) {
Node.prototype._setNetwork = function(config) {
if (config.network === 'testnet') {
this.network = Networks.get('testnet');
} else if (config.network === 'regtest') {
@ -367,156 +65,36 @@ Node.prototype._loadNetwork = function(config) {
});
this.network = Networks.get('regtest');
} else {
this.network = Networks.get('livenet');
this.network = Networks.defaultNetwork;
}
$.checkState(this.network, 'Unrecognized network');
};
Node.prototype._loadDB = function(config) {
var options = _.clone(config.db || {});
Node.prototype.openBus = function() {
return new Bus({node: this});
};
if (config.DB) {
// Other modules can inherit from our DB and replace it with their own
DB = config.DB;
Node.prototype.getAllAPIMethods = function() {
var methods = [];
for(var i in this.services) {
var mod = this.services[i];
methods = methods.concat(mod.getAPIMethods());
}
return methods;
};
// Store the additional indexes in a new directory
// based on the network configuration and the datadir
$.checkArgument(config.datadir, 'Please specify "datadir" in configuration options');
$.checkState(this.network, 'Network property not defined');
var regtest = Networks.get('regtest');
if (this.network === Networks.livenet) {
options.path = config.datadir + '/bitcore-node.db';
} else if (this.network === Networks.testnet) {
options.path = config.datadir + '/testnet3/bitcore-node.db';
} else if (this.network === regtest) {
options.path = config.datadir + '/regtest/bitcore-node.db';
} else {
throw new Error('Unknown network: ' + this.network);
Node.prototype.getAllPublishEvents = function() {
var events = [];
for (var i in this.services) {
var mod = this.services[i];
events = events.concat(mod.getPublishEvents());
}
options.network = this.network;
if (!fs.existsSync(options.path)) {
mkdirp.sync(options.path);
}
options.node = this;
this.db = new DB(options);
};
Node.prototype._loadConsensus = function(config) {
var options;
if (!config) {
options = {};
} else {
options = _.clone(config.consensus || {});
}
options.node = this;
this.chain = new Chain(options);
};
Node.prototype._loadAPI = function() {
var self = this;
var methodData = self.db.getAPIMethods();
methodData.forEach(function(data) {
var name = data[0];
var instance = data[1];
var method = data[2];
self[name] = function() {
return method.apply(instance, arguments);
};
});
};
Node.prototype._initialize = function() {
var self = this;
this._initializeBitcoind();
this._initializeDatabase();
this._initializeChain();
this.start(function(err) {
if(err) {
return self.emit('error', err);
}
self.emit('ready');
});
};
Node.prototype._initializeBitcoind = function() {
var self = this;
// Notify that there is a new tip
this.bitcoind.on('ready', function() {
log.info('Bitcoin Daemon Ready');
});
// Notify that there is a new tip
this.bitcoind.on('tip', function(height) {
if(!self.stopping) {
var percentage = self.bitcoind.syncPercentage();
log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage);
self._syncBitcoind();
}
});
this.bitcoind.on('error', function(err) {
Error.captureStackTrace(err);
self.emit('error', err);
});
};
Node.prototype._initializeDatabase = function() {
var self = this;
this.db.on('ready', function() {
log.info('Bitcoin Database Ready');
});
this.db.on('error', function(err) {
Error.captureStackTrace(err);
self.emit('error', err);
});
};
Node.prototype._initializeChain = function() {
var self = this;
this.chain.on('ready', function() {
log.info('Bitcoin Chain Ready');
self._syncBitcoind();
});
this.chain.on('error', function(err) {
Error.captureStackTrace(err);
self.emit('error', err);
});
};
Node.prototype.getServices = function() {
var services = [
{
name: 'bitcoind',
dependencies: []
},
{
name: 'db',
dependencies: ['bitcoind'],
},
{
name: 'chain',
dependencies: ['db']
}
];
services = services.concat(this._unloadedModules);
return services;
return events;
};
Node.prototype.getServiceOrder = function() {
var services = this.getServices();
var services = this._unloadedServices;
// organize data for sorting
var names = [];
@ -535,6 +113,7 @@ Node.prototype.getServiceOrder = function() {
var name = names[i];
var service = servicesByName[name];
$.checkState(service, 'Required dependency "' + name + '" not available.');
// first add the dependencies
addToStack(service.dependencies);
@ -553,6 +132,37 @@ Node.prototype.getServiceOrder = function() {
return stack;
};
Node.prototype._instantiateService = function(service) {
var self = this;
var mod = new service.module({
node: this
});
$.checkState(
mod instanceof BaseService,
'Unexpected module instance type for service:' + service.name
);
// include in loaded services
this.services[service.name] = mod;
// add API methods
var methodData = mod.getAPIMethods();
methodData.forEach(function(data) {
var name = data[0];
var instance = data[1];
var method = data[2];
if (self[name]) {
throw new Error('Existing API method exists: ' + name);
} else {
self[name] = function() {
return method.apply(instance, arguments);
};
}
});
};
Node.prototype.start = function(callback) {
var self = this;
var servicesOrder = this.getServiceOrder();
@ -561,14 +171,12 @@ Node.prototype.start = function(callback) {
servicesOrder,
function(service, next) {
log.info('Starting ' + service.name);
if (service.module) {
self.addModule(service);
self.modules[service.name].start(next);
} else {
// TODO: implement bitcoind, chain and db as modules
self[service.name].start(next);
try {
self._instantiateService(service);
} catch(err) {
return callback(err);
}
self.services[service.name].start(next);
},
callback
);
@ -586,12 +194,7 @@ Node.prototype.stop = function(callback) {
services,
function(service, next) {
log.info('Stopping ' + service.name);
if (service.module) {
self.modules[service.name].stop(next);
} else {
self[service.name].stop(next);
}
self.services[service.name].stop(next);
},
callback
);

View File

@ -10,10 +10,10 @@ var _ = bitcore.deps._;
/**
* @param {String} configFilePath - The absolute path to the configuration file
* @param {String} module - The name of the module
* @param {String} service - The name of the service
* @param {Function} done
*/
function addConfig(configFilePath, module, done) {
function addConfig(configFilePath, service, done) {
$.checkState(path.isAbsolute(configFilePath), 'An absolute path is expected');
fs.readFile(configFilePath, function(err, data) {
if (err) {
@ -21,12 +21,12 @@ function addConfig(configFilePath, module, done) {
}
var config = JSON.parse(data);
$.checkState(
Array.isArray(config.modules),
'Configuration file is expected to have a modules array.'
Array.isArray(config.services),
'Configuration file is expected to have a services array.'
);
config.modules.push(module);
config.modules = _.unique(config.modules);
config.modules.sort(function(a, b) {
config.services.push(service);
config.services = _.unique(config.services);
config.services.sort(function(a, b) {
return a > b;
});
fs.writeFile(configFilePath, JSON.stringify(config, null, 2), done);
@ -35,12 +35,12 @@ function addConfig(configFilePath, module, done) {
/**
* @param {String} configDir - The absolute configuration directory path
* @param {String} module - The name of the module
* @param {String} service - The name of the service
* @param {Function} done
*/
function addModule(configDir, module, done) {
function addService(configDir, service, done) {
$.checkState(path.isAbsolute(configDir), 'An absolute path is expected');
var npm = spawn('npm', ['install', module, '--save'], {cwd: configDir});
var npm = spawn('npm', ['install', service, '--save'], {cwd: configDir});
npm.stdout.on('data', function(data) {
process.stdout.write(data);
@ -52,7 +52,7 @@ function addModule(configDir, module, done) {
npm.on('close', function(code) {
if (code !== 0) {
return done(new Error('There was an error installing module: ' + module));
return done(new Error('There was an error installing service: ' + service));
} else {
return done();
}
@ -62,7 +62,7 @@ function addModule(configDir, module, done) {
/**
* @param {String} options.cwd - The current working directory
* @param {String} options.dirname - The bitcore-node configuration directory
* @param {Array} options.modules - An array of strings of module names
* @param {Array} options.services - An array of strings of service names
* @param {Function} done - A callback function called when finished
*/
function add(options, done) {
@ -72,10 +72,10 @@ function add(options, done) {
_.isString(options.path) && path.isAbsolute(options.path),
'An absolute path is expected'
);
$.checkArgument(Array.isArray(options.modules));
$.checkArgument(Array.isArray(options.services));
var configPath = options.path;
var modules = options.modules;
var services = options.services;
var bitcoreConfigPath = path.resolve(configPath, 'bitcore-node.json');
var packagePath = path.resolve(configPath, 'package.json');
@ -87,15 +87,15 @@ function add(options, done) {
}
async.eachSeries(
modules,
function(module, next) {
// npm install <module_name> --save
addModule(configPath, module, function(err) {
services,
function(service, next) {
// npm install <service_name> --save
addService(configPath, service, function(err) {
if (err) {
return next(err);
}
// add module to bitcore-node.json
addConfig(bitcoreConfigPath, module, next);
// add service to bitcore-node.json
addConfig(bitcoreConfigPath, service, next);
});
}, done
);

View File

@ -12,7 +12,7 @@ var fs = require('fs');
var BASE_CONFIG = {
name: 'My Node',
modules: [
services: [
'address'
],
datadir: './data',
@ -61,7 +61,7 @@ function createBitcoinDirectory(datadir, done) {
* @param {String} configDir - The absolute path
* @param {String} name - The name of the node
* @param {String} datadir - The bitcoin database directory
* @param {Boolean} isGlobal - If the configuration depends on globally installed node modules.
* @param {Boolean} isGlobal - If the configuration depends on globally installed node services.
* @param {Function} done - The callback function called when finished
*/
function createConfigDirectory(configDir, name, datadir, isGlobal, done) {

View File

@ -13,7 +13,7 @@ function getDefaultConfig() {
datadir: process.env.BITCORENODE_DIR || path.resolve(process.env.HOME, '.bitcoin'),
network: process.env.BITCORENODE_NETWORK || 'livenet',
port: process.env.BITCORENODE_PORT || 3001,
modules: ['address']
services: ['bitcoind', 'db', 'address']
}
};
}

View File

@ -15,42 +15,42 @@ var interval = false;
function start(options) {
/* jshint maxstatements: 100 */
var bitcoreModules = [];
var services = [];
var configPath = options.path;
var config = options.config;
if (config.modules) {
for (var i = 0; i < config.modules.length; i++) {
var moduleName = config.modules[i];
var bitcoreModule;
if (config.services) {
for (var i = 0; i < config.services.length; i++) {
var serviceName = config.services[i];
var service;
try {
// first try in the built-in bitcore-node modules directory
bitcoreModule = require(path.resolve(__dirname, '../modules/' + moduleName));
// first try in the built-in bitcore-node services directory
service = require(path.resolve(__dirname, '../services/' + serviceName));
} catch(e) {
// check if the package.json specifies a specific file to use
var modulePackage = require(moduleName + '/package.json');
var bitcoreNodeModule = moduleName;
if (modulePackage.bitcoreNode) {
bitcoreNodeModule = moduleName + '/' + modulePackage.bitcoreNode;
var servicePackage = require(serviceName + '/package.json');
var serviceModule = serviceName;
if (servicePackage.bitcoreNode) {
serviceModule = serviceName + '/' + servicePackage.bitcoreNode;
}
bitcoreModule = require(bitcoreNodeModule);
service = require(serviceModule);
}
// check that the module supports expected methods
if (!bitcoreModule.prototype ||
!bitcoreModule.dependencies ||
!bitcoreModule.prototype.start ||
!bitcoreModule.prototype.stop) {
// check that the service supports expected methods
if (!service.prototype ||
!service.dependencies ||
!service.prototype.start ||
!service.prototype.stop) {
throw new Error(
'Could not load module "' + moduleName + '" as it does not support necessary methods.'
'Could not load service "' + serviceName + '" as it does not support necessary methods.'
);
}
bitcoreModules.push({
name: moduleName,
module: bitcoreModule,
dependencies: bitcoreModule.dependencies
services.push({
name: serviceName,
module: service,
dependencies: service.dependencies
});
}
@ -61,15 +61,15 @@ function start(options) {
// expand to the full path
fullConfig.datadir = path.resolve(configPath, config.datadir);
// load the modules
fullConfig.modules = bitcoreModules;
// load the services
fullConfig.services = services;
var node = new BitcoreNode(fullConfig);
function logSyncStatus() {
log.info(
'Sync Status: Tip:', node.chain.tip.hash,
'Height:', node.chain.tip.__height,
'Sync Status: Tip:', node.services.db.tip.hash,
'Height:', node.services.db.tip.__height,
'Rate:', count/10, 'blocks per second'
);
}
@ -184,15 +184,17 @@ function start(options) {
log.error(err);
});
node.chain.on('addblock', function(block) {
count++;
// Initialize logging if not already instantiated
if (!interval) {
interval = setInterval(function() {
logSyncStatus();
count = 0;
}, 10000);
}
node.on('ready', function() {
node.services.db.on('addblock', function(block) {
count++;
// Initialize logging if not already instantiated
if (!interval) {
interval = setInterval(function() {
logSyncStatus();
count = 0;
}, 10000);
}
});
});
node.on('stopping', function() {

View File

@ -1,13 +1,20 @@
'use strict';
var Module = function(options) {
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Service = function(options) {
EventEmitter.call(this);
this.node = options.node;
};
util.inherits(Service, EventEmitter);
/**
* Describes the dependencies that should be loaded before this module.
* Describes the dependencies that should be loaded before this service.
*/
Module.dependencies = [];
Service.dependencies = [];
/**
* blockHandler
@ -15,7 +22,7 @@ Module.dependencies = [];
* @param {Boolean} add - whether the block is being added or removed
* @param {Function} callback - call with the leveldb database operations to perform
*/
Module.prototype.blockHandler = function(block, add, callback) {
Service.prototype.blockHandler = function(block, add, callback) {
// implement in the child class
setImmediate(callback);
};
@ -24,7 +31,7 @@ Module.prototype.blockHandler = function(block, add, callback) {
* the bus events available for subscription
* @return {Array} an array of event info
*/
Module.prototype.getPublishEvents = function() {
Service.prototype.getPublishEvents = function() {
// Example:
// return [
// ['eventname', this, this.subscribeEvent, this.unsubscribeEvent],
@ -36,7 +43,7 @@ Module.prototype.getPublishEvents = function() {
* the API methods to expose
* @return {Array} return array of methods
*/
Module.prototype.getAPIMethods = function() {
Service.prototype.getAPIMethods = function() {
// Example:
// return [
// ['getData', this, this.getData, 1]
@ -46,16 +53,16 @@ Module.prototype.getAPIMethods = function() {
};
// Example:
// Module.prototype.getData = function(arg1, callback) {
// Service.prototype.getData = function(arg1, callback) {
//
// };
Module.prototype.start = function(done) {
Service.prototype.start = function(done) {
setImmediate(done);
};
Module.prototype.stop = function(done) {
Service.prototype.stop = function(done) {
setImmediate(done);
};
module.exports = Module;
module.exports = Service;

View File

@ -1,6 +1,6 @@
'use strict';
var BaseModule = require('../module');
var BaseService = require('../service');
var inherits = require('util').inherits;
var async = require('async');
var index = require('../');
@ -14,30 +14,30 @@ var EventEmitter = require('events').EventEmitter;
var PublicKey = bitcore.PublicKey;
var Address = bitcore.Address;
var AddressModule = function(options) {
BaseModule.call(this, options);
var AddressService = function(options) {
BaseService.call(this, options);
this.subscriptions = {};
this.subscriptions['address/transaction'] = {};
this.subscriptions['address/balance'] = {};
this.node.bitcoind.on('tx', this.transactionHandler.bind(this));
this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this));
};
inherits(AddressModule, BaseModule);
inherits(AddressService, BaseService);
AddressModule.dependencies = [
AddressService.dependencies = [
'bitcoind',
'db'
];
AddressModule.PREFIXES = {
AddressService.PREFIXES = {
OUTPUTS: 'outs',
SPENTS: 'sp'
};
AddressModule.prototype.getAPIMethods = function() {
AddressService.prototype.getAPIMethods = function() {
return [
['getBalance', this, this.getBalance, 2],
['getOutputs', this, this.getOutputs, 2],
@ -47,7 +47,7 @@ AddressModule.prototype.getAPIMethods = function() {
];
};
AddressModule.prototype.getPublishEvents = function() {
AddressService.prototype.getPublishEvents = function() {
return [
{
name: 'address/transaction',
@ -73,7 +73,7 @@ AddressModule.prototype.getPublishEvents = function() {
* @param {Number} outputIndex - The index of the output in the transaction
* @param {Boolean} rejected - If the transaction was rejected by the mempool
*/
AddressModule.prototype.transactionOutputHandler = function(messages, tx, outputIndex, rejected) {
AddressService.prototype.transactionOutputHandler = function(messages, tx, outputIndex, rejected) {
var script = tx.outputs[outputIndex].script;
// If the script is invalid skip
@ -82,10 +82,10 @@ AddressModule.prototype.transactionOutputHandler = function(messages, tx, output
}
// Find the address for the output
var address = script.toAddress(this.node.db.network);
var address = script.toAddress(this.node.network);
if (!address && script.isPublicKeyOut()) {
var pubkey = script.chunks[0].buf;
address = Address.fromPublicKey(new PublicKey(pubkey), this.node.db.network);
address = Address.fromPublicKey(new PublicKey(pubkey), this.node.network);
} else if (!address){
return;
}
@ -112,7 +112,7 @@ AddressModule.prototype.transactionOutputHandler = function(messages, tx, output
* @param {Boolean} txInfo.mempool - If the transaction was accepted in the mempool
* @param {String} txInfo.hash - The hash of the transaction
*/
AddressModule.prototype.transactionHandler = function(txInfo) {
AddressService.prototype.transactionHandler = function(txInfo) {
// Basic transaction format is handled by the daemon
// and we can safely assume the buffer is properly formatted.
@ -130,7 +130,7 @@ AddressModule.prototype.transactionHandler = function(txInfo) {
}
};
AddressModule.prototype.blockHandler = function(block, addOutput, callback) {
AddressService.prototype.blockHandler = function(block, addOutput, callback) {
var txs = block.transactions;
var action = 'put';
@ -162,10 +162,10 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) {
continue;
}
var address = script.toAddress(this.node.db.network);
var address = script.toAddress(this.node.network);
if (!address && script.isPublicKeyOut()) {
var pubkey = script.chunks[0].buf;
address = Address.fromPublicKey(new PublicKey(pubkey), this.node.db.network);
address = Address.fromPublicKey(new PublicKey(pubkey), this.node.network);
} else if (!address){
continue;
}
@ -178,7 +178,7 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) {
var addressStr = address.toString();
var scriptHex = output._scriptBuffer.toString('hex');
var key = [AddressModule.PREFIXES.OUTPUTS, addressStr, timestamp, txid, outputIndex].join('-');
var key = [AddressService.PREFIXES.OUTPUTS, addressStr, timestamp, txid, outputIndex].join('-');
var value = [output.satoshis, scriptHex, height].join(':');
operations.push({
@ -217,7 +217,7 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) {
var input = inputs[k].toObject();
operations.push({
type: action,
key: [AddressModule.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'),
key: [AddressService.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'),
value: [txid, k].join(':')
});
}
@ -238,7 +238,7 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) {
* @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
*/
AddressModule.prototype.transactionEventHandler = function(obj) {
AddressService.prototype.transactionEventHandler = function(obj) {
if(this.subscriptions['address/transaction'][obj.address]) {
var emitters = this.subscriptions['address/transaction'][obj.address];
for(var i = 0; i < emitters.length; i++) {
@ -247,7 +247,7 @@ AddressModule.prototype.transactionEventHandler = function(obj) {
}
};
AddressModule.prototype.balanceEventHandler = function(block, address) {
AddressService.prototype.balanceEventHandler = function(block, address) {
if(this.subscriptions['address/balance'][address]) {
var emitters = this.subscriptions['address/balance'][address];
this.getBalance(address, true, function(err, balance) {
@ -262,7 +262,7 @@ AddressModule.prototype.balanceEventHandler = function(block, address) {
}
};
AddressModule.prototype.subscribe = function(name, emitter, addresses) {
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');
@ -274,7 +274,7 @@ AddressModule.prototype.subscribe = function(name, emitter, addresses) {
}
};
AddressModule.prototype.unsubscribe = function(name, emitter, addresses) {
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');
@ -293,7 +293,7 @@ AddressModule.prototype.unsubscribe = function(name, emitter, addresses) {
}
};
AddressModule.prototype.unsubscribeAll = function(name, emitter) {
AddressService.prototype.unsubscribeAll = function(name, emitter) {
$.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter');
for(var address in this.subscriptions[name]) {
@ -305,7 +305,7 @@ AddressModule.prototype.unsubscribeAll = function(name, emitter) {
}
};
AddressModule.prototype.getBalance = function(address, queryMempool, callback) {
AddressService.prototype.getBalance = function(address, queryMempool, callback) {
this.getUnspentOutputs(address, queryMempool, function(err, outputs) {
if(err) {
return callback(err);
@ -323,13 +323,13 @@ AddressModule.prototype.getBalance = function(address, queryMempool, callback) {
});
};
AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback) {
AddressService.prototype.getOutputs = function(addressStr, queryMempool, callback) {
var self = this;
var outputs = [];
var key = [AddressModule.PREFIXES.OUTPUTS, addressStr].join('-');
var key = [AddressService.PREFIXES.OUTPUTS, addressStr].join('-');
var stream = this.node.db.store.createReadStream({
var stream = this.node.services.db.store.createReadStream({
start: key,
end: key + '~'
});
@ -347,7 +347,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback
satoshis: Number(value[0]),
script: value[1],
blockHeight: Number(value[2]),
confirmations: self.node.chain.tip.__height - Number(value[2]) + 1
confirmations: self.node.services.db.tip.__height - Number(value[2]) + 1
};
outputs.push(output);
@ -368,7 +368,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback
}
if(queryMempool) {
outputs = outputs.concat(self.node.bitcoind.getMempoolOutputs(addressStr));
outputs = outputs.concat(self.node.services.bitcoind.getMempoolOutputs(addressStr));
}
callback(null, outputs);
@ -378,7 +378,7 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback
};
AddressModule.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) {
AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) {
var self = this;
if(!Array.isArray(addresses)) {
@ -403,7 +403,7 @@ AddressModule.prototype.getUnspentOutputs = function(addresses, queryMempool, ca
});
};
AddressModule.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) {
AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) {
var self = this;
@ -424,26 +424,26 @@ AddressModule.prototype.getUnspentOutputsForAddress = function(address, queryMem
});
};
AddressModule.prototype.isUnspent = function(output, queryMempool, callback) {
AddressService.prototype.isUnspent = function(output, queryMempool, callback) {
this.isSpent(output, queryMempool, function(spent) {
callback(!spent);
});
};
AddressModule.prototype.isSpent = function(output, queryMempool, callback) {
AddressService.prototype.isSpent = function(output, queryMempool, callback) {
var self = this;
var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid;
setImmediate(function() {
callback(self.node.bitcoind.isSpent(txid, output.outputIndex));
callback(self.node.services.bitcoind.isSpent(txid, output.outputIndex));
});
};
AddressModule.prototype.getSpendInfoForOutput = function(txid, outputIndex, callback) {
AddressService.prototype.getSpendInfoForOutput = function(txid, outputIndex, callback) {
var self = this;
var key = [AddressModule.PREFIXES.SPENTS, txid, outputIndex].join('-');
this.node.db.store.get(key, function(err, value) {
var key = [AddressService.PREFIXES.SPENTS, txid, outputIndex].join('-');
this.node.services.db.store.get(key, function(err, value) {
if(err) {
return callback(err);
}
@ -459,7 +459,7 @@ AddressModule.prototype.getSpendInfoForOutput = function(txid, outputIndex, call
});
};
AddressModule.prototype.getAddressHistory = function(addresses, queryMempool, callback) {
AddressService.prototype.getAddressHistory = function(addresses, queryMempool, callback) {
var self = this;
if(!Array.isArray(addresses)) {
@ -482,7 +482,7 @@ AddressModule.prototype.getAddressHistory = function(addresses, queryMempool, ca
});
};
AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMempool, callback) {
AddressService.prototype.getAddressHistoryForAddress = function(address, queryMempool, callback) {
var self = this;
var txinfos = {};
@ -492,19 +492,19 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem
return callback(null, txinfos[txid]);
}
self.node.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) {
self.node.services.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) {
if(err) {
return callback(err);
}
transaction.populateInputs(self.node.db, [], function(err) {
transaction.populateInputs(self.node.services.db, [], function(err) {
if(err) {
return callback(err);
}
var confirmations = 0;
if(transaction.__height >= 0) {
confirmations = self.node.chain.tip.__height - transaction.__height;
confirmations = self.node.services.db.tip.__height - transaction.__height;
}
txinfos[transaction.hash] = {
@ -581,4 +581,4 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem
});
};
module.exports = AddressModule;
module.exports = AddressService;

220
lib/services/bitcoind.js Normal file
View File

@ -0,0 +1,220 @@
'use strict';
var util = require('util');
var bindings = require('bindings')('bitcoind.node');
var mkdirp = require('mkdirp');
var fs = require('fs');
var bitcore = require('bitcore');
var $ = bitcore.util.preconditions;
var index = require('../');
var log = index.log;
var Service = require('../service');
/**
* Provides an interface to native bindings to Bitcoin Core
* @param {Object} options
* @param {String} options.datadir - The bitcoin data directory
* @param {Node} options.node - A reference to the node
*/
function Bitcoin(options) {
if (!(this instanceof Bitcoin)) {
return new Bitcoin(options);
}
var self = this;
Service.call(this, options);
if (Object.keys(this.instances).length) {
throw new Error('Bitcoin cannot be instantiated more than once.');
}
$.checkState(this.node.datadir, 'Node is missing datadir property');
Object.keys(exports).forEach(function(key) {
self[key] = exports[key];
});
}
util.inherits(Bitcoin, Service);
Bitcoin.dependencies = [];
Bitcoin.instances = {};
Bitcoin.prototype.instances = Bitcoin.instances;
Bitcoin.__defineGetter__('global', function() {
return Bitcoin.instances[Object.keys(Bitcoin.instances)[0]];
});
Bitcoin.prototype.__defineGetter__('global', function() {
return Bitcoin.global;
});
Bitcoin.DEFAULT_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n';
Bitcoin.prototype._loadConfiguration = function() {
/* jshint maxstatements: 25 */
$.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);
}
if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, Bitcoin.DEFAULT_CONFIG);
}
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;
}
}
$.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'
);
};
Bitcoin.prototype.start = function(callback) {
var self = this;
this._loadConfiguration();
if (this.instances[this.datadir]) {
return callback(new Error('Bitcoin already started'));
}
this.instances[this.datadir] = true;
bindings.start({
datadir: this.node.datadir,
network: this.node.network.name
}, function(err) {
if(err) {
return callback(err);
}
self._started = true;
bindings.onBlocksReady(function(err, result) {
function onTipUpdateListener(result) {
if (result) {
// Emit and event that the tip was updated
self.height = result;
self.emit('tip', result);
// TODO stopping status
if(!self.stopping) {
var percentage = self.syncPercentage();
log.info('Bitcoin Core Daemon New Height:', self.height, 'Percentage:', percentage);
}
// Recursively wait until the next update
bindings.onTipUpdate(onTipUpdateListener);
}
}
bindings.onTipUpdate(onTipUpdateListener);
bindings.startTxMon(function(txs) {
for(var i = 0; i < txs.length; i++) {
self.emit('tx', txs[i]);
}
});
// Set the current chain height
var info = self.getInfo();
self.height = info.blocks;
// Get the genesis block
self.getBlock(0, function(err, block) {
self.genesisBuffer = block;
self.emit('ready', result);
log.info('Bitcoin Daemon Ready');
setImmediate(callback);
});
});
});
};
Bitcoin.prototype.isSynced = function() {
return bindings.isSynced();
};
Bitcoin.prototype.syncPercentage = function() {
return bindings.syncPercentage();
};
Bitcoin.prototype.getBlock = function(blockhash, callback) {
return bindings.getBlock(blockhash, callback);
};
Bitcoin.prototype.isSpent = function(txid, outputIndex) {
return bindings.isSpent(txid, outputIndex);
};
Bitcoin.prototype.getBlockIndex = function(blockHash) {
return bindings.getBlockIndex(blockHash);
};
Bitcoin.prototype.estimateFee = function(blocks) {
return bindings.estimateFee(blocks);
};
Bitcoin.prototype.sendTransaction = function(transaction, allowAbsurdFees) {
return bindings.sendTransaction(transaction, allowAbsurdFees);
};
Bitcoin.prototype.getTransaction = function(txid, queryMempool, callback) {
return bindings.getTransaction(txid, queryMempool, callback);
};
Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) {
return bindings.getTransactionWithBlockInfo(txid, queryMempool, callback);
};
Bitcoin.prototype.getMempoolOutputs = function(address) {
return bindings.getMempoolOutputs(address);
};
Bitcoin.prototype.addMempoolUncheckedTransaction = function(txBuffer) {
return bindings.addMempoolUncheckedTransaction(txBuffer);
};
Bitcoin.prototype.getInfo = function() {
return bindings.getInfo();
};
Bitcoin.prototype.stop = function(callback) {
return bindings.stop(function(err, status) {
setImmediate(function() {
if (err) {
return callback(err);
} else {
log.info(status);
return callback();
}
});
});
};
module.exports = Bitcoin;

670
lib/services/db.js Normal file
View File

@ -0,0 +1,670 @@
'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');
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');
var utils = require('../utils');
var MAX_STACK_DEPTH = 1000;
/**
* Represents the current state of the bitcoin blockchain. Other services
* can extend the data that is indexed by implementing a `blockHandler` method.
*
* @param {Object} options
* @param {String} options.datadir - The bitcoin data directory
* @param {Node} options.node - A reference to the node
*/
function DB(options) {
/* jshint maxstatements: 20 */
if (!(this instanceof DB)) {
return new DB(options);
}
if (!options) {
options = {};
}
Service.call(this, options);
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.cache = {
hashes: {}, // dictionary of hash -> prevHash
chainHashes: {}
};
this.lastSavedMetadata = null;
this.lastSavedMetadataThreshold = 0; // Set this during syncing for faster performance
this.levelupStore = leveldown;
if (options.store) {
this.levelupStore = options.store;
}
this.subscriptions = {
transaction: [],
block: []
};
}
util.inherits(DB, Service);
DB.dependencies = ['bitcoind'];
DB.prototype._setDataPath = function() {
$.checkState(this.node.datadir, 'Node is expected to have a "datadir" property');
var regtest = Networks.get('regtest');
if (this.node.network === Networks.livenet) {
this.dataPath = this.node.datadir + '/bitcore-node.db';
} else if (this.node.network === Networks.testnet) {
this.dataPath = this.node.datadir + '/testnet3/bitcore-node.db';
} else if (this.node.network === regtest) {
this.dataPath = this.node.datadir + '/regtest/bitcore-node.db';
} else {
throw new Error('Unknown network: ' + this.network);
}
};
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 });
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) {
var percentage = self.node.services.bitcoind.syncPercentage();
log.info('Bitcoin Core Daemon New Height:', height, 'Percentage:', percentage);
self.sync();
}
});
});
// Does our database already have a tip?
self.getMetadata(function(err, metadata) {
if(err) {
return callback(err);
} else if(!metadata || !metadata.tip) {
self.tip = self.genesis;
self.tip.__height = 0;
self.connectBlock(self.genesis, function(err) {
if(err) {
return callback(err);
}
self.emit('addblock', self.genesis);
self.saveMetadata();
self.sync();
self.emit('ready');
setImmediate(callback);
});
} else {
metadata.tip = metadata.tip;
self.getBlock(metadata.tip, function(err, tip) {
if(err) {
return callback(err);
}
self.tip = tip;
self.tip.__height = metadata.tipHeight;
self.cache = metadata.cache;
self.sync();
self.emit('ready');
setImmediate(callback);
});
}
});
};
DB.prototype.stop = function(callback) {
// TODO Figure out how to call this.store.close() without issues
setImmediate(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 - A function that accepts: Error
*/
DB.prototype.close = function(callback) {
this.store.close(callback);
};
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('transaction', {
rejected: !txInfo.mempool,
tx: tx
});
}
};
DB.prototype.getAPIMethods = function() {
var methods = [
['getBlock', this, this.getBlock, 1],
['getTransaction', this, this.getTransaction, 2],
['getTransactionWithBlockInfo', this, this.getTransactionWithBlockInfo, 2],
['sendTransaction', this, this.sendTransaction, 1],
['estimateFee', this, this.estimateFee, 1]
];
return methods;
};
DB.prototype.getBlock = function(hash, callback) {
this.node.services.bitcoind.getBlock(hash, function(err, blockData) {
if (err) {
return callback(err);
}
callback(null, Block.fromBuffer(blockData));
});
};
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));
});
};
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.__height = obj.height;
tx.__timestamp = obj.timestamp;
callback(null, tx);
});
};
DB.prototype.sendTransaction = function(tx, callback) {
if (tx instanceof Transaction) {
tx = tx.toString();
}
$.checkArgument(typeof tx === 'string', 'Argument must be a hex string or Transaction');
try {
var txid = this.node.services.bitcoind.sendTransaction(tx);
return callback(null, txid);
} catch(err) {
return callback(err);
}
};
DB.prototype.estimateFee = function(blocks, callback) {
var self = this;
setImmediate(function() {
callback(null, self.node.services.bitcoind.estimateFee(blocks));
});
};
DB.prototype.getPublishEvents = function() {
return [
{
name: 'transaction',
scope: this,
subscribe: this.subscribe.bind(this, 'transaction'),
unsubscribe: this.unsubscribe.bind(this, 'transaction')
},
{
name: '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'));
}
});
};
/**
* Saves metadata to the database
* @param {Object} metadata - The metadata
* @param {Function} callback - A function that accepts: Error
*/
DB.prototype.putMetadata = function(metadata, callback) {
this.store.put('metadata', JSON.stringify(metadata), {}, callback);
};
DB.prototype.saveMetadata = function(callback) {
var self = this;
callback = callback || function() {};
if(self.lastSavedMetadata && Date.now() < self.lastSavedMetadata.getTime() + self.lastSavedMetadataThreshold) {
return callback();
}
var metadata = {
tip: self.tip ? self.tip.hash : null,
tipHeight: self.tip && self.tip.__height ? self.tip.__height : 0,
cache: self.cache
};
self.lastSavedMetadata = new Date();
self.putMetadata(metadata, callback);
};
/**
* Retrieves metadata from the database
* @param {Function} callback - A function that accepts: Error and Object
*/
DB.prototype.getMetadata = function(callback) {
var self = this;
self.store.get('metadata', {}, function(err, data) {
if (err instanceof levelup.errors.NotFoundError) {
return callback(null, {});
} else if (err) {
return callback(err);
}
var metadata;
try {
metadata = JSON.parse(data);
} catch(e) {
return callback(new Error('Could not parse metadata'));
}
callback(null, metadata);
});
};
/**
* 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
* and save 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('block', block.hash);
}
async.eachSeries(
this.node.services,
function(mod, next) {
mod.blockHandler.call(mod, block, add, function(err, ops) {
if (err) {
return next(err);
}
if (ops) {
operations = operations.concat(ops);
}
next();
});
},
function(err) {
if (err) {
return callback(err);
}
log.debug('Updating the database with operations', operations);
self.store.batch(operations, callback);
}
);
};
/**
* Will get an array of hashes all the way to the genesis block for
* the chain based on "block hash" as the tip.
*
* @param {String} block hash - a block hash
* @param {Function} callback - A function that accepts: Error and Array of hashes
*/
DB.prototype.getHashes = function getHashes(tipHash, callback) {
var self = this;
$.checkArgument(utils.isHash(tipHash));
var hashes = [];
var depth = 0;
function getHashAndContinue(err, hash) {
/* jshint maxstatements: 20 */
if (err) {
return callback(err);
}
depth++;
hashes.unshift(hash);
if (hash === self.genesis.hash) {
// Stop at the genesis block
self.cache.chainHashes[tipHash] = hashes;
callback(null, hashes);
} else if(self.cache.chainHashes[hash]) {
hashes.shift();
hashes = self.cache.chainHashes[hash].concat(hashes);
delete self.cache.chainHashes[hash];
self.cache.chainHashes[tipHash] = hashes;
callback(null, hashes);
} else {
// Continue with the previous hash
// check cache first
var prevHash = self.cache.hashes[hash];
if(prevHash) {
// Don't let the stack get too deep. Otherwise we will crash.
if(depth >= MAX_STACK_DEPTH) {
depth = 0;
return setImmediate(function() {
getHashAndContinue(null, prevHash);
});
} else {
return getHashAndContinue(null, prevHash);
}
} else {
// do a db call if we don't have it
self.getPrevHash(hash, function(err, prevHash) {
if(err) {
return callback(err);
}
return getHashAndContinue(null, prevHash);
});
}
}
}
getHashAndContinue(null, tipHash);
};
/**
* This function will find the common ancestor between the current chain and a forked block,
* by moving backwards from the forked block until it meets the current chain.
* @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;
// The current chain of hashes will likely already be available in a cache.
self.getHashes(self.tip.hash, function(err, currentHashes) {
if (err) {
done(err);
}
// Create a hash map for faster lookups
var currentHashesMap = {};
var length = currentHashes.length;
for (var i = 0; i < length; i++) {
currentHashesMap[currentHashes[i]] = true;
}
// TODO: expose prevHash as a string from bitcore
var ancestorHash = BufferUtil.reverse(block.header.prevHash).toString('hex');
// We only need to go back until we meet the main chain for the forked block
// and thus don't need to find the entire chain of hashes.
while(ancestorHash && !currentHashesMap[ancestorHash]) {
var blockIndex = self.node.services.bitcoind.getBlockIndex(ancestorHash);
ancestorHash = blockIndex ? blockIndex.prevHash : null;
}
// Hash map is no-longer needed, quickly let
// scavenging garbage collection know to cleanup
currentHashesMap = null;
if (!ancestorHash) {
return done(new Error('Unknown common ancestor.'));
}
done(null, ancestorHash);
});
};
/**
* 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);
}
// 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.saveMetadata();
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) {
return;
}
if (!self.tip) {
return;
}
self.bitcoindSyncing = true;
self.lastSavedMetadataThreshold = 30000;
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;
// Update cache.hashes
self.cache.hashes[block.hash] = prevHash;
// Update cache.chainHashes
self.getHashes(block.hash, function(err, hashes) {
if (err) {
return done(err);
}
// Create indexes
self.connectBlock(block, function(err) {
if (err) {
return done(err);
}
self.tip = block;
log.debug('Saving metadata');
self.saveMetadata();
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.
self.syncRewind(block, done);
}
});
}, function(err) {
if (err) {
Error.captureStackTrace(err);
return self.node.emit('error', err);
}
if(self.node.stopping) {
return;
}
self.bitcoindSyncing = false;
self.lastSavedMetadataThreshold = 0;
// If bitcoind is completely synced
if (self.node.services.bitcoind.isSynced()) {
self.node.emit('synced');
}
});
};
module.exports = DB;

View File

@ -35,7 +35,7 @@
"package": "node bin/package.js",
"upload": "node bin/upload.js",
"start": "node bin/start.js",
"test": "NODE_ENV=test mocha --recursive",
"test": "NODE_ENV=test mocha -R spec --recursive",
"coverage": "istanbul cover _mocha -- --recursive",
"libbitcoind": "node bin/start-libbitcoind.js"
},

View File

@ -7,28 +7,26 @@ var Bus = require('../lib/bus');
describe('Bus', function() {
describe('#subscribe', function() {
it('will call db and modules subscribe function with the correct arguments', function() {
it('will call db and services subscribe function with the correct arguments', function() {
var subscribeDb = sinon.spy();
var subscribeModule = sinon.spy();
var db = {
getPublishEvents: sinon.stub().returns([
{
name: 'dbtest',
scope: this,
subscribe: subscribeDb
}
]
)
};
var subscribeService = sinon.spy();
var node = {
db: db,
modules: {
module1: {
services: {
db: {
getPublishEvents: sinon.stub().returns([
{
name: 'dbtest',
scope: this,
subscribe: subscribeDb
}
])
},
service1: {
getPublishEvents: sinon.stub().returns([
{
name: 'test',
scope: this,
subscribe: subscribeModule,
subscribe: subscribeService,
}
])
}
@ -37,42 +35,40 @@ describe('Bus', function() {
var bus = new Bus({node: node});
bus.subscribe('dbtest', 'a', 'b', 'c');
bus.subscribe('test', 'a', 'b', 'c');
subscribeModule.callCount.should.equal(1);
subscribeService.callCount.should.equal(1);
subscribeDb.callCount.should.equal(1);
subscribeDb.args[0][0].should.equal(bus);
subscribeDb.args[0][1].should.equal('a');
subscribeDb.args[0][2].should.equal('b');
subscribeDb.args[0][3].should.equal('c');
subscribeModule.args[0][0].should.equal(bus);
subscribeModule.args[0][1].should.equal('a');
subscribeModule.args[0][2].should.equal('b');
subscribeModule.args[0][3].should.equal('c');
subscribeService.args[0][0].should.equal(bus);
subscribeService.args[0][1].should.equal('a');
subscribeService.args[0][2].should.equal('b');
subscribeService.args[0][3].should.equal('c');
});
});
describe('#unsubscribe', function() {
it('will call db and modules unsubscribe function with the correct arguments', function() {
it('will call db and services unsubscribe function with the correct arguments', function() {
var unsubscribeDb = sinon.spy();
var unsubscribeModule = sinon.spy();
var db = {
getPublishEvents: sinon.stub().returns([
{
name: 'dbtest',
scope: this,
unsubscribe: unsubscribeDb
}
]
)
};
var unsubscribeService = sinon.spy();
var node = {
db: db,
modules: {
module1: {
services: {
db: {
getPublishEvents: sinon.stub().returns([
{
name: 'dbtest',
scope: this,
unsubscribe: unsubscribeDb
}
])
},
service1: {
getPublishEvents: sinon.stub().returns([
{
name: 'test',
scope: this,
unsubscribe: unsubscribeModule,
unsubscribe: unsubscribeService,
}
])
}
@ -81,43 +77,41 @@ describe('Bus', function() {
var bus = new Bus({node: node});
bus.unsubscribe('dbtest', 'a', 'b', 'c');
bus.unsubscribe('test', 'a', 'b', 'c');
unsubscribeModule.callCount.should.equal(1);
unsubscribeService.callCount.should.equal(1);
unsubscribeDb.callCount.should.equal(1);
unsubscribeDb.args[0][0].should.equal(bus);
unsubscribeDb.args[0][1].should.equal('a');
unsubscribeDb.args[0][2].should.equal('b');
unsubscribeDb.args[0][3].should.equal('c');
unsubscribeModule.args[0][0].should.equal(bus);
unsubscribeModule.args[0][1].should.equal('a');
unsubscribeModule.args[0][2].should.equal('b');
unsubscribeModule.args[0][3].should.equal('c');
unsubscribeService.args[0][0].should.equal(bus);
unsubscribeService.args[0][1].should.equal('a');
unsubscribeService.args[0][2].should.equal('b');
unsubscribeService.args[0][3].should.equal('c');
});
});
describe('#close', function() {
it('will unsubscribe from all events', function() {
var unsubscribeDb = sinon.spy();
var unsubscribeModule = sinon.spy();
var db = {
getPublishEvents: sinon.stub().returns([
{
name: 'dbtest',
scope: this,
unsubscribe: unsubscribeDb
}
]
)
};
var unsubscribeService = sinon.spy();
var node = {
db: db,
modules: {
module1: {
services: {
db: {
getPublishEvents: sinon.stub().returns([
{
name: 'test',
scope: this,
unsubscribe: unsubscribeModule
}
{
name: 'dbtest',
scope: this,
unsubscribe: unsubscribeDb
}
])
},
service1: {
getPublishEvents: sinon.stub().returns([
{
name: 'test',
scope: this,
unsubscribe: unsubscribeService
}
])
}
}
@ -126,11 +120,11 @@ describe('Bus', function() {
bus.close();
unsubscribeDb.callCount.should.equal(1);
unsubscribeModule.callCount.should.equal(1);
unsubscribeService.callCount.should.equal(1);
unsubscribeDb.args[0].length.should.equal(1);
unsubscribeDb.args[0][0].should.equal(bus);
unsubscribeModule.args[0].length.should.equal(1);
unsubscribeModule.args[0][0].should.equal(bus);
unsubscribeDb.args[0][0].should.equal(bus);
unsubscribeService.args[0].length.should.equal(1);
unsubscribeService.args[0][0].should.equal(bus);
});
});

View File

@ -1,266 +0,0 @@
'use strict';
var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
var memdown = require('memdown');
var index = require('../');
var DB = index.DB;
var Chain = index.Chain;
var bitcore = require('bitcore');
var BufferUtil = bitcore.util.buffer;
var Block = bitcore.Block;
var BN = bitcore.crypto.BN;
var chainData = require('./data/testnet-blocks.json');
describe('Bitcoin Chain', function() {
describe('@constructor', function() {
it('can create a new instance with and without `new`', function() {
var chain = new Chain();
chain = Chain();
});
});
describe('#start', function() {
it('should call the callback when base chain is initialized', function(done) {
var chain = new Chain();
chain.node = {};
chain.node.bitcoind = {};
chain.node.bitcoind.genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex');
chain.initialize = function() {
chain.emit('initialized');
};
chain.start(done);
});
});
describe('#initialize', function() {
it('should initialize the chain with the genesis block if no metadata is found in the db', function(done) {
var db = {};
db.getMetadata = sinon.stub().callsArgWith(0, null, {});
db.putBlock = sinon.stub().callsArg(1);
db.putMetadata = sinon.stub().callsArg(1);
db.getTransactionsFromBlock = sinon.stub();
db._onChainAddBlock = sinon.stub().callsArg(1);
db.mempool = {
on: sinon.spy()
};
var node = {
db: db
};
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
chain.on('ready', function() {
should.exist(chain.tip);
db.putBlock.callCount.should.equal(1);
chain.tip.hash.should.equal('genesis');
Number(chain.tip.__weight.toString(10)).should.equal(0);
done();
});
chain.on('error', function(err) {
should.not.exist(err);
done();
});
chain.initialize();
});
it('should initialize the chain with the metadata from the database if it exists', function(done) {
var db = {};
db.getMetadata = sinon.stub().callsArgWith(0, null, {tip: 'block2', tipWeight: 2});
db.putBlock = sinon.stub().callsArg(1);
db.putMetadata = sinon.stub().callsArg(1);
db.getBlock = sinon.stub().callsArgWith(1, null, {hash: 'block2', prevHash: 'block1'});
db.getTransactionsFromBlock = sinon.stub();
db.mempool = {
on: sinon.spy()
};
var node = {
db: db
};
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
chain.getHeightForBlock = sinon.stub().callsArgWith(1, null, 10);
chain.getWeight = sinon.stub().callsArgWith(1, null, new BN(50));
chain.on('ready', function() {
should.exist(chain.tip);
db.putBlock.callCount.should.equal(0);
chain.tip.hash.should.equal('block2');
done();
});
chain.on('error', function(err) {
should.not.exist(err);
done();
});
chain.initialize();
});
it('emit error from getMetadata', function(done) {
var db = {
getMetadata: function(cb) {
cb(new Error('getMetadataError'));
}
};
db.getTransactionsFromBlock = sinon.stub();
db.mempool = {
on: sinon.spy()
};
var node = {
db: db
};
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
chain.on('error', function(error) {
should.exist(error);
error.message.should.equal('getMetadataError');
done();
});
chain.initialize();
});
it('emit error from putBlock', function(done) {
var db = {
getMetadata: function(cb) {
cb(null, null);
},
putBlock: function(block, cb) {
cb(new Error('putBlockError'));
}
};
db.getTransactionsFromBlock = sinon.stub();
db.mempool = {
on: sinon.spy()
};
var node = {
db: db
};
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
chain.on('error', function(error) {
should.exist(error);
error.message.should.equal('putBlockError');
done();
});
chain.initialize();
});
it('emit error from getBlock', function(done) {
var db = {
getMetadata: function(cb) {
cb(null, {tip: 'tip'});
},
getBlock: function(tip, cb) {
cb(new Error('getBlockError'));
}
};
db.getTransactionsFromBlock = sinon.stub();
db.mempool = {
on: sinon.spy()
};
var node = {
db: db
};
var chain = new Chain({node: node, genesis: {hash: 'genesis'}});
chain.on('error', function(error) {
should.exist(error);
error.message.should.equal('getBlockError');
done();
});
chain.initialize();
});
});
describe('#stop', function() {
it('should call the callback', function(done) {
var chain = new Chain();
chain.stop(done);
});
});
describe('#_validateBlock', function() {
it('should call the callback', function(done) {
var chain = new Chain();
chain._validateBlock('block', function(err) {
should.not.exist(err);
done();
});
});
});
describe('#getWeight', function() {
var work = '000000000000000000000000000000000000000000005a7b3c42ea8b844374e9';
var chain = new Chain();
chain.node = {};
chain.node.db = {};
chain.node.bitcoind = {
getBlockIndex: sinon.stub().returns({
chainWork: work
})
};
it('should give the weight as a BN', function(done) {
chain.getWeight('hash', function(err, weight) {
should.not.exist(err);
weight.toString(16, 64).should.equal(work);
done();
});
});
it('should give an error if the weight is undefined', function(done) {
chain.node.bitcoind.getBlockIndex = sinon.stub().returns(undefined);
chain.getWeight('hash2', function(err, weight) {
should.exist(err);
done();
});
});
});
describe('#getHashes', function() {
it('should get an array of chain hashes', function(done) {
var blocks = {};
var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex'));
var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex'));
var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex'));
blocks[genesisBlock.hash] = genesisBlock;
blocks[block1.hash] = block1;
blocks[block2.hash] = block2;
var db = new DB({store: memdown});
db.getPrevHash = function(blockHash, cb) {
// TODO: expose prevHash as a string from bitcore
var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex');
cb(null, prevHash);
};
var node = {
db: db
};
var chain = new Chain({
node: node,
genesis: genesisBlock
});
chain.tip = block2;
delete chain.cache.hashes[block1.hash];
// the test
chain.getHashes(block2.hash, function(err, hashes) {
should.not.exist(err);
should.exist(hashes);
hashes.length.should.equal(3);
done();
});
});
});
});

View File

@ -1,376 +0,0 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var index = require('../');
var DB = index.DB;
var blockData = require('./data/livenet-345003.json');
var bitcore = require('bitcore');
var Block = bitcore.Block;
var transactionData = require('./data/bitcoin-transactions.json');
var errors = index.errors;
var memdown = require('memdown');
var inherits = require('util').inherits;
var BaseModule = require('../lib/module');
var bitcore = require('bitcore');
var Transaction = bitcore.Transaction;
describe('Bitcoin DB', function() {
describe('#start', function() {
it('should emit ready', function(done) {
var db = new DB({store: memdown});
db.node = {};
db.node.bitcoind = {
on: sinon.spy()
};
db.addModule = sinon.spy();
var readyFired = false;
db.on('ready', function() {
readyFired = true;
});
db.start(function() {
readyFired.should.equal(true);
done();
});
});
});
describe('#stop', function() {
it('should immediately call the callback', function(done) {
var db = new DB({store: memdown});
db.stop(function(err) {
should.not.exist(err);
done();
});
});
});
describe('#getTransaction', function() {
it('will return a NotFound error', function(done) {
var db = new DB({store: memdown});
db.node = {};
db.node.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({store: memdown});
db.node = {};
db.node.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({store: memdown});
db.node = {};
db.node.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('#getBlock', function() {
var db = new DB({store: memdown});
var blockBuffer = new Buffer(blockData, 'hex');
var expectedBlock = Block.fromBuffer(blockBuffer);
db.node = {};
db.node.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.bitcoind = {};
db.node.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('#putBlock', function() {
it('should call callback', function(done) {
var db = new DB({store: memdown});
db.putBlock('block', function(err) {
should.not.exist(err);
done();
});
});
});
describe('#getPrevHash', function() {
it('should return prevHash from bitcoind', function(done) {
var db = new DB({store: memdown});
db.node = {};
db.node.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({store: memdown});
db.node = {};
db.node.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 = {
height: 530482,
timestamp: 1439559434000,
buffer: txBuffer
};
var db = new DB({store: memdown});
db.node = {};
db.node.bitcoind = {
getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, info)
};
db.getTransactionWithBlockInfo('2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f', true, function(err, tx) {
should.not.exist(err);
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({store: memdown});
db.node = {};
db.node.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 give the txid on success', function(done) {
var db = new DB({store: memdown});
db.node = {};
db.node.bitcoind = {
sendTransaction: sinon.stub().returns('txid')
};
var tx = new Transaction();
db.sendTransaction(tx, function(err, txid) {
should.not.exist(err);
txid.should.equal('txid');
done();
});
});
it('should give an error if bitcoind threw an error', function(done) {
var db = new DB({store: memdown});
db.node = {};
db.node.bitcoind = {
sendTransaction: sinon.stub().throws(new Error('error'))
};
var tx = new Transaction();
db.sendTransaction(tx, function(err, txid) {
should.exist(err);
done();
});
});
});
describe("#estimateFee", function() {
it('should pass along the fee from bitcoind', function(done) {
var db = new DB({store: memdown});
db.node = {};
db.node.bitcoind = {
estimateFee: sinon.stub().returns(1000)
};
db.estimateFee(5, function(err, fee) {
should.not.exist(err);
fee.should.equal(1000);
db.node.bitcoind.estimateFee.args[0][0].should.equal(5);
done();
});
});
});
describe('#getOutputTotal', function() {
it('should return the correct value including the coinbase', function() {
var totals = [10, 20, 30];
var db = new DB({path: 'path', store: memdown});
var transactions = totals.map(function(total) {
return {
_getOutputAmount: function() {
return total;
},
isCoinbase: function() {
return total === 10 ? true : false;
}
};
});
var grandTotal = db.getOutputTotal(transactions);
grandTotal.should.equal(60);
});
it('should return the correct value excluding the coinbase', function() {
var totals = [10, 20, 30];
var db = new DB({path: 'path', store: memdown});
var transactions = totals.map(function(total) {
return {
_getOutputAmount: function() {
return total;
},
isCoinbase: function() {
return total === 10 ? true : false;
}
};
});
var grandTotal = db.getOutputTotal(transactions, true);
grandTotal.should.equal(50);
});
});
describe('#getInputTotal', function() {
it('should return the correct value', function() {
var totals = [10, 20, 30];
var db = new DB({path: 'path', store: memdown});
var transactions = totals.map(function(total) {
return {
_getInputAmount: function() {
return total;
},
isCoinbase: sinon.stub().returns(false)
};
});
var grandTotal = db.getInputTotal(transactions);
grandTotal.should.equal(60);
});
it('should return 0 if the tx is a coinbase', function() {
var db = new DB({store: memdown});
var tx = {
isCoinbase: sinon.stub().returns(true)
};
var total = db.getInputTotal([tx]);
total.should.equal(0);
});
});
describe('#_onChainAddBlock', function() {
it('should remove block from mempool and call blockHandler with true', function(done) {
var db = new DB({store: memdown});
db.mempool = {
removeBlock: sinon.stub()
};
db.blockHandler = sinon.stub().callsArg(2);
db._onChainAddBlock({hash: 'hash'}, function(err) {
should.not.exist(err);
db.blockHandler.args[0][1].should.equal(true);
done();
});
});
});
describe('#_onChainRemoveBlock', function() {
it('should call blockHandler with false', function(done) {
var db = new DB({store: memdown});
db.blockHandler = sinon.stub().callsArg(2);
db._onChainRemoveBlock({hash: 'hash'}, function(err) {
should.not.exist(err);
db.blockHandler.args[0][1].should.equal(false);
done();
});
});
});
describe('#blockHandler', function() {
var db = new DB({store: memdown});
var Module1 = function() {};
Module1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']);
var Module2 = function() {};
Module2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']);
db.node = {};
db.node.modules = {
module1: new Module1(),
module2: new Module2()
};
db.store = {
batch: sinon.stub().callsArg(1)
};
it('should call blockHandler in all modules and perform operations', function(done) {
db.blockHandler('block', true, function(err) {
should.not.exist(err);
db.store.batch.args[0][0].should.deep.equal(['op1', 'op2', 'op3', 'op4', 'op5']);
done();
});
});
it('should give an error if one of the modules gives an error', function(done) {
var Module3 = function() {};
Module3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error'));
db.node.modules.module3 = new Module3();
db.blockHandler('block', true, function(err) {
should.exist(err);
done();
});
});
});
describe('#getAPIMethods', function() {
it('should return the correct db methods', function() {
var db = new DB({store: memdown});
db.node = {};
db.node.modules = {};
var methods = db.getAPIMethods();
methods.length.should.equal(5);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ describe('#add', function() {
var testDir = path.resolve(basePath, 'temporary-test-data');
var startConfig = {
name: 'My Node',
modules: []
services: []
};
var startPackage = {};
@ -56,7 +56,7 @@ describe('#add', function() {
it('will give an error if expected files do not exist', function(done) {
add({
path: path.resolve(testDir, 's0'),
modules: ['a', 'b', 'c']
services: ['a', 'b', 'c']
}, function(err) {
should.exist(err);
err.message.match(/^Invalid state/);
@ -82,15 +82,15 @@ describe('#add', function() {
addtest({
path: path.resolve(testDir, 's0/s1/'),
modules: ['a', 'b', 'c']
services: ['a', 'b', 'c']
}, function(err) {
should.exist(err);
err.message.should.equal('There was an error installing module: a');
err.message.should.equal('There was an error installing service: a');
done();
});
});
it('will update bitcore-node.json modules', function(done) {
it('will update bitcore-node.json services', function(done) {
var spawn = sinon.stub().returns({
stdout: {
on: sinon.stub()
@ -107,12 +107,12 @@ describe('#add', function() {
});
addtest({
path: path.resolve(testDir, 's0/s1/'),
modules: ['a', 'b', 'c']
services: ['a', 'b', 'c']
}, function(err) {
should.not.exist(err);
var configPath = path.resolve(testDir, 's0/s1/bitcore-node.json');
var config = JSON.parse(fs.readFileSync(configPath));
config.modules.should.deep.equal(['a','b','c']);
config.services.should.deep.equal(['a','b','c']);
done();
});
});

View File

@ -75,7 +75,7 @@ describe('#create', function() {
var config = JSON.parse(fs.readFileSync(configPath));
config.name.should.equal('My Node 1');
config.modules.should.deep.equal(['address']);
config.services.should.deep.equal(['address']);
config.datadir.should.equal('./data');
config.network.should.equal('livenet');

View File

@ -3,18 +3,18 @@
var should = require('chai').should();
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var AddressModule = require('../../lib/modules/address');
var AddressService = require('../../lib/services/address');
describe('#start', function() {
describe('will dynamically create a node from a configuration', function() {
it('require each bitcore-node module', function(done) {
it('require each bitcore-node service', function(done) {
var node;
var TestNode = function(options) {
options.modules[0].should.deep.equal({
options.services[0].should.deep.equal({
name: 'address',
module: AddressModule,
module: AddressService,
dependencies: ['bitcoind', 'db']
});
};
@ -30,7 +30,7 @@ describe('#start', function() {
node = starttest({
path: __dirname,
config: {
modules: [
services: [
'address'
],
datadir: './data'

View File

@ -3,31 +3,31 @@
var should = require('chai').should();
var sinon = require('sinon');
var bitcorenode = require('../../');
var AddressModule = bitcorenode.modules.AddressModule;
var AddressService = bitcorenode.services.Address;
var blockData = require('../data/livenet-345003.json');
var bitcore = require('bitcore');
var Networks = bitcore.Networks;
var EventEmitter = require('events').EventEmitter;
var errors = bitcorenode.errors;
var levelup = require('levelup');
var mockdb = {
bitcoind: {
on: sinon.stub()
}
};
var mocknode = {
db: mockdb,
bitcoind: {
on: sinon.stub()
services: {
bitcoind: {
on: sinon.stub()
}
}
};
describe('AddressModule', function() {
describe('Address Service', function() {
describe('#getAPIMethods', function() {
it('should return the correct methods', function() {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var methods = am.getAPIMethods();
methods.length.should.equal(5);
});
@ -35,7 +35,7 @@ describe('AddressModule', function() {
describe('#getPublishEvents', function() {
it('will return an array of publish event objects', function() {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
am.subscribe = sinon.spy();
am.unsubscribe = sinon.spy();
var events = am.getPublishEvents();
@ -71,7 +71,8 @@ describe('AddressModule', function() {
it('create a message for an address', function() {
var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex');
var tx = bitcore.Transaction().fromBuffer(txBuf);
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
am.node.network = Networks.livenet;
var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
var messages = {};
am.transactionOutputHandler(messages, tx, 0, true);
@ -87,7 +88,7 @@ describe('AddressModule', function() {
describe('#transactionHandler', function() {
it('will pass outputs to transactionOutputHandler and call transactionEventHandler', function() {
var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex');
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
var message = {};
am.transactionOutputHandler = function(messages) {
@ -103,12 +104,6 @@ describe('AddressModule', function() {
describe('#blockHandler', function() {
var am;
var db = {
bitcoind: {
on: sinon.stub()
}
};
var testBlock = bitcore.Block.fromString(blockData);
var data = [
@ -158,7 +153,8 @@ describe('AddressModule', function() {
var value64 = data[2].value;
before(function() {
am = new AddressModule({node: mocknode, network: 'livenet'});
am = new AddressService({node: mocknode});
am.node.network = Networks.livenet;
});
it('should create the correct operations when updating/adding outputs', function(done) {
@ -212,13 +208,7 @@ describe('AddressModule', function() {
});
});
it('should continue if output script is null', function(done) {
var db = {
bitcoind: {
on: sinon.stub()
}
};
var am = new AddressModule({node: mocknode, network: 'livenet'});
var am = new AddressService({node: mocknode, network: 'livenet'});
var block = {
__height: 345003,
@ -247,18 +237,16 @@ describe('AddressModule', function() {
});
it('will call event handlers', function() {
var testBlock = bitcore.Block.fromString(blockData);
var db = {
bitcoind: {
on: sinon.stub()
}
};
var db = {};
var testnode = {
db: db,
bitcoind: {
on: sinon.stub()
services: {
bitcoind: {
on: sinon.stub()
}
}
};
var am = new AddressModule({node: testnode, network: 'livenet'});
var am = new AddressService({node: testnode, network: 'livenet'});
am.transactionEventHandler = sinon.spy();
am.balanceEventHandler = sinon.spy();
@ -286,7 +274,7 @@ describe('AddressModule', function() {
describe('#transactionEventHandler', function() {
it('will emit a transaction if there is a subscriber', function(done) {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var emitter = new EventEmitter();
am.subscriptions['address/transaction'] = {
'1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter]
@ -316,7 +304,7 @@ describe('AddressModule', function() {
describe('#balanceEventHandler', function() {
it('will emit a balance if there is a subscriber', function(done) {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var emitter = new EventEmitter();
am.subscriptions['address/balance'] = {
'1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter]
@ -336,7 +324,7 @@ describe('AddressModule', function() {
describe('#subscribe', function() {
it('will add emitters to the subscribers array (transaction)', function() {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var emitter = new EventEmitter();
var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N';
@ -353,7 +341,7 @@ describe('AddressModule', function() {
am.subscriptions['address/transaction'][address].should.deep.equal([emitter, emitter2]);
});
it('will add an emitter to the subscribers array (balance)', function() {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var emitter = new EventEmitter();
var name = 'address/balance';
var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N';
@ -372,7 +360,7 @@ describe('AddressModule', function() {
describe('#unsubscribe', function() {
it('will remove emitter from subscribers array (transaction)', function() {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var emitter = new EventEmitter();
var emitter2 = new EventEmitter();
var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N';
@ -382,7 +370,7 @@ describe('AddressModule', function() {
am.subscriptions['address/transaction'][address].should.deep.equal([emitter2]);
});
it('will remove emitter from subscribers array (balance)', function() {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var emitter = new EventEmitter();
var emitter2 = new EventEmitter();
var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N';
@ -392,7 +380,7 @@ describe('AddressModule', function() {
am.subscriptions['address/balance'][address].should.deep.equal([emitter2]);
});
it('should unsubscribe from all addresses if no addresses are specified', function() {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var emitter = new EventEmitter();
var emitter2 = new EventEmitter();
am.subscriptions['address/balance'] = {
@ -409,7 +397,7 @@ describe('AddressModule', function() {
describe('#getBalance', function() {
it('should sum up the unspent outputs', function(done) {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
var outputs = [
{satoshis: 1000}, {satoshis: 2000}, {satoshis: 3000}
];
@ -422,7 +410,7 @@ describe('AddressModule', function() {
});
it('will handle error from unspent outputs', function(done) {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
am.getUnspentOutputs = sinon.stub().callsArgWith(2, new Error('error'));
am.getBalance('someaddress', false, function(err) {
should.exist(err);
@ -436,26 +424,27 @@ describe('AddressModule', function() {
describe('#getOutputs', function() {
var am;
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
var db = {};
var db = {
tip: {
__height: 1
}
};
var testnode = {
chain: {
tip: {
__height: 1
services: {
db: db,
bitcoind: {
on: sinon.stub()
}
},
db: db,
bitcoind: {
on: sinon.stub()
}
};
before(function() {
am = new AddressModule({node: testnode});
am = new AddressService({node: testnode});
});
it('should get outputs for an address', function(done) {
var readStream1 = new EventEmitter();
am.node.db.store = {
am.node.services.db.store = {
createReadStream: sinon.stub().returns(readStream1)
};
var mempoolOutputs = [
@ -467,7 +456,7 @@ describe('AddressModule', function() {
blockHeight: 352532
}
];
am.node.bitcoind = {
am.node.services.bitcoind = {
getMempoolOutputs: sinon.stub().returns(mempoolOutputs)
};
@ -510,7 +499,7 @@ describe('AddressModule', function() {
it('should give an error if the readstream has an error', function(done) {
var readStream2 = new EventEmitter();
am.node.db.store = {
am.node.services.db.store = {
createReadStream: sinon.stub().returns(readStream2)
};
@ -535,18 +524,16 @@ describe('AddressModule', function() {
'addr3': ['utxo3']
};
var db = {
bitcoind: {
on: sinon.spy()
}
};
var db = {};
var testnode = {
db: db,
bitcoind: {
on: sinon.stub()
services: {
db: db,
bitcoind: {
on: sinon.stub()
}
}
};
var am = new AddressModule({node: testnode});
var am = new AddressService({node: testnode});
am.getUnspentOutputsForAddress = function(address, queryMempool, callback) {
var result = addresses[address];
if(result instanceof Error) {
@ -569,18 +556,16 @@ describe('AddressModule', function() {
'addr3': ['utxo3']
};
var db = {
bitcoind: {
on: sinon.spy()
}
};
var db = {};
var testnode = {
db: db,
bitcoind: {
on: sinon.stub()
services: {
bitcoind: {
on: sinon.stub()
}
}
};
var am = new AddressModule({node: testnode});
var am = new AddressService({node: testnode});
am.getUnspentOutputsForAddress = function(address, queryMempool, callback) {
var result = addresses[address];
if(result instanceof Error) {
@ -604,18 +589,16 @@ describe('AddressModule', function() {
'addr3': ['utxo3']
};
var db = {
bitcoind: {
on: sinon.spy()
}
};
var db = {};
var testnode = {
db: db,
bitcoind: {
on: sinon.stub()
services: {
bitcoind: {
on: sinon.stub()
}
}
};
var am = new AddressModule({node: testnode});
var am = new AddressService({node: testnode});
am.getUnspentOutputsForAddress = function(address, queryMempool, callback) {
var result = addresses[address];
if(result instanceof Error) {
@ -651,7 +634,7 @@ describe('AddressModule', function() {
];
var i = 0;
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
am.getOutputs = sinon.stub().callsArgWith(2, null, outputs);
am.isUnspent = function(output, queryMempool, callback) {
callback(!outputs[i].spent);
@ -667,7 +650,7 @@ describe('AddressModule', function() {
});
});
it('should handle an error from getOutputs', function(done) {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
am.getOutputs = sinon.stub().callsArgWith(2, new Error('error'));
am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) {
should.exist(err);
@ -676,7 +659,7 @@ describe('AddressModule', function() {
});
});
it('should handle when there are no outputs', function(done) {
var am = new AddressModule({node: mocknode});
var am = new AddressService({node: mocknode});
am.getOutputs = sinon.stub().callsArgWith(2, null, []);
am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) {
should.exist(err);
@ -691,7 +674,7 @@ describe('AddressModule', function() {
var am;
before(function() {
am = new AddressModule({node: mocknode});
am = new AddressService({node: mocknode});
});
it('should give true when isSpent() gives false', function(done) {
@ -721,20 +704,18 @@ describe('AddressModule', function() {
describe('#isSpent', function() {
var am;
var db = {
bitcoind: {
on: sinon.stub()
}
};
var db = {};
var testnode = {
db: db,
bitcoind: {
on: sinon.stub()
services: {
bitcoind: {
on: sinon.stub()
}
}
};
before(function() {
am = new AddressModule({node: testnode});
am.node.bitcoind = {
am = new AddressService({node: testnode});
am.node.services.bitcoind = {
isSpent: sinon.stub().returns(true),
on: sinon.stub()
};
@ -756,12 +737,14 @@ describe('AddressModule', function() {
}
};
var testnode = {
db: db,
bitcoind: {
on: sinon.stub()
services: {
db: db,
bitcoind: {
on: sinon.stub()
}
}
};
var am = new AddressModule({node: testnode});
var am = new AddressService({node: testnode});
am.getSpendInfoForOutput('txid', 3, function(err, info) {
should.not.exist(err);
info.txid.should.equal('spendtxid');
@ -835,6 +818,9 @@ describe('AddressModule', function() {
];
var db = {
tip: {
__height: 1
},
getTransactionWithBlockInfo: function(txid, queryMempool, callback) {
var transaction = {
populateInputs: sinon.stub().callsArg(2)
@ -869,17 +855,14 @@ describe('AddressModule', function() {
}
};
var testnode = {
chain: {
tip: {
__height: 1
services: {
db: db,
bitcoind: {
on: sinon.stub()
}
},
db: db,
bitcoind: {
on: sinon.stub()
}
};
var am = new AddressModule({node: testnode});
var am = new AddressService({node: testnode});
am.getOutputs = sinon.stub().callsArgWith(2, null, incoming);
am.getSpendInfoForOutput = function(txid, outputIndex, callback) {

View File

@ -0,0 +1,50 @@
'use strict';
var should = require('chai').should();
var proxyquire = require('proxyquire');
var fs = require('fs');
var sinon = require('sinon');
var BitcoinService = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/bitcoin.conf'))
}
});
var BadBitcoin = proxyquire('../../lib/services/bitcoind', {
fs: {
readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/badbitcoin.conf'))
}
});
describe('Bitcoin Service', function() {
var baseConfig = {
node: {
datadir: 'testdir',
network: {
name: 'regtest'
}
}
};
describe('#_loadConfiguration', function() {
it('will parse a bitcoin.conf file', function() {
var bitcoind = new BitcoinService(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,
port: 20000,
rpcallowip: '127.0.0.1',
rpcuser: 'bitcoin',
rpcpassword: 'local321'
});
});
it('should throw an exception if txindex isn\'t enabled in the configuration', function() {
var bitcoind = new BadBitcoin(baseConfig);
(function() {
bitcoind._loadConfiguration({datadir: './test'});
}).should.throw('Txindex option');
});
});
});

789
test/services/db.unit.js Normal file
View File

@ -0,0 +1,789 @@
'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');
var Networks = bitcore.Networks;
var Block = bitcore.Block;
var BufferUtil = bitcore.util.buffer;
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 bitcore = require('bitcore');
var Transaction = bitcore.Transaction;
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
};
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.remove(Networks.testnet);
Networks.add({
name: 'regtest',
alias: 'regtest',
pubkeyhash: 0x6f,
privatekey: 0xef,
scripthash: 0xc4,
xpubkey: 0x043587cf,
xprivkey: 0x04358394,
networkMagic: 0xfabfb5da,
port: 18444,
dnsSeeds: [ ]
});
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.remove(regtest);
});
});
describe('#start', function() {
var TestDB;
var genesisBuffer;
before(function() {
TestDB = proxyquire('../../lib/services/db', {
fs: {
existsSync: sinon.stub().returns(true)
},
levelup: sinon.stub()
});
genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex');
});
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.getMetadata = sinon.stub().callsArg(0);
db.connectBlock = sinon.stub().callsArg(1);
db.saveMetadata = sinon.stub();
db.sync = sinon.stub();
var readyFired = false;
db.on('ready', function() {
readyFired = true;
});
db.start(function() {
readyFired.should.equal(true);
done();
});
});
it('genesis block if no metadata is found in the db', function(done) {
var node = {
network: Networks.testnet,
datadir: 'testdir',
services: {
bitcoind: {
genesisBuffer: genesisBuffer,
on: sinon.stub()
}
}
};
var db = new TestDB({node: node});
db.getMetadata = sinon.stub().callsArgWith(0, null, null);
db.connectBlock = sinon.stub().callsArg(1);
db.saveMetadata = sinon.stub();
db.sync = sinon.stub();
db.start(function() {
should.exist(db.tip);
db.tip.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206');
done();
});
});
it('metadata from the database if it exists', function(done) {
var node = {
network: Networks.testnet,
datadir: 'testdir',
services: {
bitcoind: {
genesisBuffer: genesisBuffer,
on: sinon.stub()
}
}
};
var tip = Block.fromBuffer(genesisBuffer);
var db = new TestDB({node: node});
var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206';
db.getMetadata = sinon.stub().callsArgWith(0, null, {
tip: tipHash,
tipHeight: 0
});
db.getBlock = sinon.stub().callsArgWith(1, null, tip);
db.saveMetadata = sinon.stub();
db.sync = sinon.stub();
db.start(function() {
should.exist(db.tip);
db.tip.hash.should.equal(tipHash);
done();
});
});
it('emit error from getMetadata', function(done) {
var node = {
network: Networks.testnet,
datadir: 'testdir',
services: {
bitcoind: {
genesisBuffer: genesisBuffer,
on: sinon.stub()
}
}
};
var db = new TestDB({node: node});
db.getMetadata = sinon.stub().callsArgWith(0, new Error('test'));
db.start(function(err) {
should.exist(err);
err.message.should.equal('test');
done();
});
});
it('emit error from getBlock', function(done) {
var node = {
network: Networks.testnet,
datadir: 'testdir',
services: {
bitcoind: {
genesisBuffer: genesisBuffer,
on: sinon.stub()
}
}
};
var db = new TestDB({node: node});
var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206';
db.getMetadata = sinon.stub().callsArgWith(0, null, {
tip: tipHash,
tipHeigt: 0
});
db.getBlock = sinon.stub().callsArgWith(1, new Error('test'));
db.start(function(err) {
should.exist(err);
err.message.should.equal('test');
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.syncPercentage = sinon.spy();
db.node.services.bitcoind.genesisBuffer = genesisBuffer;
db.getMetadata = sinon.stub().callsArg(0);
db.connectBlock = sinon.stub().callsArg(1);
db.saveMetadata = sinon.stub();
db.sync = sinon.stub();
db.start(function() {
db.sync = function() {
db.node.services.bitcoind.syncPercentage.callCount.should.equal(1);
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.getMetadata = sinon.stub().callsArg(0);
db.connectBlock = sinon.stub().callsArg(1);
db.saveMetadata = sinon.stub();
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 immediately call the callback', function(done) {
var db = new DB(baseConfig);
db.stop(function(err) {
should.not.exist(err);
done();
});
});
});
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('#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('#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 = {
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.__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 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();
db.sendTransaction(tx, function(err, txid) {
should.not.exist(err);
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();
db.sendTransaction(tx, function(err, txid) {
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']);
db.node = {};
db.node.services = {
service1: new Service1(),
service2: new Service2()
};
db.store = {
batch: sinon.stub().callsArg(1)
};
it('should call blockHandler in all services and perform operations', function(done) {
db.runAllBlockHandlers('block', true, function(err) {
should.not.exist(err);
db.store.batch.args[0][0].should.deep.equal(['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();
});
});
});
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(5);
});
});
describe('#getHashes', function() {
it('should get an array of chain hashes', function(done) {
var blocks = {};
var genesisBlock = Block.fromBuffer(new Buffer(chainData[0], 'hex'));
var block1 = Block.fromBuffer(new Buffer(chainData[1], 'hex'));
var block2 = Block.fromBuffer(new Buffer(chainData[2], 'hex'));
blocks[genesisBlock.hash] = genesisBlock;
blocks[block1.hash] = block1;
blocks[block2.hash] = block2;
var db = new DB(baseConfig);
db.genesis = genesisBlock;
db.getPrevHash = function(blockHash, cb) {
// TODO: expose prevHash as a string from bitcore
var prevHash = BufferUtil.reverse(blocks[blockHash].header.prevHash).toString('hex');
cb(null, prevHash);
};
db.tip = block2;
// the test
db.getHashes(block2.hash, function(err, hashes) {
should.not.exist(err);
should.exist(hashes);
hashes.length.should.equal(3);
done();
});
});
});
describe('#findCommonAncestor', function() {
it('will find an ancestor 6 deep', function() {
var db = new DB(baseConfig);
db.getHashes = function(tipHash, callback) {
callback(null, chainHashes);
};
db.tip = {
hash: chainHashes[chainHashes.length]
};
var expectedAncestor = chainHashes[chainHashes.length - 6];
var forkedBlocks = {
'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': {
header: {
prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a')
}
},
'76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': {
header: {
prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c')
}
},
'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': {
header: {
prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31')
}
},
'2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': {
header: {
prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453')
}
},
'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': {
header: {
prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618')
}
},
'3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': {
header: {
prevHash: hexlebuf(expectedAncestor)
}
},
};
db.node.services = {};
db.node.services.bitcoind = {
getBlockIndex: function(hash) {
var block = forkedBlocks[hash];
return {
prevHash: BufferUtil.reverse(block.header.prevHash).toString('hex')
};
}
};
var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82'];
db.findCommonAncestor(block, function(err, ancestorHash) {
if (err) {
throw err;
}
ancestorHash.should.equal(expectedAncestor);
});
});
});
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.saveMetadata = sinon.stub();
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.getHashes = sinon.stub().callsArgWith(1, null);
db.saveMetadata = sinon.stub();
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.saveMetadata = sinon.stub();
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);
});
});
});

View File

@ -4,10 +4,6 @@ var should = require('chai').should();
var sinon = require('sinon');
var bitcoinlib = require('../');
var Transaction = bitcoinlib.Transaction;
var transactionData = require('./data/bitcoin-transactions.json');
var memdown = require('memdown');
var DB = bitcoinlib.DB;
var db = new DB({store: memdown});
var levelup = require('levelup');
describe('Bitcoin Transaction', function() {