diff --git a/README.md b/README.md index e66a0103..fa5e22c1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Bitcoin full node for building applications and services with Node.js. A node ## Install ```bash -npm install -g bitcore-node@0.2.0-beta.X +npm install -g bitcore-node@latest bitcore-node start ``` @@ -31,6 +31,13 @@ To start bitcore-node as a daemon: bitcore-node start --daemon ``` +## Add-on Services + +There are several add-on services available to extend the functionality of Bitcore Node: + +- [Insight API](https://github.com/bitpay/insight-api/tree/v0.3.0) +- [Insight UI](https://github.com/bitpay/insight/tree/v0.3.0) + ## Documentation - [Services](docs/services.md) diff --git a/docs/node.md b/docs/node.md index 4cc9ef93..aa7cdbd2 100644 --- a/docs/node.md +++ b/docs/node.md @@ -16,14 +16,23 @@ A node represents a collection of services that are loaded together. For more in ```js -var BitcoinNode = require('bitcore-node').Node; +var index = require('bitcore-node'); +var Bitcoin = index.services.Bitcoin; +var Node = index.Node; var configuration = { datadir: '~/.bitcoin', - network: 'testnet' + network: 'testnet', + services: [ + { + name: 'bitcoind', + module: Bitcoin, + config: {} + } + ] }; -var node = new BitcoinNode(configuration); +var node = new Node(configuration); node.on('ready', function() { console.log('Bitcoin Node Ready'); diff --git a/docs/services/address.md b/docs/services/address.md index 63c4f0ff..8e5ee824 100644 --- a/docs/services/address.md +++ b/docs/services/address.md @@ -1,35 +1,142 @@ # 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. +The address service builds on the [Bitcoin Service](bitcoind.md) and the [Database Service](db.md) to add additional functionality for querying and subscribing to information based on bitcoin addresses. This will typically represent the core functionality for wallet applications. ## API Documentation -Get Unspent Outputs +These methods are exposed over the JSON-RPC interface and can be called directly from a node via: ```js -var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; +node.services.address. +``` + +**Get Unspent Outputs** + +One of the most common uses will be to retrieve unspent outputs necessary to create a transaction, here is how to get the unspent outputs for an address: + +```js +var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; var includeMempool = true; -node.getUnspentOutputs(address, includeMempool, function(err, unspentOutputs) { - //... +node.services.address.getUnspentOutputs(address, includeMempool, function(err, unspentOutputs) { + // see below }); ``` -View Balances +The `unspentOutputs` will have the format: ```js -var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; +[ + { + address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW', + txid: '9d956c5d324a1c2b12133f3242deff264a9b9f61be701311373998681b8c1769', + outputIndex: 1, + height: 150, + satoshis: 1000000000, + script: '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac', + confirmations: 3 + } +] +``` + +**View Balances** + +```js +var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; var includeMempool = true; -node.getBalance(address, includeMempool, function(err, balance) { - //... +node.services.address.getBalance(address, includeMempool, function(err, balance) { + // balance will be in satoshis }); ``` -Get Outputs +**View Address History** + +This method will give history of an address limited by a range of block heights by using +the "start" and "end" arguments. The "start" value is the more recent, and greater, block height. +The "end" value is the older, and lesser, block height. This feature is most useful for synchronization +as previous history can be omitted. Furthermore for large ranges of block heights, results can be +paginated by using the "from" and "to" arguments. ```js -var address = '15vkcKf7gB23wLAnZLmbVuMiiVDc1Nm4a2'; -var includeMempool = true; -node.getOutputs(address, includeMempool, function(err, outputs) { - //... +var addresses = ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']; +var options = { + start: 345000, + end: 344000, + queryMempool: true +}; +node.services.address.getAddressHistory(addresses, options, function(err, history) { + // see below }); ``` + +The history format will be: +```js +{ + totalCount: 1, // The total number of items within "start" and "end" + items: [ + { + addresses: { + 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW': { + inputIndexes: [], + outputIndexes: [0] + } + }, + satoshis: 1000000000, + height: 150, // the block height of the transaction + confirmations: 3, + timestamp: 1442948127, // in seconds + fees: 191, + tx: // the populated transaction + } + ] +} +``` + +**View Address Summary** + +```js +var address = 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'; +var options = { + noTxList: false +}; + +node.services.address.getAddressSummary(address, options, function(err, summary) { + // see below +}); +``` + +The `summary` will have the format (values are in satoshis): +```js +{ + totalReceived: 1000000000, + totalSpent: 0, + balance: 1000000000, + unconfirmedBalance: 1000000000, + appearances: 1, // number of transactions + unconfirmedAppearances: 0, + txids: [ + '3f7d13efe12e82f873f4d41f7e63bb64708fc4c942eb8c6822fa5bd7606adb00' + ] +} +``` + +## Events + +For details on instantiating a bus for a node, see the [Bus Documentation](../bus.md). + +- Name: `address/transaction`, Arguments: `[address, address...]` +- Name: `address/balance`, Arguments: `[address, address...]` + +**Examples:** + +```js +bus.subscribe('address/transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); +bus.subscribe('address/balance', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); + +bus.on('address/transaction', function(transaction) { + +}); + +bus.on('address/balance', function(balance) { + +}); +``` \ No newline at end of file diff --git a/docs/services/bitcoind.md b/docs/services/bitcoind.md index 50f11064..5cf83203 100644 --- a/docs/services/bitcoind.md +++ b/docs/services/bitcoind.md @@ -1,21 +1,121 @@ # 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. +The Bitcoin Service adds a native Node.js 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.isMainChain(blockHash)` - Returns true if block is on the main chain. Returns false if it is an orphan. -- `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.getMempoolTransactions()` - Will return an array of transaction buffers. +These methods are currently only available via directly interfacing with a node: + +```js +node.services.bitcoind. +``` + +**Getting Block Information** + +It's possible to query blocks by both block hash and by height. Blocks are given as Node.js buffers and can be parsed via Bitcore: + +```js +var blockHeight = 0; +node.services.bitcoind.getBlock(blockHeight, function(err, blockBuffer) { + if (err) { + throw err; + } + var block = bitcore.Block.fromBuffer(blockBuffer); + console.log(block); +}; + +// check if the block is part of the main chain +var mainChain = node.services.bitcoind.isMainChain(block.hash); +console.log(mainChain); + +// get only the block index (including chain work and previous hash) +var blockIndex = node.services.bitcoind.getBlockIndex(blockHeight); +console.log(blockIndex); +``` + +**Retrieving and Sending Transactions** + +Get a transaction asynchronously by reading it from disk, with an argument to optionally not include the mempool: + +```js +var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; +var queryMempool = true; +node.services.bitcoind.getTransaction(txid, queryMempool, function(err, transactionBuffer) { + if (err) { + throw err; + } + var transaction = bitcore.Transaction().fromBuffer(transactionBuffer); +}); + + +// also retrieve the block timestamp and height +node.services.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, info) { + console.log(info.blockHash); + console.log(info.height); + console.log(info.timestamp); // in seconds + var transaction = bitcore.Transaction().fromBuffer(transactionBuffer); +}); + +``` + +Send a transaction to the network: + +```js +var numberOfBlocks = 3; +var feesPerKilobyte = node.services.bitcoind.estimateFee(numberOfBlocks); // in satoshis + +try { + node.services.bitcoind.sendTransaction(transaction.serialize()); +} catch(err) { + // handle error +} +``` + +Get all of the transactions in the mempool: + +```js +var mempool = node.services.bitcoind.getMempoolTransactions(); +var transactions = []; +for (var i = 0; i < mempool.length; i++) { + transactions.push(bitcore.Transaction().fromBuffer(transactions[i])); +} +``` + +Determine if an output is spent (excluding the mempool): + +```js +var spent = node.services.bitcoind.isSpent(txid, outputIndex); +console.log(spent); +``` + +**Miscellaneous** + +- `bitcoind.start(callback)` - Start the JavaScript Bitcoin node, the callback is called when the daemon is ready. - `bitcoind.getInfo()` - Basic information about the chain including total number of blocks. +- `bitcoind.getTxOutSetInfo()` - 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. +- `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. +## Events + +The Bitcoin Service doesn't expose any events via the Bus, however there are a few events that can be directly registered: + +```js +node.services.bitcoind.on('tip', function(blockHash) { + // a new block tip has been added +}); + +node.services.bitcoind.on('tx', function(txInfo) { + // a new transaction has been broadcast in the network +}); +``` + +The `txInfo` object will have the format: +```js +{ + buffer: , + mempool: true, + hash: '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623' +} +``` diff --git a/docs/services/db.md b/docs/services/db.md index 5c7e31c0..1d7a6201 100644 --- a/docs/services/db.md +++ b/docs/services/db.md @@ -1,45 +1,119 @@ # 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. +This service synchronizes a leveldb database with the [Bitcoin Service](bitcoind.md) block chain by connecting and disconnecting blocks to build new indexes that can be queried. Other services can extend the data that is indexed by implementing a `blockHandler` method, similar to the built-in [Address Service](address.md). + +## Adding Indexes + +For a service to include additional block data, it can implement a `blockHandler` method that will be run to when there are new blocks added or removed. + +```js +CustomService.prototype.blockHandler = function(block, add, callback) { + var transactions = block.transactions; + var operations = []; + operations.push({ + type: add ? 'put' : 'del', + key: 'key', + value: 'value' + }); + callback(null, operations); +}; +``` + +Take a look at the Address Service implementation for more details about how to encode the key, value for the best effeciency and ways to format the keys for streaming reads. + +Additionally the mempool can have an index, the mempool index will be updated once bitcoind and the db have both fully synced. A service can implement a `resetMempoolIndex` method that will be run during this time, and the "synced" event will wait until this task has been finished: + +```js +CustomService.prototype.resetMempoolIndex = function(callback) { + var transactionBuffers = this.node.services.bitcoind.getMempoolTransactions(); + // interact over the transactions asynchronously here + callback(); +}; +``` ## API Documentation -Get Transaction +These methods are exposed over the JSON-RPC interface and can be called directly from a node via: ```js -var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e'; -var includeMempool = true; -node.getTransaction(txid, includeMempool, function(err, transaction) { - //... -}); +node.services.db. ``` -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) { - //... -}); -``` - -Get Block Hashes by Timestamp Range +**Query Blocks by Date** +One of the additional indexes created by the Database Service is querying for blocks by ranges of dates: ```js var newest = 1441914000; // Notice time is in seconds not milliseconds var oldest = 1441911000; -node.getBlockHashesByTimestamp(newest, oldest, function(err, hashes) { - //... +node.services.db.getBlockHashesByTimestamp(newest, oldest, function(err, hashes) { + // hashes will be an array of block hashes }); + ``` + +**Working with Blocks and Transactions as Bitcore Instances** + +```js + +var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e'; +var includeMempool = true; +node.services.db.getTransaction(txid, includeMempool, function(err, transaction) { + console.log(transaction.toObject()); +}); + +var txid = 'c349b124b820fe6e32136c30e99f6c4f115fce4d750838edf0c46d3cb4d7281e'; +var includeMempool = true; +node.services.db.getTransactionWithBlockInfo(txid, includeMempool, function(err, transaction) { + console.log(transaction.toObject()); + console.log(transaction.__blockHash); + console.log(transaction.__height); + console.log(transaction.__timestamp); +}); + +var blockHash = '00000000d17332a156a807b25bc5a2e041d2c730628ceb77e75841056082a2c2'; +node.services.db.getBlock(blockHash, function(err, block) { + console.log(block.toObject()); +}); + +// contruct a transaction +var transaction = bitcore.Transaction(); + +node.services.db.sendTransaction(transaction, function(err) { + if (err) { + throw err; + } + // otherwise the transaction has been sent +}); + +``` + +## Events + +For details on instantiating a bus for a node, see the [Bus Documentation](../bus.md). + +- Name: `db/transaction` +- Name: `db/block` + +**Examples:** +```js +bus.subscribe('db/transaction'); +bus.subscribe('db/block'); + +bus.on('db/block', function(blockHash) { + // blockHash will be a hex string of the block hash +}); + +bus.on('db/transaction', function(txInfo) { + // see below +}); + +``` + +The `txInfo` object will have the format: +```js +{ + rejected: true, // If the transaction was rejected into the mempool + tx: // a Bitcore Transaction instance +} +``` \ No newline at end of file diff --git a/integration/regtest-node.js b/integration/regtest-node.js index 9a7362e5..a0e4699c 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -231,7 +231,7 @@ describe('Node Functionality', function() { var bus = node.openBus(); var block; bus.subscribe('db/block'); - bus.on('block', function(data) { + bus.on('db/block', function(data) { bus.unsubscribe('db/block'); data.should.be.equal(block); done(); @@ -303,7 +303,6 @@ describe('Node Functionality', function() { if (err) { throw err; } - results.totalReceived.should.equal(1000000000); results.totalSpent.should.equal(0); results.balance.should.equal(1000000000); diff --git a/integration/regtest.js b/integration/regtest.js index 701c4038..68521c77 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -446,4 +446,19 @@ describe('Daemon Binding Functionality', function() { }); }); + describe('#getInfo', function() { + it('will get information', function() { + var info = bitcoind.getInfo(); + should.exist(info); + should.exist(info.version); + should.exist(info.blocks); + should.exist(info.timeoffset); + should.exist(info.connections); + should.exist(info.difficulty); + should.exist(info.testnet); + should.exist(info.relayfee); + should.exist(info.errors); + }); + }); + }); diff --git a/lib/bus.js b/lib/bus.js index 73b4383a..720261d0 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -3,6 +3,13 @@ var events = require('events'); var util = require('util'); +/** + * The bus represents a connection to node, decoupled from the transport layer, that can + * listen and subscribe to any events that are exposed by available services. Services + * can expose events that can be subscribed to by implementing a `getPublishEvents` method. + * @param {Object} params + * @param {Node} params.node - A reference to the node + */ function Bus(params) { events.EventEmitter.call(this); this.node = params.node; @@ -10,6 +17,12 @@ function Bus(params) { util.inherits(Bus, events.EventEmitter); +/** + * This function will find the service that exposes the event by name and + * call the associated subscribe method with the arguments excluding the + * first argument of this function. + * @param {String} name - The name of the event + */ Bus.prototype.subscribe = function(name) { var events = []; @@ -28,6 +41,10 @@ Bus.prototype.subscribe = function(name) { } }; +/** + * The inverse of the subscribe method. + * @param {String} name - The name of the event + */ Bus.prototype.unsubscribe = function(name) { var events = []; @@ -46,6 +63,9 @@ Bus.prototype.unsubscribe = function(name) { } }; +/** + * This function will unsubscribe all events. + */ Bus.prototype.close = function() { var events = []; diff --git a/lib/node.js b/lib/node.js index 6309da6c..37eb5751 100644 --- a/lib/node.js +++ b/lib/node.js @@ -12,12 +12,34 @@ var log = index.log; var Bus = require('./bus'); var errors = require('./errors'); +/** + * A node is a hub of services, and will manage starting and stopping the services in + * the correct order based the the dependency chain. The node also holds common configuration + * properties that can be shared across services, such as network settings. + * + * The array of services should have the format: + * { + * name: 'bitcoind', + * config: {}, // options to pass into constructor + * module: + * } + * + * @param {Object} config - The configuration of the node + * @param {Array} config.services - The array of services + * @param {String} config.datadir - The directory for data (e.g. bitcoind datadir) + * @param {Number} config.port - The HTTP port for services + * @param {Boolean} config.https - Enable https + * @param {Object} config.httpsOptions - Options for https + * @param {String} config.httpsOptions.key - Path to key file + * @param {String} config.httpsOptions.cert - Path to cert file + * @param {} + */ function Node(config) { + /* jshint maxstatements: 20 */ if(!(this instanceof Node)) { return new Node(config); } - - this.errors = errors; // So services can use errors without having to have bitcore-node as a dependency + this.errors = errors; this.log = log; this.network = null; this.services = {}; @@ -28,19 +50,21 @@ function Node(config) { $.checkArgument(Array.isArray(config.services)); this._unloadedServices = config.services; } - $.checkState(config.datadir, 'Node config expects "datadir"'); this.datadir = config.datadir; this.port = config.port; this.https = config.https; this.httpsOptions = config.httpsOptions; - this._setNetwork(config); - } util.inherits(Node, EventEmitter); +/** + * Will set the this.network based on a network string. + * @param {Object} config + * @param {String} config.network - Possible options "testnet", "regtest" or "livenet" + */ Node.prototype._setNetwork = function(config) { if (config.network === 'testnet') { this.network = Networks.get('testnet'); @@ -65,10 +89,18 @@ Node.prototype._setNetwork = function(config) { $.checkState(this.network, 'Unrecognized network'); }; +/** + * Will instantiate a new Bus for this node. + * @returns {Bus} + */ Node.prototype.openBus = function() { return new Bus({node: this}); }; +/** + * Will get an array of API method descriptions from all of the available services. + * @returns {Array} + */ Node.prototype.getAllAPIMethods = function() { var methods = []; for(var i in this.services) { @@ -78,6 +110,10 @@ Node.prototype.getAllAPIMethods = function() { return methods; }; +/** + * Will get an array of events from all of the available services. + * @returns {Array} + */ Node.prototype.getAllPublishEvents = function() { var events = []; for (var i in this.services) { @@ -87,6 +123,11 @@ Node.prototype.getAllPublishEvents = function() { return events; }; +/** + * Will organize services into the order that they should be started + * based on the service's dependencies. + * @returns {Array} + */ Node.prototype.getServiceOrder = function() { var services = this._unloadedServices; @@ -127,6 +168,17 @@ Node.prototype.getServiceOrder = function() { return stack; }; +/** + * Will instantiate an instance of the service module, add it to the node + * services, start the service and add available API methods to the node and + * checking for any conflicts. + * @param {Object} serviceInfo + * @param {String} serviceInfo.name - The name of the service + * @param {Object} serviceInfo.module - The service module constructor + * @param {Object} serviceInfo.config - Options to pass into the constructor + * @param {Function} callback - Called when the service is started + * @private + */ Node.prototype._startService = function(serviceInfo, callback) { var self = this; @@ -176,6 +228,10 @@ Node.prototype._startService = function(serviceInfo, callback) { }; +/** + * Will start all running services in the order based on the dependency chain. + * @param {Function} callback - Called when all services are started + */ Node.prototype.start = function(callback) { var self = this; var servicesOrder = this.getServiceOrder(); @@ -195,6 +251,11 @@ Node.prototype.start = function(callback) { ); }; +/** + * Will stop all running services in the reverse order that they + * were initially started. + * @param {Function} callback - Called when all services are stopped + */ Node.prototype.stop = function(callback) { log.info('Beginning shutdown'); var self = this; diff --git a/lib/services/address/history.js b/lib/services/address/history.js index 8ab47338..751d6cd4 100644 --- a/lib/services/address/history.js +++ b/lib/services/address/history.js @@ -8,8 +8,7 @@ var _ = bitcore.deps._; * This represents an instance that keeps track of data over a series of * asynchronous I/O calls to get the transaction history for a group of * addresses. History can be queried by start and end block heights to limit large sets - * of results (uses leveldb key streaming). See AddressService.prototype.getAddressHistory - * for complete documentation about options. + * of results (uses leveldb key streaming). */ function AddressHistory(args) { this.node = args.node; @@ -27,13 +26,15 @@ function AddressHistory(args) { AddressHistory.MAX_ADDRESS_QUERIES = 20; +/** + * This function will give detailed history for the configured + * addresses. See AddressService.prototype.getAddressHistory + * for complete documentation about options and response format. + */ AddressHistory.prototype.get = function(callback) { var self = this; var totalCount; - // TODO check for mempool inputs and outputs by a group of addresses, currently - // each address individually loops through the mempool and does not check input scripts. - async.eachLimit( self.addresses, AddressHistory.MAX_ADDRESS_QUERIES, @@ -68,6 +69,12 @@ AddressHistory.prototype.get = function(callback) { ); }; +/** + * This function will retrieve input and output information for an address + * and set the property `this.transactionInfo`. + * @param {String} address - A base58check encoded address + * @param {Function} next + */ AddressHistory.prototype.getTransactionInfo = function(address, next) { var self = this; @@ -109,9 +116,8 @@ AddressHistory.prototype.getTransactionInfo = function(address, next) { }; /** - * This function combines results from getInputs and getOutputs by - * combining inputIndexes and outputIndexes for address transaction - * matching combinations. + * This function combines results from getInputs and getOutputs at + * `this.transactionInfo` to be "txid" unique at `this.combinedArray`. */ AddressHistory.prototype.combineTransactionInfo = function() { var combinedArrayMap = {}; @@ -154,6 +160,9 @@ AddressHistory.prototype.combineTransactionInfo = function() { } }; +/** + * A helper function to sort and slice/paginate the `combinedArray` + */ AddressHistory.prototype.sortAndPaginateCombinedArray = function() { this.combinedArray.sort(AddressHistory.sortByHeight); if (!_.isUndefined(this.options.from) && !_.isUndefined(this.options.to)) { @@ -161,6 +170,12 @@ AddressHistory.prototype.sortAndPaginateCombinedArray = function() { } }; +/** + * A helper sort function to order by height and then by date + * for transactions that are in the mempool. + * @param {Object} a - An item from the `combinedArray` + * @param {Object} b + */ AddressHistory.sortByHeight = function(a, b) { if (a.height < 0 && b.height < 0) { // Both are from the mempool, compare timestamps @@ -184,6 +199,12 @@ AddressHistory.sortByHeight = function(a, b) { } }; +/** + * This function will transform items from the combinedArray into + * the detailedArray with the full transaction, satoshis and confirmation. + * @param {Object} txInfo - An item from the `combinedArray` + * @param {Function} next + */ AddressHistory.prototype.getDetailedInfo = function(txInfo, next) { var self = this; var queryMempool = _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool; @@ -218,6 +239,10 @@ AddressHistory.prototype.getDetailedInfo = function(txInfo, next) { ); }; +/** + * A helper function for `getDetailedInfo` for getting the confirmations. + * @param {Transaction} transaction - A transaction with a populated __height value. + */ AddressHistory.prototype.getConfirmationsDetail = function(transaction) { var confirmations = 0; if (transaction.__height >= 0) { @@ -226,6 +251,11 @@ AddressHistory.prototype.getConfirmationsDetail = function(transaction) { return confirmations; }; +/** + * A helper function for `getDetailedInfo` for getting the satoshis. + * @param {Transaction} transaction - A transaction populated with previous outputs + * @param {Object} txInfo - An item from `combinedArray` + */ AddressHistory.prototype.getSatoshisDetail = function(transaction, txInfo) { var satoshis = txInfo.satoshis || 0; diff --git a/lib/services/address/index.js b/lib/services/address/index.js index 4974cfb2..3ecceb2c 100644 --- a/lib/services/address/index.js +++ b/lib/services/address/index.js @@ -17,6 +17,14 @@ var PublicKey = bitcore.PublicKey; var Address = bitcore.Address; var AddressHistory = require('./history'); +/** + * The Address Service builds upon the Database Service and the Bitcoin Service to add additional + * functionality for getting information by base58check encoded addresses. This includes getting the + * balance for an address, the history for a collection of addresses, and unspent outputs for + * contstructing transactions. This is typically the core functionality for building a wallet. + * @param {Node} node - An instance of the node + * @param {String} [name] - An optional name of the service + */ var AddressService = function(options) { BaseService.call(this, options); @@ -47,6 +55,10 @@ AddressService.PREFIXES = { AddressService.SPACER_MIN = new Buffer('00', 'hex'); AddressService.SPACER_MAX = new Buffer('ff', 'hex'); +/** + * Called by the Node to get the available API methods for this service, + * that can be exposed over the JSON-RPC interface. + */ AddressService.prototype.getAPIMethods = function() { return [ ['getBalance', this, this.getBalance, 2], @@ -58,6 +70,9 @@ AddressService.prototype.getAPIMethods = function() { ]; }; +/** + * Called by the Bus to get the available events for this service. + */ AddressService.prototype.getPublishEvents = function() { return [ { @@ -114,8 +129,8 @@ AddressService.prototype.transactionOutputHandler = function(messages, tx, outpu /** * This will handle data from the daemon "tx" event, go through each of the outputs - * and send messages to any subscribers for a particular address. - * + * and send messages by calling `transactionEventHandler` to any subscribers for a + * particular address. * @param {Object} txInfo - The data from the daemon.on('tx') event * @param {Buffer} txInfo.buffer - The transaction buffer * @param {Boolean} txInfo.mempool - If the transaction was accepted in the mempool @@ -144,7 +159,27 @@ AddressService.prototype.transactionHandler = function(txInfo) { } }; +/** + * This function will update the mempool address index with the necessary + * information for further lookups. There are three indexes: + * + * mempoolOutputIndex, an object keyed by base58check encoded addresses with values: + * txid - A hex string of the transaction hash + * outputIndex - A number of the corresponding output + * satoshis - Total number of satoshis + * script - The script as a hex string + * + * mempoolInputIndex, an object keyed by base58check encoded addreses with values: + * txid - A hex string of the transaction hash + * inputIndex - A number of the corresponding input + * + * mempoolSpentIndex, an object keyed by - + * + * @param {Transaction} - An instance of a Bitcore Transaction + */ AddressService.prototype.updateMempoolIndex = function(tx) { + /* jshint maxstatements: 30 */ + var outputLength = tx.outputs.length; for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) { var output = tx.outputs[outputIndex]; @@ -199,6 +234,12 @@ AddressService.prototype.updateMempoolIndex = function(tx) { }; +/** + * This function is called by the Database Service when the database itself + * has finished synchronization. It will retrieve a copy of all transactions + * from the mempool and create an address index for fast look-ups. The previous + * index will be reset. + */ AddressService.prototype.resetMempoolIndex = function(callback) { var self = this; var transactionBuffers = self.node.services.bitcoind.getMempoolTransactions(); @@ -217,6 +258,12 @@ AddressService.prototype.resetMempoolIndex = function(callback) { }); }; +/** + * This function is optimized to return address information about an output script + * without constructing a Bitcore Address instance. + * @param {Script} - An instance of a Bitcore Script + * @private + */ AddressService.prototype._extractAddressInfoFromScript = function(script) { var hashBuffer; var addressType; @@ -242,6 +289,13 @@ AddressService.prototype._extractAddressInfoFromScript = function(script) { }; }; +/** + * The Database Service will run this function when blocks are connected and + * disconnected to the chain during syncing and reorganizations. + * @param {Block} block - An instance of a Bitcore Block + * @param {Boolean} addOutput - If the block is being removed or added to the chain + * @param {Function} callback + */ AddressService.prototype.blockHandler = function(block, addOutput, callback) { var txs = block.transactions; var height = block.__height; @@ -450,6 +504,8 @@ AddressService.prototype._decodeInputValue = function(buffer) { }; /** + * This function is responsible for emitting events to any subscribers to the + * `address/transaction` event. * @param {Object} obj * @param {Transaction} obj.tx - The transaction * @param {Object} obj.addressInfo @@ -485,6 +541,8 @@ AddressService.prototype.transactionEventHandler = function(obj) { }; /** + * The function is responsible for emitting events to any subscribers for the + * `address/balance` event. * @param {Block} block * @param {Object} obj * @param {String} obj.hashHex @@ -510,6 +568,14 @@ AddressService.prototype.balanceEventHandler = function(block, obj) { } }; +/** + * The Bus will use this function to subscribe to the available + * events for this service. For information about the available events + * please see `getPublishEvents`. + * @param {String} name - The name of the event + * @param {EventEmitter} emitter - An event emitter instance + * @param {Array} addresses - An array of addresses to subscribe + */ AddressService.prototype.subscribe = function(name, emitter, addresses) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); $.checkArgument(Array.isArray(addresses), 'Second argument is expected to be an Array of addresses'); @@ -523,6 +589,13 @@ AddressService.prototype.subscribe = function(name, emitter, addresses) { } }; +/** + * The Bus will use this function to unsubscribe to the available + * events for this service. + * @param {String} name - The name of the event + * @param {EventEmitter} emitter - An event emitter instance + * @param {Array} addresses - An array of addresses to subscribe + */ AddressService.prototype.unsubscribe = function(name, emitter, addresses) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); $.checkArgument(Array.isArray(addresses) || _.isUndefined(addresses), 'Second argument is expected to be an Array of addresses or undefined'); @@ -543,6 +616,11 @@ AddressService.prototype.unsubscribe = function(name, emitter, addresses) { } }; +/** + * A helper function for the `unsubscribe` method to unsubscribe from all addresses. + * @param {String} name - The name of the event + * @param {EventEmitter} emiter - An instance of an event emitter + */ AddressService.prototype.unsubscribeAll = function(name, emitter) { $.checkArgument(emitter instanceof EventEmitter, 'First argument is expected to be an EventEmitter'); @@ -555,6 +633,13 @@ AddressService.prototype.unsubscribeAll = function(name, emitter) { } }; +/** + * Will sum the total of all unspent outputs to calculate the balance + * for an address. + * @param {String} address - The base58check encoded address + * @param {Boolean} queryMempool - Include mempool in the results + * @param {Function} callback + */ AddressService.prototype.getBalance = function(address, queryMempool, callback) { this.getUnspentOutputs(address, queryMempool, function(err, outputs) { if(err) { @@ -574,6 +659,13 @@ AddressService.prototype.getBalance = function(address, queryMempool, callback) }; /** + * Will give inputs that spend previous outputs for an address as an object with: + * address - The base58check encoded address + * txid - A string of the transaction hash + * outputIndex - A number of corresponding transaction input + * height - The height of the block the transaction was included, will be -1 for mempool transactions + * confirmations - The number of confirmations, will equal 0 for mempool transactions + * * @param {String} addressStr - The relevant address * @param {Object} options - Additional options for query the outputs * @param {Number} [options.start] - The relevant start block height @@ -677,6 +769,15 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { }; /** + * Will give outputs for an address as an object with: + * address - The base58check encoded address + * txid - A string of the transaction hash + * outputIndex - A number of corresponding transaction output + * height - The height of the block the transaction was included, will be -1 for mempool transactions + * satoshis - The satoshis value of the output + * script - The script of the output as a hex string + * confirmations - The number of confirmations, will equal 0 for mempool transactions + * * @param {String} addressStr - The relevant address * @param {Object} options - Additional options for query the outputs * @param {Number} [options.start] - The relevant start block height @@ -779,6 +880,12 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { }; +/** + * Will give unspent outputs for an address or an array of addresses. + * @param {Array|String} addresses - An array of addresses + * @param {Boolean} queryMempool - Include or exclude the mempool + * @param {Function} callback + */ AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) { var self = this; @@ -804,6 +911,12 @@ AddressService.prototype.getUnspentOutputs = function(addresses, queryMempool, c }); }; +/** + * Will give unspent outputs for an address. + * @param {String} address - An address in base58check encoding + * @param {Boolean} queryMempool - Include or exclude the mempool + * @param {Function} callback + */ AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var self = this; @@ -816,7 +929,7 @@ AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMe } var isUnspent = function(output, callback) { - self.isUnspent(output, queryMempool, callback); + self.isUnspent(output, callback); }; async.filter(outputs, isUnspent, function(results) { @@ -825,13 +938,23 @@ AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMe }); }; -AddressService.prototype.isUnspent = function(output, queryMempool, callback) { - this.isSpent(output, queryMempool, function(spent) { +/** + * Will give the inverse of isSpent + * @param {Object} output + * @param {Function} callback + */ +AddressService.prototype.isUnspent = function(output, callback) { + this.isSpent(output, function(spent) { callback(!spent); }); }; -AddressService.prototype.isSpent = function(output, queryMempool, callback) { +/** + * Will determine if an output is spent, results do not include the mempool. + * @param {Object} output - An output as returned from getOutputs + * @param {Function} callback + */ +AddressService.prototype.isSpent = function(output, callback) { var self = this; var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; @@ -883,18 +1006,18 @@ AddressService.prototype.getAddressHistory = function(addresses, options, callba }; /** - * This will return an object with: + * This will give an object with: * balance - confirmed balance * unconfirmedBalance - unconfirmed balance * totalReceived - satoshis received * totalSpent - satoshis spent - * appearances - number of times used in confirmed transactions - * unconfirmedAppearances - number of times used in unconfirmed transactions + * appearances - number of transactions + * unconfirmedAppearances - number of unconfirmed transactions * txids - list of txids (unless noTxList is set) * * @param {String} address * @param {Object} options - * @param {Boolean} options.noTxList - if set, txid array will not be included + * @param {Boolean} [options.noTxList] - if set, txid array will not be included * @param {Function} callback */ AddressService.prototype.getAddressSummary = function(address, options, callback) { diff --git a/lib/services/bitcoind.js b/lib/services/bitcoind.js index d3d0e248..1d08addb 100644 --- a/lib/services/bitcoind.js +++ b/lib/services/bitcoind.js @@ -4,18 +4,16 @@ var fs = require('fs'); var util = require('util'); var bindings = require('bindings')('bitcoind.node'); var mkdirp = require('mkdirp'); -var async = require('async'); var bitcore = require('bitcore'); -var Transaction = require('../transaction'); var $ = bitcore.util.preconditions; var index = require('../'); var log = index.log; var Service = require('../service'); /** - * Provides an interface to native bindings to Bitcoin Core + * Provides an interface to native bindings to Bitcoin Core compiled as a static library. + * The C++ bindings can be found at src/libbitcoind.cc * @param {Object} options - * @param {String} options.datadir - The bitcoin data directory * @param {Node} options.node - A reference to the node */ function Bitcoin(options) { @@ -140,6 +138,10 @@ Bitcoin.prototype._onReady = function(result, callback) { }; +/** + * Called by Node to start the service + * @param {Function} callback + */ Bitcoin.prototype.start = function(callback) { var self = this; @@ -176,70 +178,183 @@ Bitcoin.prototype.start = function(callback) { }); }; +/** + * Helper to determine the state of the database. + * @returns {Boolean} If the database is fully synced + */ Bitcoin.prototype.isSynced = function() { return bindings.isSynced(); }; +/** + * Helper to determine the progress of the database. + * @returns {Number} An estimated percentage of the syncronization status + */ Bitcoin.prototype.syncPercentage = function() { return bindings.syncPercentage(); }; -Bitcoin.prototype.getBlock = function(blockhash, callback) { - return bindings.getBlock(blockhash, callback); +/** + * Will retreive a block as a Node.js Buffer from disk. + * @param {String|Number} block - A block hash or block height number + */ +Bitcoin.prototype.getBlock = function(block, callback) { + return bindings.getBlock(block, callback); }; +/** + * Will return the spent status of an output (not including the mempool) + * @param {String} txid - The transaction hash + * @param {Number} outputIndex - The output index in the transaction + * @returns {Boolean} If the output has been spent + */ Bitcoin.prototype.isSpent = function(txid, outputIndex) { return bindings.isSpent(txid, outputIndex); }; -Bitcoin.prototype.getBlockIndex = function(blockHash) { - return bindings.getBlockIndex(blockHash); +/** + * Will return the block index information, the output will have the format: + * { + * prevHash: '7194fcf33f58c96720f88f21ab28c34ebc5638c5f88d7838517deb27313b59de', + * hash: '7c5caf0af1bf16e3467b275a3b408bc1d251bff3c25be20cb727c47b66a7b216', + * chainWork: '0000000000000000000000000000000000000000000000000000000000000016', + * height: 10 + * } + * @param {String|Number} block - A block hash or block height + * @returns {Object} + */ +Bitcoin.prototype.getBlockIndex = function(block) { + return bindings.getBlockIndex(block); }; +/** + * Will return if the block is a part of the main chain. + * @param {String} blockHash + * @returns {Boolean} + */ Bitcoin.prototype.isMainChain = function(blockHash) { return bindings.isMainChain(blockHash); }; +/** + * Will estimate the fee per kilobyte. + * @param {Number} blocks - The number of blocks for the transaction to be confirmed. + * @returns {Number} + */ Bitcoin.prototype.estimateFee = function(blocks) { return bindings.estimateFee(blocks); }; +/** + * Will add a transaction to the mempool and relay to connected peers, the function + * will throw an error if there were validation problems. + * @param {String} transaction - The hex string of the transaction + * @param {Boolean} allowAbsurdFees - Enable large fees + */ Bitcoin.prototype.sendTransaction = function(transaction, allowAbsurdFees) { return bindings.sendTransaction(transaction, allowAbsurdFees); }; +/** + * Will get a transaction as a Node.js Buffer from disk and the mempool. + * @param {String} txid - The transaction hash + * @param {Boolean} queryMempool - Include the mempool + * @param {Function} callback + */ Bitcoin.prototype.getTransaction = function(txid, queryMempool, callback) { return bindings.getTransaction(txid, queryMempool, callback); }; +/** + * Will get a transation with additional information about the block, in the format: + * { + * blockHash: '2725743288feae6bdaa976590af7cb12d7b535b5a242787de6d2789c73682ed1', + * height: 48, + * timestamp: 1442951110, // in seconds + * buffer: // transaction buffer + * } + * @param {String} txid - The transaction hash + * @param {Boolean} queryMempool - Include the mempool + * @param {Function} callback + */ Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { return bindings.getTransactionWithBlockInfo(txid, queryMempool, callback); }; +/** + * Will return the entire mempool as an Array of transaction Buffers. + * @returns {Array} + */ Bitcoin.prototype.getMempoolTransactions = function() { return bindings.getMempoolTransactions(); }; -Bitcoin.prototype.addMempoolUncheckedTransaction = function(txBuffer) { - return bindings.addMempoolUncheckedTransaction(txBuffer); +/** + * Will add a transaction to the mempool without any validation. This is used + * exclusively for testing purposes. + * @param {String} transaction - The hex string for the transaction + */ +Bitcoin.prototype.addMempoolUncheckedTransaction = function(transaction) { + return bindings.addMempoolUncheckedTransaction(transaction); }; +/** + * Will get the best block hash for the chain. + * @returns {String} + */ Bitcoin.prototype.getBestBlockHash = function() { return bindings.getBestBlockHash(); }; +/** + * Will get the next block hash for a block hash. + * @param {String} hash - The starting block hash + * @returns {String} + */ Bitcoin.prototype.getNextBlockHash = function(hash) { return bindings.getNextBlockHash(hash); }; +/** + * This query is expensive and can take quite a while to return. It will give + * statistics about the database, in the format: + * { + * height: 151, + * bestblock: '085241174bf5c9d606e591b3cbf52f9306f3d6aca50ea77604ca898472dc4669', + * transactions: 151, + * txouts: 151, + * bytes_serialized: 10431, + * hash_serialized: 'b1e42afb98d0d9f1978c048648685590a7ef0be46f83f509fc0d0668d6a39d8c', + * total_amount: 750000000000 + * } + * @returns {Object} + */ Bitcoin.prototype.getTxOutSetInfo = function() { return bindings.getTxOutSetInfo(); }; +/** + * This will return information about the database in the format: + * { + * version: 110000, + * protocolversion: 70002, + * blocks: 151, + * timeoffset: 0, + * connections: 0, + * difficulty: 4.6565423739069247e-10, + * testnet: false, + * relayfee: 1000, + * errors: '' + * } + */ Bitcoin.prototype.getInfo = function() { return bindings.getInfo(); }; +/** + * Called by Node to stop the service. + * @param {Function} callback + */ Bitcoin.prototype.stop = function(callback) { return bindings.stop(function(err, status) { if (err) { diff --git a/lib/services/db.js b/lib/services/db.js index ef22ac04..38e7340b 100644 --- a/lib/services/db.js +++ b/lib/services/db.js @@ -18,12 +18,13 @@ var Transaction = require('../transaction'); var Service = require('../service'); /** - * Represents the current state of the bitcoin blockchain. Other services - * can extend the data that is indexed by implementing a `blockHandler` method. + * This service synchronizes a leveldb database with bitcoin block chain by connecting and + * disconnecting blocks to build new indexes that can be queried. Other services can extend + * the data that is indexed by implementing a `blockHandler` method. * * @param {Object} options - * @param {String} options.datadir - The bitcoin data directory * @param {Node} options.node - A reference to the node + * @param {Node} options.store - A levelup backend store */ function DB(options) { /* jshint maxstatements: 20 */ @@ -64,6 +65,10 @@ DB.PREFIXES = { BLOCKS: new Buffer('01', 'hex') }; +/** + * This function will set `this.dataPath` based on `this.node.network`. + * @private + */ DB.prototype._setDataPath = function() { $.checkState(this.node.datadir, 'Node is expected to have a "datadir" property'); var regtest = Networks.get('regtest'); @@ -78,6 +83,10 @@ DB.prototype._setDataPath = function() { } }; +/** + * Called by Node to start the service. + * @param {Function} callback + */ DB.prototype.start = function(callback) { var self = this; @@ -140,9 +149,12 @@ DB.prototype.start = function(callback) { }); } }); - }; +/** + * Called by Node to stop the service + * @param {Function} callback + */ DB.prototype.stop = function(callback) { var self = this; @@ -156,6 +168,10 @@ DB.prototype.stop = function(callback) { }); }; +/** + * Will give information about the database from bitcoin. + * @param {Function} callback + */ DB.prototype.getInfo = function(callback) { var self = this; setImmediate(function() { @@ -166,22 +182,32 @@ DB.prototype.getInfo = function(callback) { /** * Closes the underlying store database - * @param {Function} callback - A function that accepts: Error + * @param {Function} callback */ DB.prototype.close = function(callback) { this.store.close(callback); }; +/** + * This function is responsible for emitting `db/transaction` events. + * @param {Object} txInfo - The data from the bitcoind.on('tx') event + * @param {Buffer} txInfo.buffer - The transaction buffer + * @param {Boolean} txInfo.mempool - If the transaction was accepted in the mempool + * @param {String} txInfo.hash - The hash of the transaction + */ DB.prototype.transactionHandler = function(txInfo) { var tx = Transaction().fromBuffer(txInfo.buffer); for (var i = 0; i < this.subscriptions.transaction.length; i++) { - this.subscriptions.transaction[i].emit('transaction', { + this.subscriptions.transaction[i].emit('db/transaction', { rejected: !txInfo.mempool, tx: tx }); } }; +/** + * Called by Node to determine the available API methods. + */ DB.prototype.getAPIMethods = function() { var methods = [ ['getBlock', this, this.getBlock, 1], @@ -194,17 +220,21 @@ DB.prototype.getAPIMethods = function() { return methods; }; +/** + * Will get a block from bitcoind and give a Bitcore Block + * @param {String|Number} hash - A block hash or block height + */ DB.prototype.getBlock = function(hash, callback) { - this.node.services.bitcoind.getBlock(hash, function(err, blockData) { + this.node.services.bitcoind.getBlock(hash, function(err, blockBuffer) { if (err) { return callback(err); } - callback(null, Block.fromBuffer(blockData)); + callback(null, Block.fromBuffer(blockBuffer)); }); }; /** - * get block hashes between two timestamps + * Get block hashes between two timestamps * @param {Number} high - high timestamp, in seconds, inclusive * @param {Number} low - low timestamp, in seconds, inclusive * @param {Function} callback @@ -212,10 +242,12 @@ DB.prototype.getBlock = function(hash, callback) { DB.prototype.getBlockHashesByTimestamp = function(high, low, callback) { var self = this; var hashes = []; + var lowKey; + var highKey; try { - var lowKey = this._encodeBlockIndexKey(low); - var highKey = this._encodeBlockIndexKey(high); + lowKey = this._encodeBlockIndexKey(low); + highKey = this._encodeBlockIndexKey(high); } catch(e) { return callback(e); } @@ -244,13 +276,18 @@ DB.prototype.getBlockHashesByTimestamp = function(high, low, callback) { if (error) { return callback(error); } - callback(null, hashes); }); return stream; }; +/** + * Will give a Bitcore Transaction from bitcoind by txid + * @param {String} txid - A transaction hash + * @param {Boolean} queryMempool - Include the mempool + * @param {Function} callback + */ DB.prototype.getTransaction = function(txid, queryMempool, callback) { this.node.services.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) { if (err) { @@ -264,6 +301,12 @@ DB.prototype.getTransaction = function(txid, queryMempool, callback) { }); }; +/** + * Will give a Bitcore Transaction and populated information about the block included. + * @param {String} txid - A transaction hash + * @param {Boolean} queryMempool - Include the mempool + * @param {Function} callback + */ DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) { this.node.services.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) { if (err) { @@ -279,20 +322,34 @@ DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback }); }; +/** + * Will send a transaction to the Bitcoin network. + * @param {Transaction} tx - An instance of a Bitcore Transaction + * @param {Function} callback + */ DB.prototype.sendTransaction = function(tx, callback) { + var txString; if (tx instanceof Transaction) { - tx = tx.toString(); + txString = tx.serialize(); + } else { + txString = tx; } - $.checkArgument(typeof tx === 'string', 'Argument must be a hex string or Transaction'); + $.checkArgument(typeof txString === 'string', 'Argument must be a hex string or Transaction'); try { - var txid = this.node.services.bitcoind.sendTransaction(tx); + var txid = this.node.services.bitcoind.sendTransaction(txString); return callback(null, txid); } catch(err) { return callback(err); } }; +/** + * Will estimate fees for a transaction and give a result in + * satoshis per kilobyte. Similar to the bitcoind estimateFee method. + * @param {Number} blocks - The number of blocks for the transaction to be included. + * @param {Function} callback + */ DB.prototype.estimateFee = function(blocks, callback) { var self = this; setImmediate(function() { @@ -300,6 +357,9 @@ DB.prototype.estimateFee = function(blocks, callback) { }); }; +/** + * Called by the Bus to determine the available events. + */ DB.prototype.getPublishEvents = function() { return [ { @@ -412,6 +472,10 @@ DB.prototype.disconnectBlock = function(block, callback) { this.runAllBlockHandlers(block, false, callback); }; +/** + * Will run all `resetMempoolIndex` methods implemented on sibling + * services to update the mempool indexes. + */ DB.prototype.runAllMempoolIndexes = function(callback) { async.eachSeries( this.node.services, @@ -427,8 +491,8 @@ DB.prototype.runAllMempoolIndexes = function(callback) { }; /** - * Will collect all database operations for a block from other services - * and save to the database. + * Will collect all database operations for a block from other services that implement + * `blockHandler` methods and then save operations to the database. * @param {Block} block - The bitcore block * @param {Boolean} add - If the block is being added/connected or removed/disconnected * @param {Function} callback @@ -439,7 +503,7 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) { // Notify block subscribers for (var i = 0; i < this.subscriptions.block.length; i++) { - this.subscriptions.block[i].emit('block', block.hash); + this.subscriptions.block[i].emit('db/block', block.hash); } // Update block index @@ -497,7 +561,7 @@ DB.prototype._decodeBlockIndexValue = function(value) { /** * 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. + * by moving backwards on both chains until there is a meeting point. * @param {Block} block - The new tip that forks the current chain. * @param {Function} done - A callback function that is called when complete. */ diff --git a/lib/services/web.js b/lib/services/web.js index 28372953..c15e82f5 100644 --- a/lib/services/web.js +++ b/lib/services/web.js @@ -11,6 +11,20 @@ var index = require('../'); var log = index.log; var fs = require('fs'); +/** + * This service respresents a hub for combining several services over a single HTTP port. Services + * can extend routes by implementing the methods `getRoutePrefix` and `setupRoutes`. Additionally + * events that are exposed via the `getPublishEvents` and API methods exposed via `getAPIMethods` + * will be available over a socket.io connection. + * + * @param {Object} options + * @param {Node} options.node - A reference to the node + * @param {Boolean} options.https - Enable https, will default to node.https settings. + * @param {Object} options.httpsOptions - Options passed into https.createServer, defaults to node settings. + * @param {String} options.httpsOptions.key - Path to key file + * @param {String} options.httpsOptions.cert - Path to cert file + * @param {Number} options.port - The port for the service, defaults to node settings. + */ var WebService = function(options) { var self = this; this.node = options.node; @@ -30,8 +44,11 @@ inherits(WebService, BaseService); WebService.dependencies = []; +/** + * Called by Node to start the service + * @param {Function} callback + */ WebService.prototype.start = function(callback) { - var self = this; this.app = express(); this.app.use(bodyParser.json()); @@ -48,6 +65,10 @@ WebService.prototype.start = function(callback) { setImmediate(callback); }; +/** + * Called by Node. stop the service + * @param {Function} callback + */ WebService.prototype.stop = function(callback) { var self = this; @@ -55,11 +76,14 @@ WebService.prototype.stop = function(callback) { if(self.server) { self.server.close(); } - callback(); }); }; +/** + * This function will iterate over all of the available services gathering + * all of the exposed HTTP routes. + */ WebService.prototype.setupAllRoutes = function() { for(var key in this.node.services) { var subApp = new express.Router(); @@ -67,13 +91,17 @@ WebService.prototype.setupAllRoutes = function() { if(service.getRoutePrefix && service.setupRoutes) { this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp); - this.node.services[key].setupRoutes(subApp, express); + this.node.services[key].setupRoutes(subApp, express); } else { log.debug('No routes defined for: ' + key); } } }; +/** + * This function will contruct an API methods map of all of the + * available methods that can be called from enable services. + */ WebService.prototype.createMethodsMap = function() { var self = this; var methods = this.node.getAllAPIMethods(); @@ -93,6 +121,10 @@ WebService.prototype.createMethodsMap = function() { }); }; +/** + * This function will gather all of the available events exposed from + * the enabled services. + */ WebService.prototype.getEventNames = function() { var events = this.node.getAllPublishEvents(); var eventNames = []; @@ -101,9 +133,8 @@ WebService.prototype.getEventNames = function() { if(eventNames.indexOf(name) > -1) { throw new Error('Duplicate event ' + name); } - eventNames.push(name); - }; + } events.forEach(function(event) { addEventName(event.name); @@ -118,9 +149,12 @@ WebService.prototype.getEventNames = function() { return eventNames; }; +/** + * This function is responsible for managing a socket.io connection, including + * instantiating a new Bus, subscribing/unsubscribing and handling RPC commands. + * @param {Socket} socket - A socket.io socket instance + */ WebService.prototype.socketHandler = function(socket) { - var self = this; - var bus = this.node.openBus(); socket.on('message', this.socketMessageHandler.bind(this)); @@ -153,6 +187,12 @@ WebService.prototype.socketHandler = function(socket) { }); }; +/** + * This method will handle incoming RPC messages to a socket.io connection, + * call the appropriate method, and respond with the result. + * @param {Object} message - The socket.io "message" object + * @param {Function} socketCallback + */ WebService.prototype.socketMessageHandler = function(message, socketCallback) { if (this.methodsMap[message.method]) { var params = message.params; @@ -195,6 +235,10 @@ WebService.prototype.socketMessageHandler = function(message, socketCallback) { } }; +/** + * This method will read `key` and `cert` from disk based on `httpsOptions` and + * replace the options with the files. + */ WebService.prototype.transformHttpsOptions = function() { if(!this.httpsOptions || !this.httpsOptions.key || !this.httpsOptions.cert) { throw new Error('Missing https options'); diff --git a/test/services/address/index.unit.js b/test/services/address/index.unit.js index ef3da7bb..c0e17d13 100644 --- a/test/services/address/index.unit.js +++ b/test/services/address/index.unit.js @@ -454,7 +454,7 @@ describe('Address Service', function() { store: { createReadStream: sinon.stub().returns(testStream) } - } + }; var testnode = { services: { db: db, @@ -475,7 +475,7 @@ describe('Address Service', function() { height: -1, confirmations: 0 } - ] + ]; am.getInputs(address, args, function(err, inputs) { should.not.exist(err); inputs.length.should.equal(1); @@ -657,7 +657,7 @@ describe('Address Service', function() { script: '76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac', outputIndex: 0 } - ] + ]; am.getOutputs(address, options, function(err, outputs) { should.not.exist(err); @@ -836,7 +836,7 @@ describe('Address Service', function() { var am = new AddressService({node: mocknode}); am.getOutputs = sinon.stub().callsArgWith(2, null, outputs); - am.isUnspent = function(output, queryMempool, callback) { + am.isUnspent = function(output, callback) { callback(!outputs[i].spent); i++; }; @@ -878,24 +878,24 @@ describe('Address Service', function() { }); it('should give true when isSpent() gives false', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, false); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { + am.isSpent = sinon.stub().callsArgWith(1, false); + am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', function(unspent) { unspent.should.equal(true); done(); }); }); it('should give false when isSpent() gives true', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, true); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { + am.isSpent = sinon.stub().callsArgWith(1, true); + am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', function(unspent) { unspent.should.equal(false); done(); }); }); it('should give false when isSpent() returns an error', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, new Error('error')); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { + am.isSpent = sinon.stub().callsArgWith(1, new Error('error')); + am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', function(unspent) { unspent.should.equal(false); done(); }); @@ -922,7 +922,7 @@ describe('Address Service', function() { }); it('should give true if bitcoind.isSpent gives true', function(done) { - am.isSpent('output', true, function(spent) { + am.isSpent('output', function(spent) { spent.should.equal(true); done(); }); @@ -1001,30 +1001,30 @@ describe('Address Service', function() { }; var inputs = [ { - "txid": "9f183412de12a6c1943fc86c390174c1cde38d709217fdb59dcf540230fa58a6", - "height": -1, - "confirmations": 0, - "addresses": { - "mpkDdnLq26djg17s6cYknjnysAm3QwRzu2": { - "outputIndexes": [], - "inputIndexes": [ + 'txid': '9f183412de12a6c1943fc86c390174c1cde38d709217fdb59dcf540230fa58a6', + 'height': -1, + 'confirmations': 0, + 'addresses': { + 'mpkDdnLq26djg17s6cYknjnysAm3QwRzu2': { + 'outputIndexes': [], + 'inputIndexes': [ 3 ] } }, - "address": "mpkDdnLq26djg17s6cYknjnysAm3QwRzu2" + 'address': 'mpkDdnLq26djg17s6cYknjnysAm3QwRzu2' } ]; var outputs = [ { - "address": "mpkDdnLq26djg17s6cYknjnysAm3QwRzu2", - "txid": "689e9f543fa4aa5b2daa3b5bb65f9a00ad5aa1a2e9e1fc4e11061d85f2aa9bc5", - "outputIndex": 0, - "height": 556351, - "satoshis": 3487110, - "script": "76a914653b58493c2208481e0902a8ffb97b8112b13fe188ac", - "confirmations": 13190 + 'address': 'mpkDdnLq26djg17s6cYknjnysAm3QwRzu2', + 'txid': '689e9f543fa4aa5b2daa3b5bb65f9a00ad5aa1a2e9e1fc4e11061d85f2aa9bc5', + 'outputIndex': 0, + 'height': 556351, + 'satoshis': 3487110, + 'script': '76a914653b58493c2208481e0902a8ffb97b8112b13fe188ac', + 'confirmations': 13190 } ]; diff --git a/test/services/db.unit.js b/test/services/db.unit.js index 10631e9e..b4b296bd 100644 --- a/test/services/db.unit.js +++ b/test/services/db.unit.js @@ -513,6 +513,21 @@ describe('DB Service', function() { }); describe('#sendTransaction', function() { + it('should handle a basic serialized transaction hex string', function(done) { + var db = new DB(baseConfig); + db.node = {}; + db.node.services = {}; + db.node.services.bitcoind = { + sendTransaction: sinon.stub().returns('txid') + }; + + var tx = 'hexstring'; + db.sendTransaction(tx, function(err, txid) { + should.not.exist(err); + txid.should.equal('txid'); + done(); + }); + }); it('should give the txid on success', function(done) { var db = new DB(baseConfig); db.node = {}; @@ -522,8 +537,10 @@ describe('DB Service', function() { }; var tx = new Transaction(); + tx.serialize = sinon.stub().returns('txstring'); db.sendTransaction(tx, function(err, txid) { should.not.exist(err); + tx.serialize.callCount.should.equal(1); txid.should.equal('txid'); done(); }); @@ -537,7 +554,9 @@ describe('DB Service', function() { }; var tx = new Transaction(); + tx.serialize = sinon.stub().returns('txstring'); db.sendTransaction(tx, function(err, txid) { + tx.serialize.callCount.should.equal(1); should.exist(err); done(); });