From 890b38744d314c5d82d1779c6b2fb8595c9070f4 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Wed, 13 Apr 2016 11:13:44 -0400 Subject: [PATCH] test: update unit tests, refactoring and cleanup --- .travis.yml | 8 +- lib/errors.js | 17 +- lib/scaffold/default-base-config.js | 1 + lib/services/bitcoind.js | 201 +- lib/transaction.js | 28 +- package.json | 1 + scripts/regtest | 8 + test/bin/get-tarball-name.js | 18 - test/data/bitcoin.conf | 14 +- test/data/default.bitcoin.conf | 11 + test/index.unit.js | 19 - .../default-base-config.integration.js | 21 +- test/scaffold/default-config.integration.js | 44 +- test/scaffold/start.integration.js | 16 +- test/scaffold/start.unit.js | 104 - test/services/address/encoding.unit.js | 103 - test/services/address/history.unit.js | 544 ---- test/services/address/index.unit.js | 2676 ----------------- test/services/bitcoind.unit.js | 2445 +++++++++++++-- test/services/db.unit.js | 1038 ------- test/transaction.unit.js | 90 +- 21 files changed, 2405 insertions(+), 5002 deletions(-) create mode 100755 scripts/regtest delete mode 100644 test/bin/get-tarball-name.js create mode 100644 test/data/default.bitcoin.conf delete mode 100644 test/index.unit.js delete mode 100644 test/services/address/encoding.unit.js delete mode 100644 test/services/address/history.unit.js delete mode 100644 test/services/address/index.unit.js delete mode 100644 test/services/db.unit.js diff --git a/.travis.yml b/.travis.yml index 06a1ff97..8e1fcec0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,5 @@ node_js: - "v0.12.7" - "v4" script: - - _mocha -R spec regtest/p2p.js - - _mocha -R spec regtest/bitcoind.js - - _mocha -R spec regtest/cluster.js - - _mocha -R spec regtest/node.js - - _mocha -R spec --recursive - + - npm run regtest + - npm run test diff --git a/lib/errors.js b/lib/errors.js index 0a1b6a42..c534d0a2 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -3,23 +3,10 @@ var createError = require('errno').create; var BitcoreNodeError = createError('BitcoreNodeError'); -var NoOutputs = createError('NoOutputs', BitcoreNodeError); -var NoOutput = createError('NoOutput', BitcoreNodeError); -var Wallet = createError('WalletError', BitcoreNodeError); -Wallet.InsufficientFunds = createError('InsufficientFunds', Wallet); - -var Consensus = createError('Consensus', BitcoreNodeError); -Consensus.BlockExists = createError('BlockExists', Consensus); - -var Transaction = createError('Transaction', BitcoreNodeError); -Transaction.NotFound = createError('NotFound', Transaction); +var RPCError = createError('RPCError', BitcoreNodeError); module.exports = { Error: BitcoreNodeError, - NoOutputs: NoOutputs, - NoOutput: NoOutput, - Wallet: Wallet, - Consensus: Consensus, - Transaction: Transaction + RPCError: RPCError }; diff --git a/lib/scaffold/default-base-config.js b/lib/scaffold/default-base-config.js index fc5f8c6d..1f584b55 100644 --- a/lib/scaffold/default-base-config.js +++ b/lib/scaffold/default-base-config.js @@ -7,6 +7,7 @@ var path = require('path'); * or default locations. * @param {Object} options * @param {String} options.network - "testnet" or "livenet" + * @param {String} options.datadir - Absolute path to bitcoin database directory */ function getDefaultBaseConfig(options) { if (!options) { diff --git a/lib/services/bitcoind.js b/lib/services/bitcoind.js index fdc2a612..fe2d89d4 100644 --- a/lib/services/bitcoind.js +++ b/lib/services/bitcoind.js @@ -14,7 +14,9 @@ var $ = bitcore.util.preconditions; var _ = bitcore.deps._; var index = require('../'); +var errors = index.errors; var log = index.log; +var utils = require('../utils'); var Service = require('../service'); var Transaction = require('../transaction'); @@ -45,6 +47,12 @@ function Bitcoin(options) { this.subscriptions.transaction = []; this.subscriptions.block = []; + // limits + this.maxAddressesQuery = options.maxAddressesQuery || Bitcoin.DEFAULT_MAX_ADDRESSES_QUERY; + + // try all interval + this.tryAllInterval = options.tryAllInterval || Bitcoin.DEFAULT_TRY_ALL_INTERVAL; + // available bitcoind nodes this._initClients(); } @@ -52,7 +60,21 @@ util.inherits(Bitcoin, Service); Bitcoin.dependencies = []; -Bitcoin.DEFAULT_CONFIG = 'whitelist=127.0.0.1\n' + 'txindex=1\n' + 'addressindex=1\n' + 'server=1\n'; +Bitcoin.DEFAULT_MAX_ADDRESSES_QUERY = 10000; +Bitcoin.DEFAULT_TRY_ALL_INTERVAL = 1000; +Bitcoin.DEFAULT_CONFIG_SETTINGS = { + server: 1, + whitelist: '127.0.0.1', + txindex: 1, + addressindex: 1, + timestampindex: 1, + spentindex: 1, + zmqpubrawtx: 'tcp://127.0.0.1:28332', + zmqpubhashblock: 'tcp://127.0.0.1:28332', + rpcallowip: '127.0.0.1', + rpcuser: 'bitcoin', + rpcpassword: 'local321' +}; Bitcoin.prototype._initCaches = function() { // caches valid until there is a new block @@ -70,8 +92,8 @@ Bitcoin.prototype._initCaches = function() { this.blockHeaderCache = LRU(288); this.zmqKnownTransactions = LRU(50); this.zmqKnownBlocks = LRU(50); - this.zmqLastBlock = 0; - this.zmqUpdateTipTimeout = false; + this.lastTip = 0; + this.lastTipTimeout = false; }; Bitcoin.prototype._initClients = function() { @@ -149,6 +171,15 @@ Bitcoin.prototype.unsubscribe = function(name, emitter) { } }; +Bitcoin.prototype._getDefaultConfig = function() { + var config = ''; + var defaults = Bitcoin.DEFAULT_CONFIG_SETTINGS; + for(var key in defaults) { + config += key + '=' + defaults[key] + '\n'; + } + return config; +}; + Bitcoin.prototype._loadSpawnConfiguration = function(node) { /* jshint maxstatements: 25 */ @@ -169,6 +200,11 @@ Bitcoin.prototype._loadSpawnConfiguration = function(node) { mkdirp.sync(spawnOptions.datadir); } + if (!fs.existsSync(configPath)) { + var defaultConfig = this._getDefaultConfig(); + fs.writeFileSync(configPath, defaultConfig); + } + var file = fs.readFileSync(configPath); var unparsed = file.toString().split('\n'); for(var i = 0; i < unparsed.length; i++) { @@ -187,6 +223,11 @@ Bitcoin.prototype._loadSpawnConfiguration = function(node) { var spawnConfig = this.spawn.config; + this._checkConfigIndexes(spawnConfig, node); + +}; + +Bitcoin.prototype._checkConfigIndexes = function(spawnConfig, node) { $.checkState( spawnConfig.txindex && spawnConfig.txindex === 1, '"txindex" option is required in order to use transaction query features of bitcore-node. ' + @@ -233,7 +274,6 @@ Bitcoin.prototype._loadSpawnConfiguration = function(node) { 'of bitcore-node services will start.'); node._reindex = true; } - }; Bitcoin.prototype._resetCaches = function() { @@ -245,11 +285,11 @@ Bitcoin.prototype._resetCaches = function() { }; Bitcoin.prototype._tryAll = function(func, callback) { - async.retry({times: this.nodes.length, interval: 1000}, func, callback); + async.retry({times: this.nodes.length, interval: this.tryAllInterval || 1000}, func, callback); }; Bitcoin.prototype._wrapRPCError = function(errObj) { - var err = new Error(errObj.message); + var err = new errors.RPCError(errObj.message); err.code = errObj.code; return err; }; @@ -274,11 +314,11 @@ Bitcoin.prototype._initChain = function(callback) { return callback(self._wrapRPCError(err)); } var blockhash = response.result; - self.getBlock(blockhash, function(err, block) { + self.getRawBlock(blockhash, function(err, blockBuffer) { if (err) { return callback(err); } - self.genesisBuffer = block.toBuffer(); + self.genesisBuffer = blockBuffer; self.emit('ready'); log.info('Bitcoin Daemon Ready'); callback(); @@ -303,55 +343,72 @@ Bitcoin.prototype._getNetworkOption = function() { Bitcoin.prototype._zmqBlockHandler = function(node, message) { var self = this; - function updateChain() { - var hex = message.toString('hex'); - if (hex !== self.tiphash) { - self._resetCaches(); - self.tiphash = message.toString('hex'); - node.client.getBlock(self.tiphash, function(err, response) { - if (err) { - return log.error(self._wrapRPCError(err)); - } - self.height = response.result.height; - $.checkState(self.height >= 0); - self.emit('tip', self.height); - }); - - if(!self.node.stopping) { - self.syncPercentage(function(err, percentage) { - if (err) { - return log.error(err); - } - if (Math.round(percentage) >= 100) { - self.emit('synced', self.height); - } - log.info('Bitcoin Height:', self.height, 'Percentage:', percentage.toFixed(2)); - }); - } - } - } - - // Prevent a rapid succession of tip updates - if (new Date() - self.zmqLastBlock > 1000) { - self.zmqLastBlock = new Date(); - updateChain(); - } else { - clearTimeout(self.zmqUpdateTipTimeout); - self.zmqUpdateTipTimeout = setTimeout(function() { - updateChain(); - }, 1000); - } + // Update the current chain tip + self._rapidProtectedUpdateTip(node, message); // Notify block subscribers var id = message.toString('binary'); - if (!self.zmqKnownBlocks[id]) { - self.zmqKnownBlocks[id] = true; + if (!self.zmqKnownBlocks.get(id)) { + self.zmqKnownBlocks.set(id, true); self.emit('block', message); for (var i = 0; i < this.subscriptions.block.length; i++) { this.subscriptions.block[i].emit('bitcoind/block', message.toString('hex')); } + } +}; + +Bitcoin.prototype._rapidProtectedUpdateTip = function(node, message) { + var self = this; + + // Prevent a rapid succession of tip updates + if (new Date() - self.lastTip > 1000) { + self.lastTip = new Date(); + self._updateTip(node, message); + } else { + clearTimeout(self.lastTipTimeout); + self.lastTipTimeout = setTimeout(function() { + self._updateTip(node, message); + }, 1000); + } +}; + +Bitcoin.prototype._updateTip = function(node, message) { + var self = this; + + var hex = message.toString('hex'); + if (hex !== self.tiphash) { + self.tiphash = message.toString('hex'); + + // reset block valid caches + self._resetCaches(); + + node.client.getBlock(self.tiphash, function(err, response) { + if (err) { + var error = self._wrapRPCError(err); + log.error(error); + self.emit('error', error); + } else { + self.height = response.result.height; + $.checkState(self.height >= 0); + self.emit('tip', self.height); + } + }); + + if(!self.node.stopping) { + self.syncPercentage(function(err, percentage) { + if (err) { + log.error(err); + self.emit('error', err); + } else { + if (Math.round(percentage) >= 100) { + self.emit('synced', self.height); + } + log.info('Bitcoin Height:', self.height, 'Percentage:', percentage.toFixed(2)); + } + }); + } } }; @@ -359,16 +416,15 @@ Bitcoin.prototype._zmqBlockHandler = function(node, message) { Bitcoin.prototype._zmqTransactionHandler = function(node, message) { var self = this; var id = message.toString('binary'); - if (!self.zmqKnownTransactions[id]) { - self.zmqKnownTransactions[id] = true; + if (!self.zmqKnownTransactions.get(id)) { + self.zmqKnownTransactions.set(id, true); self.emit('tx', message); - } - // Notify transaction subscribers - for (var i = 0; i < this.subscriptions.transaction.length; i++) { - this.subscriptions.transaction[i].emit('bitcoind/transaction', message); + // Notify transaction subscribers + for (var i = 0; i < this.subscriptions.transaction.length; i++) { + this.subscriptions.transaction[i].emit('bitcoind/transaction', message.toString('hex')); + } } - }; Bitcoin.prototype._subscribeZmqEvents = function(node) { @@ -414,21 +470,24 @@ Bitcoin.prototype._initZmqSubSocket = function(node, zmqUrl) { Bitcoin.prototype._checkReindex = function(node, callback) { var self = this; + var interval; + function finish(err) { + clearInterval(interval); + callback(err); + } if (node._reindex) { - var interval = setInterval(function() { + interval = setInterval(function() { node.client.syncPercentage(function(err, percentSynced) { if (err) { - return log.error(self._wrapRPCError(err)); + return finish(self._wrapRPCError(err)); } log.info('Bitcoin Core Daemon Reindex Percentage: ' + percentSynced.toFixed(2)); if (Math.round(percentSynced) >= 100) { node._reindex = false; - callback(); - clearInterval(interval); + finish(); } }); }, self._reindexWait); - } else { callback(); } @@ -787,11 +846,15 @@ Bitcoin.prototype.getAddressTxids = function(addressArg, options, callback) { }; Bitcoin.prototype._getConfirmationsDetail = function(transaction) { + $.checkState(this.height > 0, 'current height is unknown'); var confirmations = 0; if (transaction.__height >= 0) { confirmations = this.height - transaction.__height + 1; } - return confirmations; + if (confirmations < 0) { + log.warn('Negative confirmations calculated for transaction:', transaction.hash); + } + return Math.max(0, confirmations); }; Bitcoin.prototype._getAddressDetailsForTransaction = function(transaction, addressStrings) { @@ -904,6 +967,7 @@ Bitcoin.prototype._getAddressStrings = function(addresses) { Bitcoin.prototype._paginateTxids = function(fullTxids, from, to) { var txids; if (from >= 0 && to >= 0) { + $.checkState(from < to, '"from" is expected to be less than "to"'); txids = fullTxids.slice(from, to); } else { txids = fullTxids; @@ -933,7 +997,11 @@ Bitcoin.prototype.getAddressHistory = function(addressArg, options, callback) { } var totalCount = txids.length; - txids = self._paginateTxids(txids, options.from, options.to); + try { + txids = self._paginateTxids(txids, options.from, options.to); + } catch(e) { + return callback(e); + } async.mapSeries( txids, @@ -1304,6 +1372,7 @@ Bitcoin.prototype.getTransaction = function(txid, callback) { * @param {Function} callback */ Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, callback) { + // TODO give response back as standard js object with bitcore tx var self = this; var tx = self.transactionInfoCache.get(txid); if (tx) { @@ -1408,9 +1477,11 @@ Bitcoin.prototype.generateBlock = function(num, callback) { */ Bitcoin.prototype.stop = function(callback) { if (this.spawn && this.spawn.process) { - this.spawn.process.once('exit', function(err, status) { - if (err) { - return callback(err); + this.spawn.process.once('exit', function(code) { + if (code !== 0) { + var error = new Error('bitcoind spawned process exited with status code: ' + code); + error.code = code; + return callback(error); } else { return callback(); } diff --git a/lib/transaction.js b/lib/transaction.js index ef1d7d2a..66042a16 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -4,9 +4,6 @@ var async = require('async'); var bitcore = require('bitcore-lib'); var Transaction = bitcore.Transaction; -var index = require('./'); -var errors = index.errors; - var MAX_TRANSACTION_LIMIT = 5; Transaction.prototype.populateSpentInfo = function(db, options, callback) { @@ -53,11 +50,13 @@ Transaction.prototype.populateInputs = function(db, poolTransactions, callback) Transaction.prototype._populateInput = function(db, input, poolTransactions, callback) { if (!input.prevTxId || !Buffer.isBuffer(input.prevTxId)) { - return callback(new Error('Input is expected to have prevTxId as a buffer')); + return callback(new TypeError('Input is expected to have prevTxId as a buffer')); } var txid = input.prevTxId.toString('hex'); db.getTransaction(txid, function(err, prevTx) { - if(!prevTx) { + if(err) { + return callback(err); + } else if (!prevTx) { // Check the pool for transaction for(var i = 0; i < poolTransactions.length; i++) { if(txid === poolTransactions[i].hash) { @@ -65,25 +64,10 @@ Transaction.prototype._populateInput = function(db, input, poolTransactions, cal return callback(); } } - return callback(new Error('Previous tx ' + input.prevTxId.toString('hex') + ' not found')); - } else if(err) { - callback(err); - } else { - input.output = prevTx.outputs[input.outputIndex]; - callback(); - } - }); -}; - -Transaction.prototype._checkSpent = function(db, input, poolTransactions, callback) { - // TODO check and see if another transaction in the pool spent the output - db.isSpentDB(input, function(spent) { - if(spent) { - return callback(new Error('Input already spent')); - } else { - callback(); } + input.output = prevTx.outputs[input.outputIndex]; + callback(); }); }; diff --git a/package.json b/package.json index a7bc9fdd..4f4848bb 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "scripts": { "install": "./scripts/install", "test": "NODE_ENV=test mocha -R spec --recursive", + "regtest": "./scripts/regtest", "coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive" }, "tags": [ diff --git a/scripts/regtest b/scripts/regtest new file mode 100755 index 00000000..96078911 --- /dev/null +++ b/scripts/regtest @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +_mocha -R spec regtest/p2p.js +_mocha -R spec regtest/bitcoind.js +_mocha -R spec regtest/cluster.js +_mocha -R spec regtest/node.js diff --git a/test/bin/get-tarball-name.js b/test/bin/get-tarball-name.js deleted file mode 100644 index 6f8bdbde..00000000 --- a/test/bin/get-tarball-name.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var path = require('path'); -var getTarballName = require('../../bin/get-tarball-name'); -var execSync = require('child_process').execSync; - -describe('#getTarballName', function() { - it('will return the expected tarball name', function() { - var name = getTarballName(); - var version = require(path.resolve(__dirname + '../../../package.json')).version; - var platform = process.platform; - var arch = execSync(path.resolve(__dirname) + '/../../bin/variables.sh arch'); - var abi = process.versions.modules; - var expected = 'libbitcoind-' + version + '-node' + abi + '-' + platform + '-' + arch + '.tgz'; - name.should.equal(expected); - }); -}); diff --git a/test/data/bitcoin.conf b/test/data/bitcoin.conf index 475d225d..353387fa 100644 --- a/test/data/bitcoin.conf +++ b/test/data/bitcoin.conf @@ -1,17 +1,23 @@ #testnet=1 #irc=0 -#upnp=0 +upnp=0 server=1 whitelist=127.0.0.1 txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +dbcache=8192 +checkblocks=144 +maxuploadtarget=1024 +zmqpubrawtx=tcp://127.0.0.1:28332 +zmqpubhashblock=tcp://127.0.0.1:28332 -# listen on different ports port=20000 +rpcport=50001 rpcallowip=127.0.0.1 rpcuser=bitcoin rpcpassword=local321 - - diff --git a/test/data/default.bitcoin.conf b/test/data/default.bitcoin.conf new file mode 100644 index 00000000..0f1bfde7 --- /dev/null +++ b/test/data/default.bitcoin.conf @@ -0,0 +1,11 @@ +server=1 +whitelist=127.0.0.1 +txindex=1 +addressindex=1 +timestampindex=1 +spentindex=1 +zmqpubrawtx=tcp://127.0.0.1:28332 +zmqpubhashblock=tcp://127.0.0.1:28332 +rpcallowip=127.0.0.1 +rpcuser=bitcoin +rpcpassword=local321 diff --git a/test/index.unit.js b/test/index.unit.js deleted file mode 100644 index c2190441..00000000 --- a/test/index.unit.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var index = require('..'); - -describe('Index', function() { - describe('#nodeVersionCheck', function() { - it('will throw informative error message with incompatible Node.js version 4.1.2', function() { - (function() { - index.nodeVersionCheck('4.1.2', '>=0.12.0 <1'); - }).should.throw('Node.js version'); - }); - it('will throw informative error message with incompatible Node.js version 0.10.40', function() { - (function() { - index.nodeVersionCheck('4.1.2', '>=0.12.0 <1'); - }).should.throw('Node.js version'); - }); - }); -}); diff --git a/test/scaffold/default-base-config.integration.js b/test/scaffold/default-base-config.integration.js index 5b09b3cd..b622c742 100644 --- a/test/scaffold/default-base-config.integration.js +++ b/test/scaffold/default-base-config.integration.js @@ -1,6 +1,7 @@ 'use strict'; var should = require('chai').should(); +var path = require('path'); var defaultBaseConfig = require('../../lib/scaffold/default-base-config'); describe('#defaultBaseConfig', function() { @@ -9,29 +10,19 @@ describe('#defaultBaseConfig', function() { var home = process.env.HOME; var info = defaultBaseConfig(); info.path.should.equal(cwd); - info.config.datadir.should.equal(home + '/.bitcoin'); info.config.network.should.equal('livenet'); info.config.port.should.equal(3001); - info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); + info.config.services.should.deep.equal(['bitcoind', 'web']); + var bitcoind = info.config.servicesConfig.bitcoind; + bitcoind.spawn.datadir.should.equal(home + '/.bitcoin'); + bitcoind.spawn.exec.should.equal(path.resolve(__dirname, '../../bin/bitcoind')); }); it('be able to specify a network', function() { - var cwd = process.cwd(); - var home = process.env.HOME; var info = defaultBaseConfig({network: 'testnet'}); - info.path.should.equal(cwd); - info.config.datadir.should.equal(home + '/.bitcoin'); info.config.network.should.equal('testnet'); - info.config.port.should.equal(3001); - info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); }); it('be able to specify a datadir', function() { - var cwd = process.cwd(); - var home = process.env.HOME; var info = defaultBaseConfig({datadir: './data2', network: 'testnet'}); - info.path.should.equal(cwd); - info.config.datadir.should.equal('./data2'); - info.config.network.should.equal('testnet'); - info.config.port.should.equal(3001); - info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); + info.config.servicesConfig.bitcoind.spawn.datadir.should.equal('./data2'); }); }); diff --git a/test/scaffold/default-config.integration.js b/test/scaffold/default-config.integration.js index cbe00816..83e674e5 100644 --- a/test/scaffold/default-config.integration.js +++ b/test/scaffold/default-config.integration.js @@ -1,21 +1,29 @@ 'use strict'; +var path = require('path'); var should = require('chai').should(); var sinon = require('sinon'); var proxyquire = require('proxyquire'); describe('#defaultConfig', function() { + var expectedExecPath = path.resolve(__dirname, '../../bin/bitcoind'); + it('will return expected configuration', function() { var config = JSON.stringify({ - datadir: process.env.HOME + '/.bitcore/data', network: 'livenet', port: 3001, services: [ 'bitcoind', - 'db', - 'address', 'web' - ] + ], + servicesConfig: { + bitcoind: { + spawn: { + datadir: process.env.HOME + '/.bitcore/data', + exec: expectedExecPath + } + } + } }, null, 2); var defaultConfig = proxyquire('../../lib/scaffold/default-config', { fs: { @@ -32,28 +40,35 @@ describe('#defaultConfig', function() { sync: sinon.stub() } }); - var cwd = process.cwd(); var home = process.env.HOME; var info = defaultConfig(); info.path.should.equal(home + '/.bitcore'); - info.config.datadir.should.equal(home + '/.bitcore/data'); info.config.network.should.equal('livenet'); info.config.port.should.equal(3001); - info.config.services.should.deep.equal(['bitcoind', 'db', 'address', 'web']); + info.config.services.should.deep.equal(['bitcoind', 'web']); + var bitcoind = info.config.servicesConfig.bitcoind; + should.exist(bitcoind); + bitcoind.spawn.datadir.should.equal(home + '/.bitcore/data'); + bitcoind.spawn.exec.should.equal(expectedExecPath); }); it('will include additional services', function() { var config = JSON.stringify({ - datadir: process.env.HOME + '/.bitcore/data', network: 'livenet', port: 3001, services: [ 'bitcoind', - 'db', - 'address', 'web', 'insight-api', 'insight-ui' - ] + ], + servicesConfig: { + bitcoind: { + spawn: { + datadir: process.env.HOME + '/.bitcore/data', + exec: expectedExecPath + } + } + } }, null, 2); var defaultConfig = proxyquire('../../lib/scaffold/default-config', { fs: { @@ -75,16 +90,17 @@ describe('#defaultConfig', function() { additionalServices: ['insight-api', 'insight-ui'] }); info.path.should.equal(home + '/.bitcore'); - info.config.datadir.should.equal(home + '/.bitcore/data'); info.config.network.should.equal('livenet'); info.config.port.should.equal(3001); info.config.services.should.deep.equal([ 'bitcoind', - 'db', - 'address', 'web', 'insight-api', 'insight-ui' ]); + var bitcoind = info.config.servicesConfig.bitcoind; + should.exist(bitcoind); + bitcoind.spawn.datadir.should.equal(home + '/.bitcore/data'); + bitcoind.spawn.exec.should.equal(expectedExecPath); }); }); diff --git a/test/scaffold/start.integration.js b/test/scaffold/start.integration.js index 2374d163..b2f4f61c 100644 --- a/test/scaffold/start.integration.js +++ b/test/scaffold/start.integration.js @@ -3,7 +3,7 @@ var should = require('chai').should(); var sinon = require('sinon'); var proxyquire = require('proxyquire'); -var AddressService = require('../../lib/services/address'); +var BitcoinService = require('../../lib/services/bitcoind'); describe('#start', function() { @@ -13,8 +13,8 @@ describe('#start', function() { var node; var TestNode = function(options) { options.services[0].should.deep.equal({ - name: 'address', - module: AddressService, + name: 'bitcoind', + module: BitcoinService, config: {} }); }; @@ -32,7 +32,7 @@ describe('#start', function() { path: __dirname, config: { services: [ - 'address' + 'bitcoind' ], datadir: './data' } @@ -67,8 +67,8 @@ describe('#start', function() { var node; var TestNode = function(options) { options.services[0].should.deep.equal({ - name: 'address', - module: AddressService, + name: 'bitcoind', + module: BitcoinService, config: { param: 'test' } @@ -88,10 +88,10 @@ describe('#start', function() { path: __dirname, config: { services: [ - 'address' + 'bitcoind' ], servicesConfig: { - 'address': { + 'bitcoind': { param: 'test' } }, diff --git a/test/scaffold/start.unit.js b/test/scaffold/start.unit.js index c955cbf9..85b2c507 100644 --- a/test/scaffold/start.unit.js +++ b/test/scaffold/start.unit.js @@ -212,110 +212,6 @@ describe('#start', function() { }); }); }); - describe('#spawnChildProcess', function() { - - it('should build the appropriate arguments to spawn a child process', function() { - var child = { - unref: function() {} - }; - var _process = { - exit: function() {}, - env: { - __bitcore_node: false - }, - argv: [ - 'node', - 'bitcore-node' - ], - cwd: function(){return ''}, - pid: 999, - execPath: '/tmp' - }; - var fd = {}; - var spawn = sinon.stub().returns(child); - var openSync = sinon.stub().returns(fd); - var spawnChildProcess = proxyquire('../../lib/scaffold/start', { - fs: { - openSync: openSync - }, - child_process: { - spawn: spawn - } - }).spawnChildProcess; - - spawnChildProcess('/tmp', _process); - - spawn.callCount.should.equal(1); - spawn.args[0][0].should.equal(_process.execPath); - var expected = [].concat(_process.argv); - expected.shift(); - spawn.args[0][1].should.deep.equal(expected); - var cp_opt = { - stdio: ['ignore', fd, fd], - env: _process.env, - cwd: '', - detached: true - }; - spawn.args[0][2].should.deep.equal(cp_opt); - openSync.callCount.should.equal(1); - openSync.args[0][0].should.equal('/tmp/bitcore-node.log'); - openSync.args[0][1].should.equal('a+'); - }); - it('should not spawn a new child process if there is already a daemon running', function() { - var _process = { - exit: function() {}, - env: { - __bitcore_node: true - }, - argv: [ - 'node', - 'bitcore-node' - ], - cwd: 'cwd', - pid: 999, - execPath: '/tmp' - }; - var spawnChildProcess = proxyquire('../../lib/scaffold/start', {}).spawnChildProcess; - spawnChildProcess('/tmp', _process).should.equal(999); - }); - }); - describe('daemon', function() { - var sandbox; - var spawn; - var setup; - var registerSync; - var registerExit; - var start = require('../../lib/scaffold/start'); - var options = { - config: { - datadir: '/tmp', - daemon: true - } - } - beforeEach(function() { - sandbox = sinon.sandbox.create(); - spawn = sandbox.stub(start, 'spawnChildProcess', function() {}); - setup = sandbox.stub(start, 'setupServices', function() {}); - registerSync = sandbox.stub(start, 'registerSyncHandlers', function() {}); - registerExit = sandbox.stub(start, 'registerExitHandlers', function() {}); - }); - afterEach(function() { - sandbox.restore(); - }); - it('call spawnChildProcess if there is a config option to do so', function() { - start(options); - registerSync.callCount.should.equal(1); - registerExit.callCount.should.equal(1); - spawn.callCount.should.equal(1); - }); - it('not call spawnChildProcess if there is not an option to do so', function() { - options.config.daemon = false; - start(options); - registerSync.callCount.should.equal(1); - registerExit.callCount.should.equal(1); - spawn.callCount.should.equal(0); - }); - }); describe('#registerExitHandlers', function() { var stub; var registerExitHandlers = require('../../lib/scaffold/start').registerExitHandlers; diff --git a/test/services/address/encoding.unit.js b/test/services/address/encoding.unit.js deleted file mode 100644 index e5ba7376..00000000 --- a/test/services/address/encoding.unit.js +++ /dev/null @@ -1,103 +0,0 @@ -'use strict'; - -var chai = require('chai'); -var should = chai.should(); -var sinon = require('sinon'); -var bitcorenode = require('../../../'); -var bitcore = require('bitcore-lib'); -var Address = bitcore.Address; -var Script = bitcore.Script; -var AddressService = bitcorenode.services.Address; -var Networks = bitcore.Networks; -var encoding = require('../../../lib/services/address/encoding'); - -var mockdb = { -}; - -var mocknode = { - network: Networks.testnet, - datadir: 'testdir', - db: mockdb, - services: { - bitcoind: { - on: sinon.stub() - } - } -}; - -describe('Address Service Encoding', function() { - - describe('#encodeSpentIndexSyncKey', function() { - it('will encode to 36 bytes (string)', function() { - var txidBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); - var key = encoding.encodeSpentIndexSyncKey(txidBuffer, 12); - key.length.should.equal(36); - }); - it('will be able to decode encoded value', function() { - var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'; - var txidBuffer = new Buffer(txid, 'hex'); - var key = encoding.encodeSpentIndexSyncKey(txidBuffer, 12); - var keyBuffer = new Buffer(key, 'binary'); - keyBuffer.slice(0, 32).toString('hex').should.equal(txid); - var outputIndex = keyBuffer.readUInt32BE(32); - outputIndex.should.equal(12); - }); - }); - - describe('#_encodeInputKeyMap/#_decodeInputKeyMap roundtrip', function() { - var encoded; - var outputTxIdBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); - it('encode key', function() { - encoded = encoding.encodeInputKeyMap(outputTxIdBuffer, 13); - }); - it('decode key', function() { - var key = encoding.decodeInputKeyMap(encoded); - key.outputTxId.toString('hex').should.equal(outputTxIdBuffer.toString('hex')); - key.outputIndex.should.equal(13); - }); - }); - - describe('#_encodeInputValueMap/#_decodeInputValueMap roundtrip', function() { - var encoded; - var inputTxIdBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); - it('encode key', function() { - encoded = encoding.encodeInputValueMap(inputTxIdBuffer, 7); - }); - it('decode key', function() { - var key = encoding.decodeInputValueMap(encoded); - key.inputTxId.toString('hex').should.equal(inputTxIdBuffer.toString('hex')); - key.inputIndex.should.equal(7); - }); - }); - - - describe('#extractAddressInfoFromScript', function() { - it('pay-to-publickey', function() { - var pubkey = new bitcore.PublicKey('022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da'); - var script = Script.buildPublicKeyOut(pubkey); - var info = encoding.extractAddressInfoFromScript(script, Networks.livenet); - info.addressType.should.equal(Address.PayToPublicKeyHash); - info.hashBuffer.toString('hex').should.equal('9674af7395592ec5d91573aa8d6557de55f60147'); - }); - it('pay-to-publickeyhash', function() { - var script = Script('OP_DUP OP_HASH160 20 0x0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG'); - var info = encoding.extractAddressInfoFromScript(script, Networks.livenet); - info.addressType.should.equal(Address.PayToPublicKeyHash); - info.hashBuffer.toString('hex').should.equal('0000000000000000000000000000000000000000'); - }); - it('pay-to-scripthash', function() { - var script = Script('OP_HASH160 20 0x0000000000000000000000000000000000000000 OP_EQUAL'); - var info = encoding.extractAddressInfoFromScript(script, Networks.livenet); - info.addressType.should.equal(Address.PayToScriptHash); - info.hashBuffer.toString('hex').should.equal('0000000000000000000000000000000000000000'); - }); - it('non-address script type', function() { - var buf = new Buffer(40); - buf.fill(0); - var script = Script('OP_RETURN 40 0x' + buf.toString('hex')); - var info = encoding.extractAddressInfoFromScript(script, Networks.livenet); - info.should.equal(false); - }); - }); - -}); diff --git a/test/services/address/history.unit.js b/test/services/address/history.unit.js deleted file mode 100644 index 2b6df06c..00000000 --- a/test/services/address/history.unit.js +++ /dev/null @@ -1,544 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var sinon = require('sinon'); -var bitcore = require('bitcore-lib'); -var Transaction = require('../../../lib/transaction'); -var AddressHistory = require('../../../lib/services/address/history'); - -describe('Address Service History', function() { - - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - - describe('@constructor', function() { - it('will construct a new instance', function() { - var node = {}; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history.should.be.instanceof(AddressHistory); - history.node.should.equal(node); - history.options.should.equal(options); - history.addresses.should.equal(addresses); - history.detailedArray.should.deep.equal([]); - }); - it('will set addresses an array if only sent a string', function() { - var history = new AddressHistory({ - node: {}, - options: {}, - addresses: address - }); - history.addresses.should.deep.equal([address]); - }); - }); - - describe('#get', function() { - it('will give an error if length of addresses is too long', function(done) { - var node = {}; - var options = {}; - var addresses = []; - for (var i = 0; i < 101; i++) { - addresses.push(address); - } - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history.maxAddressesQuery = 100; - history.get(function(err) { - should.exist(err); - err.message.match(/Maximum/); - done(); - }); - }); - it('give error from getAddressSummary with one address', function(done) { - var node = { - services: { - address: { - getAddressSummary: sinon.stub().callsArgWith(2, new Error('test')) - } - } - }; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history.get(function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - it('give error from getAddressSummary with multiple addresses', function(done) { - var node = { - services: { - address: { - getAddressSummary: sinon.stub().callsArgWith(2, new Error('test2')) - } - } - }; - var options = {}; - var addresses = [address, address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history.get(function(err) { - should.exist(err); - err.message.should.equal('test2'); - done(); - }); - }); - it('will query get address summary directly with one address', function(done) { - var txids = []; - var summary = { - txids: txids - }; - var node = { - services: { - address: { - getAddressSummary: sinon.stub().callsArgWith(2, null, summary) - } - } - }; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history._mergeAndSortTxids = sinon.stub(); - history._paginateWithDetails = sinon.stub().callsArg(1); - history.get(function() { - history.node.services.address.getAddressSummary.callCount.should.equal(1); - history.node.services.address.getAddressSummary.args[0][0].should.equal(address); - history.node.services.address.getAddressSummary.args[0][1].should.deep.equal({ - noBalance: true - }); - history._paginateWithDetails.callCount.should.equal(1); - history._paginateWithDetails.args[0][0].should.equal(txids); - history._mergeAndSortTxids.callCount.should.equal(0); - done(); - }); - }); - it('will merge multiple summaries with multiple addresses', function(done) { - var txids = []; - var summary = { - txids: txids - }; - var node = { - services: { - address: { - getAddressSummary: sinon.stub().callsArgWith(2, null, summary) - } - } - }; - var options = {}; - var addresses = [address, address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - history._mergeAndSortTxids = sinon.stub().returns(txids); - history._paginateWithDetails = sinon.stub().callsArg(1); - history.get(function() { - history.node.services.address.getAddressSummary.callCount.should.equal(2); - history.node.services.address.getAddressSummary.args[0][0].should.equal(address); - history.node.services.address.getAddressSummary.args[0][1].should.deep.equal({ - fullTxList: true, - noBalance: true - }); - history._paginateWithDetails.callCount.should.equal(1); - history._paginateWithDetails.args[0][0].should.equal(txids); - history._mergeAndSortTxids.callCount.should.equal(1); - done(); - }); - }); - }); - - describe('#_paginateWithDetails', function() { - it('slice txids based on "from" and "to" (3 to 30)', function() { - var node = {}; - var options = { - from: 3, - to: 30 - }; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - sinon.stub(history, 'getDetailedInfo', function(txid, next) { - this.detailedArray.push(txid); - next(); - }); - history._paginateWithDetails(txids, function(err, result) { - result.totalCount.should.equal(11); - result.items.should.deep.equal([7, 6, 5, 4, 3, 2, 1, 0]); - }); - }); - it('slice txids based on "from" and "to" (0 to 3)', function() { - var node = {}; - var options = { - from: 0, - to: 3 - }; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - sinon.stub(history, 'getDetailedInfo', function(txid, next) { - this.detailedArray.push(txid); - next(); - }); - history._paginateWithDetails(txids, function(err, result) { - result.totalCount.should.equal(11); - result.items.should.deep.equal([10, 9, 8]); - }); - }); - it('will given an error if the full details is too long', function() { - var node = {}; - var options = { - from: 0, - to: 3 - }; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - sinon.stub(history, 'getDetailedInfo', function(txid, next) { - this.detailedArray.push(txid); - next(); - }); - history.maxHistoryQueryLength = 1; - history._paginateWithDetails(txids, function(err) { - should.exist(err); - err.message.match(/Maximum/); - }); - }); - it('will give full result without pagination options', function() { - var node = {}; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - sinon.stub(history, 'getDetailedInfo', function(txid, next) { - this.detailedArray.push(txid); - next(); - }); - history._paginateWithDetails(txids, function(err, result) { - result.totalCount.should.equal(11); - result.items.should.deep.equal([10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); - }); - }); - }); - - describe('#_mergeAndSortTxids', function() { - it('will merge and sort multiple summaries', function() { - var summaries = [ - { - totalReceived: 10000000, - totalSpent: 0, - balance: 10000000, - appearances: 2, - unconfirmedBalance: 20000000, - unconfirmedAppearances: 2, - appearanceIds: { - '56fafeb01961831b926558d040c246b97709fd700adcaa916541270583e8e579': 154, - 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce': 120 - }, - unconfirmedAppearanceIds: { - 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681': 1452898347406, - 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693': 1452898331964 - } - }, - { - totalReceived: 59990000, - totalSpent: 0, - balance: 49990000, - appearances: 3, - unconfirmedBalance: 1000000, - unconfirmedAppearances: 3, - appearanceIds: { - 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2': 156, - 'f3c1ba3ef86a0420d6102e40e2cfc8682632ab95d09d86a27f5d466b9fa9da47': 152, - 'f637384e9f81f18767ea50e00bce58fc9848b6588a1130529eebba22a410155f': 151 - }, - unconfirmedAppearanceIds: { - 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345': 1452897902377, - 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a': 1452897971363, - 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9': 1452897923107 - } - } - ]; - var node = {}; - var options = {}; - var addresses = [address]; - var history = new AddressHistory({ - node: node, - options: options, - addresses: addresses - }); - var txids = history._mergeAndSortTxids(summaries); - txids.should.deep.equal([ - 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', - 'f637384e9f81f18767ea50e00bce58fc9848b6588a1130529eebba22a410155f', - 'f3c1ba3ef86a0420d6102e40e2cfc8682632ab95d09d86a27f5d466b9fa9da47', - '56fafeb01961831b926558d040c246b97709fd700adcaa916541270583e8e579', - 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2', - 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345', - 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9', - 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a', - 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693', - 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681' - ]); - }); - }); - - describe('#getDetailedInfo', function() { - it('will add additional information to existing this.transactions', function(done) { - var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; - var tx = { - populateInputs: sinon.stub().callsArg(2), - __height: 20, - __timestamp: 1453134151, - isCoinbase: sinon.stub().returns(false), - getFee: sinon.stub().returns(1000) - }; - var history = new AddressHistory({ - node: { - services: { - db: { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, tx), - tip: { - __height: 300 - } - } - } - }, - options: {}, - addresses: [] - }); - history.getAddressDetailsForTransaction = sinon.stub().returns({ - addresses: {}, - satoshis: 1000, - }); - history.getDetailedInfo(txid, function(err) { - if (err) { - throw err; - } - history.node.services.db.getTransactionWithBlockInfo.callCount.should.equal(1); - done(); - }); - }); - it('will handle error from getTransactionFromBlock', function(done) { - var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; - var history = new AddressHistory({ - node: { - services: { - db: { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('test')), - } - } - }, - options: {}, - addresses: [] - }); - history.getDetailedInfo(txid, function(err) { - err.message.should.equal('test'); - done(); - }); - }); - it('will handle error from populateInputs', function(done) { - var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; - var history = new AddressHistory({ - node: { - services: { - db: { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, { - populateInputs: sinon.stub().callsArgWith(2, new Error('test')) - }), - } - } - }, - options: {}, - addresses: [] - }); - history.getDetailedInfo(txid, function(err) { - err.message.should.equal('test'); - done(); - }); - }); - it('will set this.transactions with correct information', function(done) { - // block #314159 - // txid 30169e8bf78bc27c4014a7aba3862c60e2e3cce19e52f1909c8255e4b7b3174e - // outputIndex 1 - var txAddress = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; - var txString = '0100000001a08ee59fcd5d86fa170abb6d925d62d5c5c476359681b70877c04f270c4ef246000000008a47304402203fb9b476bb0c37c9b9ed5784ebd67ae589492be11d4ae1612be29887e3e4ce750220741ef83781d1b3a5df8c66fa1957ad0398c733005310d7d9b1d8c2310ef4f74c0141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02b0a75fac000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac20badc02000000001976a914809dc14496f99b6deb722cf46d89d22f4beb8efd88ac00000000'; - var previousTxString = '010000000155532fad2869bb951b0bd646a546887f6ee668c4c0ee13bf3f1c4bce6d6e3ed9000000008c4930460221008540795f4ef79b1d2549c400c61155ca5abbf3089c84ad280e1ba6db2a31abce022100d7d162175483d51174d40bba722e721542c924202a0c2970b07e680b51f3a0670141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02f0af3caf000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac80969800000000001976a91421277e65777760d1f3c7c982ba14ed8f934f005888ac00000000'; - var transaction = new Transaction(); - var previousTransaction = new Transaction(); - previousTransaction.fromString(previousTxString); - var previousTransactionTxid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; - transaction.fromString(txString); - var txid = transaction.hash; - transaction.__blockHash = '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45'; - transaction.__height = 314159; - transaction.__timestamp = 1407292005; - var history = new AddressHistory({ - node: { - services: { - db: { - tip: { - __height: 314159 - }, - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, transaction), - getTransaction: function(prevTxid, queryMempool, callback) { - prevTxid.should.equal(previousTransactionTxid); - setImmediate(function() { - callback(null, previousTransaction); - }); - } - } - } - }, - options: {}, - addresses: [txAddress] - }); - var transactionInfo = { - addresses: {}, - txid: txid, - timestamp: 1407292005, - satoshis: 48020000, - address: txAddress - }; - transactionInfo.addresses[txAddress] = {}; - transactionInfo.addresses[txAddress].outputIndexes = [1]; - transactionInfo.addresses[txAddress].inputIndexes = []; - history.getDetailedInfo(txid, function(err) { - if (err) { - throw err; - } - var info = history.detailedArray[0]; - info.addresses[txAddress].should.deep.equal({ - outputIndexes: [1], - inputIndexes: [] - }); - info.satoshis.should.equal(48020000); - info.height.should.equal(314159); - info.confirmations.should.equal(1); - info.timestamp.should.equal(1407292005); - info.fees.should.equal(20000); - info.tx.should.equal(transaction); - done(); - }); - }); - }); - - describe('#getAddressDetailsForTransaction', function() { - it('will calculate details for the transaction', function(done) { - /* jshint sub:true */ - var tx = bitcore.Transaction({ - 'hash': 'b12b3ae8489c5a566b629a3c62ce4c51c3870af550fb5dc77d715b669a91343c', - 'version': 1, - 'inputs': [ - { - 'prevTxId': 'a2b7ea824a92f4a4944686e67ec1001bc8785348b8c111c226f782084077b543', - 'outputIndex': 0, - 'sequenceNumber': 4294967295, - 'script': '47304402201b81c933297241960a57ae1b2952863b965ac8c9ec7466ff0b715712d27548d50220576e115b63864f003889443525f47c7cf0bc1e2b5108398da085b221f267ba2301210229766f1afa25ca499a51f8e01c292b0255a21a41bb6685564a1607a811ffe924', - 'scriptString': '71 0x304402201b81c933297241960a57ae1b2952863b965ac8c9ec7466ff0b715712d27548d50220576e115b63864f003889443525f47c7cf0bc1e2b5108398da085b221f267ba2301 33 0x0229766f1afa25ca499a51f8e01c292b0255a21a41bb6685564a1607a811ffe924', - 'output': { - 'satoshis': 1000000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - } - } - ], - 'outputs': [ - { - 'satoshis': 100000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - }, - { - 'satoshis': 200000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - }, - { - 'satoshis': 50000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - }, - { - 'satoshis': 300000000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - }, - { - 'satoshis': 349990000, - 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' - } - ], - 'nLockTime': 0 - }); - var history = new AddressHistory({ - node: { - network: bitcore.Networks.testnet - }, - options: {}, - addresses: ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'] - }); - var details = history.getAddressDetailsForTransaction(tx); - should.exist(details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']); - details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].inputIndexes.should.deep.equal([0]); - details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].outputIndexes.should.deep.equal([ - 0, 1, 2, 3, 4 - ]); - details.satoshis.should.equal(-10000); - done(); - }); - }); - - describe('#getConfirmationsDetail', function() { - it('the correct confirmations when included in the tip', function(done) { - var history = new AddressHistory({ - node: { - services: { - db: { - tip: { - __height: 100 - } - } - } - }, - options: {}, - addresses: [] - }); - var transaction = { - __height: 100 - }; - history.getConfirmationsDetail(transaction).should.equal(1); - done(); - }); - }); -}); diff --git a/test/services/address/index.unit.js b/test/services/address/index.unit.js deleted file mode 100644 index 48bda199..00000000 --- a/test/services/address/index.unit.js +++ /dev/null @@ -1,2676 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var sinon = require('sinon'); -var stream = require('stream'); -var levelup = require('levelup'); -var proxyquire = require('proxyquire'); -var bitcorenode = require('../../../'); -var AddressService = bitcorenode.services.Address; -var blockData = require('../../data/livenet-345003.json'); -var bitcore = require('bitcore-lib'); -var _ = bitcore.deps._; -var memdown = require('memdown'); -var leveldown = require('leveldown'); -var Networks = bitcore.Networks; -var EventEmitter = require('events').EventEmitter; -var errors = bitcorenode.errors; -var Transaction = require('../../../lib/transaction'); -var txData = require('../../data/transaction.json'); -var index = require('../../../lib'); -var log = index.log; -var constants = require('../../../lib/services/address/constants'); -var encoding = require('../../../lib/services/address/encoding'); - -var mockdb = { -}; - -var mocknode = { - network: Networks.testnet, - datadir: 'testdir', - db: mockdb, - services: { - bitcoind: { - on: sinon.stub() - } - } -}; - -describe('Address Service', function() { - var txBuf = new Buffer(txData[0], 'hex'); - - describe('@constructor', function() { - it('config to use memdown for mempool index', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.levelupStore.should.equal(memdown); - }); - it('config to use leveldown for mempool index', function() { - var am = new AddressService({ - node: mocknode - }); - am.levelupStore.should.equal(leveldown); - }); - }); - - describe('#start', function() { - it('will flush existing mempool', function(done) { - var leveldownmock = { - destroy: sinon.stub().callsArgWith(1, null) - }; - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(true) - }, - 'leveldown': leveldownmock, - 'levelup': sinon.stub().callsArgWith(2, null), - 'mkdirp': sinon.stub().callsArgWith(1, null) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function() { - leveldownmock.destroy.callCount.should.equal(1); - leveldownmock.destroy.args[0][0].should.equal('testdir/testnet3/bitcore-addressmempool.db'); - done(); - }); - }); - it('will mkdirp if directory does not exist', function(done) { - var leveldownmock = { - destroy: sinon.stub().callsArgWith(1, null) - }; - var mkdirpmock = sinon.stub().callsArgWith(1, null); - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(false) - }, - 'leveldown': leveldownmock, - 'levelup': sinon.stub().callsArgWith(2, null), - 'mkdirp': mkdirpmock - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function() { - mkdirpmock.callCount.should.equal(1); - mkdirpmock.args[0][0].should.equal('testdir/testnet3/bitcore-addressmempool.db'); - done(); - }); - }); - it('start levelup db for mempool', function(done) { - var levelupStub = sinon.stub().callsArg(2); - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(true) - }, - 'leveldown': { - destroy: sinon.stub().callsArgWith(1, null) - }, - 'levelup': levelupStub, - 'mkdirp': sinon.stub().callsArgWith(1, null) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function() { - levelupStub.callCount.should.equal(1); - var dbPath1 = levelupStub.args[0][0]; - dbPath1.should.equal('testdir/testnet3/bitcore-addressmempool.db'); - var options = levelupStub.args[0][1]; - options.db.should.equal(memdown); - options.keyEncoding.should.equal('binary'); - options.valueEncoding.should.equal('binary'); - options.fillCache.should.equal(false); - done(); - }); - }); - it('handle error from mkdirp', function(done) { - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(false) - }, - 'leveldown': { - destroy: sinon.stub().callsArgWith(1, null) - }, - 'levelup': sinon.stub().callsArgWith(2, null), - 'mkdirp': sinon.stub().callsArgWith(1, new Error('testerror')) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function(err) { - err.message.should.equal('testerror'); - done(); - }); - }); - it('handle error from levelup', function(done) { - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(false) - }, - 'leveldown': { - destroy: sinon.stub().callsArgWith(1, null) - }, - 'levelup': sinon.stub().callsArgWith(2, new Error('leveltesterror')), - 'mkdirp': sinon.stub().callsArgWith(1, null) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function(err) { - err.message.should.equal('leveltesterror'); - done(); - }); - }); - it('handle error from leveldown.destroy', function(done) { - var TestAddressService = proxyquire('../../../lib/services/address', { - 'fs': { - existsSync: sinon.stub().returns(true) - }, - 'leveldown': { - destroy: sinon.stub().callsArgWith(1, new Error('destroy')) - }, - 'levelup': sinon.stub().callsArgWith(2, null), - 'mkdirp': sinon.stub().callsArgWith(1, null) - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.start(function(err) { - err.message.should.equal('destroy'); - done(); - }); - }); - }); - - describe('#stop', function() { - it('will close mempool levelup', function(done) { - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - db: mockdb, - services: { - bitcoind: { - on: sinon.stub(), - removeListener: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.mempoolIndex = {}; - am.mempoolIndex.close = sinon.stub().callsArg(0); - am.stop(function() { - am.mempoolIndex.close.callCount.should.equal(1); - am.node.services.bitcoind.removeListener.callCount.should.equal(2); - done(); - }); - }); - }); - - describe('#_setMempoolIndexPath', function() { - it('should set the database path', function() { - var testnode = { - network: Networks.livenet, - datadir: process.env.HOME + '/.bitcoin', - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am._setMempoolIndexPath(); - am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/bitcore-addressmempool.db'); - }); - it('should load the db for testnet', function() { - var testnode = { - network: Networks.testnet, - datadir: process.env.HOME + '/.bitcoin', - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am._setMempoolIndexPath(); - am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/testnet3/bitcore-addressmempool.db'); - }); - it('error with unknown network', function() { - var testnode = { - network: 'unknown', - datadir: process.env.HOME + '/.bitcoin', - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - (function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }).should.throw('Unknown network'); - }); - it('should load the db with regtest', function() { - // Switch to use regtest - Networks.enableRegtest(); - var regtest = Networks.get('regtest'); - var testnode = { - network: regtest, - datadir: process.env.HOME + '/.bitcoin', - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.mempoolIndexPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-addressmempool.db'); - Networks.disableRegtest(); - }); - }); - - describe('#getAPIMethods', function() { - it('should return the correct methods', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var methods = am.getAPIMethods(); - methods.length.should.equal(7); - }); - }); - - describe('#getPublishEvents', function() { - it('will return an array of publish event objects', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.subscribe = sinon.spy(); - am.unsubscribe = sinon.spy(); - var events = am.getPublishEvents(); - - var callCount = 0; - function testName(event, name) { - event.name.should.equal(name); - event.scope.should.equal(am); - var emitter = new EventEmitter(); - var addresses = []; - event.subscribe(emitter, addresses); - am.subscribe.callCount.should.equal(callCount + 1); - am.subscribe.args[callCount][0].should.equal(name); - am.subscribe.args[callCount][1].should.equal(emitter); - am.subscribe.args[callCount][2].should.equal(addresses); - am.subscribe.thisValues[callCount].should.equal(am); - event.unsubscribe(emitter, addresses); - am.unsubscribe.callCount.should.equal(callCount + 1); - am.unsubscribe.args[callCount][0].should.equal(name); - am.unsubscribe.args[callCount][1].should.equal(emitter); - am.unsubscribe.args[callCount][2].should.equal(addresses); - am.unsubscribe.thisValues[callCount].should.equal(am); - callCount++; - } - events.forEach(function(event) { - testName(event, event.name); - }); - - }); - }); - - describe('#transactionOutputHandler', function() { - it('create a message for an address', function() { - var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); - var tx = bitcore.Transaction().fromBuffer(txBuf); - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.node.network = Networks.livenet; - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var addrObj = bitcore.Address(address); - var hashHex = addrObj.hashBuffer.toString('hex'); - var hashType = addrObj.type; - var messages = {}; - am.transactionOutputHandler(messages, tx, 0, true); - should.exist(messages[hashHex]); - var message = messages[hashHex]; - message.tx.should.equal(tx); - message.outputIndexes.should.deep.equal([0]); - message.addressInfo.hashBuffer.toString('hex').should.equal(hashHex); - message.addressInfo.addressType.should.equal(hashType); - message.addressInfo.hashHex.should.equal(hashHex); - message.rejected.should.equal(true); - }); - }); - - describe('#transactionHandler', function() { - it('will pass outputs to transactionOutputHandler and call transactionEventHandler and balanceEventHandler', function(done) { - var txBuf = new Buffer('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000', 'hex'); - var am1 = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var message = {}; - am1.transactionOutputHandler = function(messages) { - messages[address] = message; - }; - am1.transactionEventHandler = sinon.stub(); - am1.balanceEventHandler = sinon.stub(); - am1.transactionHandler({ - buffer: txBuf - }, function(err) { - if (err) { - throw err; - } - am1.transactionEventHandler.callCount.should.equal(1); - am1.balanceEventHandler.callCount.should.equal(1); - done(); - }); - - }); - }); - - describe('#blockHandler', function() { - var am; - var testBlock = bitcore.Block.fromString(blockData); - - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.node.network = Networks.livenet; - }); - - it('should create the correct operations when updating/adding outputs', function(done) { - var block = { - __height: 345003, - header: { - timestamp: 1424836934 - }, - transactions: testBlock.transactions.slice(0, 8) - }; - - am.blockHandler(block, true, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(151); - operations[0].type.should.equal('put'); - operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b0100000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); - operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac'); - operations[3].type.should.equal('put'); - operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e0100000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); - operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); - operations[4].type.should.equal('put'); - operations[4].key.toString('hex').should.equal('053d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); - operations[4].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); - operations[121].type.should.equal('put'); - operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c75280100000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); - operations[121].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac'); - done(); - }); - }); - it('should create the correct operations when removing outputs', function(done) { - var block = { - __height: 345003, - header: { - timestamp: 1424836934 - }, - transactions: testBlock.transactions.slice(0, 8) - }; - am.blockHandler(block, false, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(151); - operations[0].type.should.equal('del'); - operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b0100000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); - operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac'); - operations[3].type.should.equal('del'); - operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e0100000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); - operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); - operations[121].type.should.equal('del'); - operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c75280100000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); - operations[121].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac'); - done(); - }); - }); - it('should continue if output script is null', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode, - }); - - var block = { - __height: 345003, - header: { - timestamp: 1424836934 - }, - transactions: [ - { - id: '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', - inputs: [], - outputs: [ - { - script: null, - satoshis: 1000, - } - ], - isCoinbase: sinon.stub().returns(false) - } - ] - }; - - am.blockHandler(block, false, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(0); - done(); - }); - }); - it('will call event handlers', function() { - var testBlock = bitcore.Block.fromString(blockData); - var db = {}; - var testnode = { - datadir: 'testdir', - db: db, - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.transactionEventHandler = sinon.spy(); - am.balanceEventHandler = sinon.spy(); - - var block = { - __height: 345003, - header: { - timestamp: 1424836934 - }, - transactions: testBlock.transactions.slice(0, 8) - }; - - am.blockHandler( - block, - true, - function(err) { - if (err) { - throw err; - } - am.transactionEventHandler.callCount.should.equal(11); - am.balanceEventHandler.callCount.should.equal(11); - } - ); - }); - }); - - describe('#transactionEventHandler', function() { - it('will emit a transaction if there is a subscriber', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - am.subscriptions['address/transaction'] = {}; - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] = [emitter]; - var block = { - __height: 0, - timestamp: new Date() - }; - var tx = {}; - emitter.on('address/transaction', function(obj) { - obj.address.toString().should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - obj.tx.should.equal(tx); - obj.timestamp.should.equal(block.timestamp); - obj.height.should.equal(block.__height); - obj.outputIndexes.should.deep.equal([1]); - done(); - }); - am.transactionEventHandler({ - addressInfo: { - hashHex: address.hashBuffer.toString('hex'), - hashBuffer: address.hashBuffer, - addressType: address.type - }, - height: block.__height, - timestamp: block.timestamp, - outputIndexes: [1], - tx: tx - }); - }); - }); - - describe('#balanceEventHandler', function() { - it('will emit a balance if there is a subscriber', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] = [emitter]; - var block = {}; - var balance = 1000; - am.getBalance = sinon.stub().callsArgWith(2, null, balance); - emitter.on('address/balance', function(a, bal, b) { - a.toString().should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - bal.should.equal(balance); - b.should.equal(block); - done(); - }); - am.balanceEventHandler(block, { - hashHex: address.hashBuffer.toString('hex'), - hashBuffer: address.hashBuffer, - addressType: address.type - }); - }); - }); - - describe('#subscribe', function() { - it('will add emitters to the subscribers array (transaction)', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - var name = 'address/transaction'; - am.subscribe(name, emitter, [address]); - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter]); - - var address2 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - am.subscribe(name, emitter, [address2]); - am.subscriptions['address/transaction'][address2.hashBuffer.toString('hex')] - .should.deep.equal([emitter]); - - var emitter2 = new EventEmitter(); - am.subscribe(name, emitter2, [address]); - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter, emitter2]); - }); - it('will add an emitter to the subscribers array (balance)', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var name = 'address/balance'; - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - am.subscribe(name, emitter, [address]); - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter]); - - var address2 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - am.subscribe(name, emitter, [address2]); - am.subscriptions['address/balance'][address2.hashBuffer.toString('hex')] - .should.deep.equal([emitter]); - - var emitter2 = new EventEmitter(); - am.subscribe(name, emitter2, [address]); - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter, emitter2]); - }); - }); - - describe('#unsubscribe', function() { - it('will remove emitter from subscribers array (transaction)', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var emitter2 = new EventEmitter(); - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] = [emitter, emitter2]; - var name = 'address/transaction'; - am.unsubscribe(name, emitter, [address]); - am.subscriptions['address/transaction'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter2]); - }); - it('will remove emitter from subscribers array (balance)', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var emitter2 = new EventEmitter(); - var address = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - var name = 'address/balance'; - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] = [emitter, emitter2]; - am.unsubscribe(name, emitter, [address]); - am.subscriptions['address/balance'][address.hashBuffer.toString('hex')] - .should.deep.equal([emitter2]); - }); - it('should unsubscribe from all addresses if no addresses are specified', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var emitter = new EventEmitter(); - var emitter2 = new EventEmitter(); - var address1 = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - var hashHex1 = address1.hashBuffer.toString('hex'); - var address2 = bitcore.Address('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); - var hashHex2 = address2.hashBuffer.toString('hex'); - am.subscriptions['address/balance'][hashHex1] = [emitter, emitter2]; - am.subscriptions['address/balance'][hashHex2] = [emitter2, emitter]; - am.unsubscribe('address/balance', emitter); - am.subscriptions['address/balance'][hashHex1].should.deep.equal([emitter2]); - am.subscriptions['address/balance'][hashHex2].should.deep.equal([emitter2]); - }); - }); - - describe('#getBalance', function() { - it('should sum up the unspent outputs', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - var outputs = [ - {satoshis: 1000}, {satoshis: 2000}, {satoshis: 3000} - ]; - am.getUnspentOutputs = sinon.stub().callsArgWith(2, null, outputs); - am.getBalance('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N', false, function(err, balance) { - should.not.exist(err); - balance.should.equal(6000); - done(); - }); - }); - - it('will handle error from unspent outputs', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getUnspentOutputs = sinon.stub().callsArgWith(2, new Error('error')); - am.getBalance('someaddress', false, function(err) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - - }); - - describe('#createInputsStream', function() { - it('transform stream from buffer into object', function(done) { - var testnode = { - network: Networks.livenet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - tip: { - __height: 157 - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - addressService.createInputsDBStream = sinon.stub().returns(streamStub); - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createInputsStream(address, {}); - testStream.once('data', function(data) { - data.address.should.equal('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - data.hashType.should.equal('pubkeyhash'); - data.txid.should.equal('7b94e3c39386845ea383b8e726b20b5172ccd3ef9be008bbb133e3b63f07df72'); - data.inputIndex.should.equal(1); - data.height.should.equal(157); - data.confirmations.should.equal(1); - done(); - }); - streamStub.emit('data', { - key: new Buffer('030b2f0a0c31bfe0406b0ccc1381fdbe311946dadc01000000009d786cfeae288d74aaf9f51f215f9882e7bd7bc18af7a550683c4d7c6962f6372900000004', 'hex'), - value: new Buffer('7b94e3c39386845ea383b8e726b20b5172ccd3ef9be008bbb133e3b63f07df7200000001', 'hex') - }); - streamStub.emit('end'); - }); - }); - - describe('#createInputsDBStream', function() { - it('will stream all keys', function() { - var streamStub = sinon.stub().returns({}); - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - store: { - createReadStream: streamStub - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = {}; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createInputsDBStream(address, options); - should.exist(testStream); - streamStub.callCount.should.equal(1); - var expectedGt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; - // The expected "lt" value should be one value above the start value, due - // to the keys having additional data following it and can't be "equal". - var expectedLt = '03038a213afdfc551fc658e9a2a58a86e98d69b68701ffffffffff'; - streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); - streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); - }); - it('will stream keys based on a range of block heights', function() { - var streamStub = sinon.stub().returns({}); - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - store: { - createReadStream: streamStub - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = { - start: 1, - end: 0 - }; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createInputsDBStream(address, options); - should.exist(testStream); - streamStub.callCount.should.equal(1); - var expectedGt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; - // The expected "lt" value should be one value above the start value, due - // to the keys having additional data following it and can't be "equal". - var expectedLt = '03038a213afdfc551fc658e9a2a58a86e98d69b687010000000002'; - streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); - streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); - }); - }); - - describe('#getInputs', function() { - var am; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - var db = { - tip: { - __height: 1 - } - }; - var testnode = { - network: Networks.livenet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }); - - it('will add mempool inputs on close', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var db = { - store: { - createReadStream: sinon.stub().returns(testStream) - }, - tip: { - __height: 10 - } - }; - var testnode = { - network: Networks.livenet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var args = { - start: 15, - end: 12, - queryMempool: true - }; - am._getInputsMempool = sinon.stub().callsArgWith(3, null, { - address: address, - hashType: 'pubkeyhash', - height: -1, - confirmations: 0 - }); - am.getInputs(address, args, function(err, inputs) { - should.not.exist(err); - inputs.length.should.equal(1); - inputs[0].address.should.equal(address); - inputs[0].height.should.equal(-1); - done(); - }); - testStream.push(null); - }); - it('will get inputs for an address and timestamp', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - start: 15, - end: 12, - queryMempool: true - }; - var createReadStreamCallCount = 0; - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, - hashTypeBuffer, new Buffer('000000000c', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, - hashTypeBuffer, new Buffer('0000000010', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am.node.services.bitcoind = { - getMempoolInputs: sinon.stub().returns([]) - }; - am._getInputsMempool = sinon.stub().callsArgWith(3, null, []); - am.getInputs(address, args, function(err, inputs) { - should.not.exist(err); - inputs.length.should.equal(1); - inputs[0].address.should.equal(address); - inputs[0].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); - inputs[0].inputIndex.should.equal(0); - inputs[0].height.should.equal(15); - done(); - }); - createReadStreamCallCount.should.equal(1); - var data = { - key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex') - }; - testStream.emit('data', data); - testStream.push(null); - }); - it('should get inputs for address', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - queryMempool: true - }; - var createReadStreamCallCount = 0; - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('0000000000', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('ffffffffff', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am.node.services.bitcoind = { - getMempoolInputs: sinon.stub().returns([]) - }; - am.getInputs(address, args, function(err, inputs) { - should.not.exist(err); - inputs.length.should.equal(1); - inputs[0].address.should.equal(address); - inputs[0].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); - inputs[0].inputIndex.should.equal(0); - inputs[0].height.should.equal(15); - done(); - }); - createReadStreamCallCount.should.equal(1); - var data = { - key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex') - }; - testStream.emit('data', data); - testStream.push(null); - }); - it('should give an error if the readstream has an error', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - am.node.services.db.store = { - createReadStream: sinon.stub().returns(testStream) - }; - - am.getInputs(address, {}, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - - testStream.emit('error', new Error('readstreamerror')); - setImmediate(function() { - testStream.push(null); - }); - }); - - }); - - describe('#_getInputsMempool', function() { - var am; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - var db = { - tip: { - __height: 1 - } - }; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }); - it('it will handle error', function(done) { - var testStream = new EventEmitter(); - am.mempoolIndex = {}; - am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - - am._getInputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - - testStream.emit('error', new Error('readstreamerror')); - setImmediate(function() { - testStream.emit('close'); - }); - }); - it('it will parse data', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - am.mempoolIndex = {}; - am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - - var nowTime = new Date().getTime(); - - am._getInputsMempool(address, hashBuffer, hashTypeBuffer, function(err, inputs) { - should.not.exist(err); - inputs.length.should.equal(1); - var input = inputs[0]; - input.address.should.equal(address); - input.txid.should.equal(txid); - input.hashType.should.equal('pubkeyhash'); - input.hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); - input.inputIndex.should.equal(5); - input.height.should.equal(-1); - input.confirmations.should.equal(0); - input.timestamp.should.equal(nowTime); - done(); - }); - - var txid = '5d32f0fff6871c377e00c16f48ebb5e89c723d0b9dd25f68fdda70c3392bee61'; - var inputIndex = 5; - var inputIndexBuffer = new Buffer(4); - var timestampBuffer = new Buffer(new Array(8)); - timestampBuffer.writeDoubleBE(nowTime); - inputIndexBuffer.writeUInt32BE(inputIndex); - var valueData = Buffer.concat([ - new Buffer(txid, 'hex'), - inputIndexBuffer, - timestampBuffer - ]); - // Note: key is not used currently - testStream.emit('data', { - value: valueData - }); - testStream.emit('close'); - }); - }); - - describe('#_getSpentMempool', function() { - it('will decode data from the database', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.mempoolIndex = {}; - var mempoolValue = Buffer.concat([ - new Buffer('85630d684f1f414264f88a31bddfc79dd0c00659330dcdc393b321c121f4078b', 'hex'), - new Buffer('00000003', 'hex') - ]); - am.mempoolIndex.get = sinon.stub().callsArgWith(1, null, mempoolValue); - var prevTxIdBuffer = new Buffer('e7888264d286be2da26b0a4dbd2fc5c9ece82b3e40e6791b137e4155b6da8981', 'hex'); - var outputIndex = 1; - var outputIndexBuffer = new Buffer('00000001', 'hex'); - var expectedKey = Buffer.concat([ - new Buffer('03', 'hex'), - prevTxIdBuffer, - outputIndexBuffer - ]).toString('hex'); - am._getSpentMempool(prevTxIdBuffer, outputIndex, function(err, value) { - if (err) { - throw err; - } - am.mempoolIndex.get.args[0][0].toString('hex').should.equal(expectedKey); - value.inputTxId.should.equal('85630d684f1f414264f88a31bddfc79dd0c00659330dcdc393b321c121f4078b'); - value.inputIndex.should.equal(3); - }); - }); - it('handle error from levelup', function() { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.mempoolIndex = {}; - am.mempoolIndex.get = sinon.stub().callsArgWith(1, new Error('test')); - var prevTxIdBuffer = new Buffer('e7888264d286be2da26b0a4dbd2fc5c9ece82b3e40e6791b137e4155b6da8981', 'hex'); - var outputIndex = 1; - am._getSpentMempool(prevTxIdBuffer, outputIndex, function(err) { - err.message.should.equal('test'); - }); - }); - }); - - describe('#createOutputsStream', function() { - it('transform stream from buffer into object', function(done) { - var testnode = { - network: Networks.livenet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - tip: { - __height: 157 - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - addressService.createOutputsDBStream = sinon.stub().returns(streamStub); - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createOutputsStream(address, {}); - testStream.once('data', function(data) { - data.address.should.equal('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'); - data.hashType.should.equal('pubkeyhash'); - data.txid.should.equal('4078b72b09391f5146e2c564f5847d49b179f9946b253f780f65b140d46ef6f9'); - data.outputIndex.should.equal(2); - data.height.should.equal(157); - data.satoshis.should.equal(10000); - data.script.toString('hex').should.equal('76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'); - data.confirmations.should.equal(1); - done(); - }); - streamStub.emit('data', { - key: new Buffer('020b2f0a0c31bfe0406b0ccc1381fdbe311946dadc01000000009d4078b72b09391f5146e2c564f5847d49b179f9946b253f780f65b140d46ef6f900000002', 'hex'), - value: new Buffer('40c388000000000076a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac', 'hex') - }); - streamStub.emit('end'); - }); - }); - - describe('#createOutputsDBStream', function() { - it('will stream all keys', function() { - var streamStub = sinon.stub().returns({}); - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - store: { - createReadStream: streamStub - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = {}; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createOutputsDBStream(address, options); - should.exist(testStream); - streamStub.callCount.should.equal(1); - var expectedGt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; - // The expected "lt" value should be one value above the start value, due - // to the keys having additional data following it and can't be "equal". - var expectedLt = '02038a213afdfc551fc658e9a2a58a86e98d69b68701ffffffffff'; - streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); - streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); - }); - it('will stream keys based on a range of block heights', function() { - var streamStub = sinon.stub().returns({}); - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - }, - db: { - store: { - createReadStream: streamStub - } - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = { - start: 1, - end: 0 - }; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var testStream = addressService.createOutputsDBStream(address, options); - should.exist(testStream); - streamStub.callCount.should.equal(1); - var expectedGt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000000'; - // The expected "lt" value should be one value above the start value, due - // to the keys having additional data following it and can't be "equal". - var expectedLt = '02038a213afdfc551fc658e9a2a58a86e98d69b687010000000002'; - streamStub.args[0][0].gt.toString('hex').should.equal(expectedGt); - streamStub.args[0][0].lt.toString('hex').should.equal(expectedLt); - }); - }); - - describe('#getOutputs', function() { - var am; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var hashBuffer = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W').hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - var db = { - tip: { - __height: 1 - } - }; - var testnode = { - network: Networks.livenet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - var options = { - queryMempool: true - }; - - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }); - - it('will get outputs for an address and timestamp', function(done) { - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - start: 15, - end: 12, - queryMempool: true - }; - var createReadStreamCallCount = 0; - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); - am.getOutputs(address, args, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(1); - outputs[0].address.should.equal(address); - outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); - outputs[0].hashType.should.equal('pubkeyhash'); - outputs[0].hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); - outputs[0].outputIndex.should.equal(1); - outputs[0].satoshis.should.equal(4527773864); - outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); - outputs[0].height.should.equal(15); - done(); - }); - createReadStreamCallCount.should.equal(1); - var data = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') - }; - testStream.emit('data', data); - testStream.push(null); - }); - - it('should get outputs for an address', function(done) { - var readStream1 = new stream.Readable(); - readStream1._read = function() { /* do nothing */ }; - am.node.services.db.store = { - createReadStream: sinon.stub().returns(readStream1) - }; - - am._getOutputsMempool = sinon.stub().callsArgWith(3, null, [ - { - address: address, - height: -1, - hashType: 'pubkeyhash', - confirmations: 0, - txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371', - satoshis: 307627737, - script: '76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac', - outputIndex: 0 - } - ]); - - am.getOutputs(address, options, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(3); - outputs[0].address.should.equal(address); - outputs[0].hashType.should.equal('pubkeyhash'); - outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); - outputs[0].outputIndex.should.equal(1); - outputs[0].satoshis.should.equal(4527773864); - outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); - outputs[0].height.should.equal(345000); - outputs[1].address.should.equal(address); - outputs[1].hashType.should.equal('pubkeyhash'); - outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); - outputs[1].outputIndex.should.equal(2); - outputs[1].satoshis.should.equal(10000); - outputs[1].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); - outputs[1].height.should.equal(345004); - outputs[2].address.should.equal(address); - outputs[2].hashType.should.equal('pubkeyhash'); - outputs[2].txid.should.equal('aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371'); - outputs[2].script.should.equal('76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac'); - outputs[2].height.should.equal(-1); - outputs[2].confirmations.should.equal(0); - done(); - }); - - var data1 = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b6870100000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') - }; - - var data2 = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b6870100000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'), - value: new Buffer('40c388000000000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') - }; - - readStream1.emit('data', data1); - readStream1.emit('data', data2); - readStream1.push(null); - }); - - it('should give an error if the readstream has an error', function(done) { - var readStream2 = new stream.Readable(); - readStream2._read = function() { /* do nothing */ }; - am.node.services.db.store = { - createReadStream: sinon.stub().returns(readStream2) - }; - - am.getOutputs(address, options, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - - readStream2.emit('error', new Error('readstreamerror')); - setImmediate(function() { - readStream2.push(null); - }); - }); - - it('should print outputs for a p2sh address', function(done) { - // This address has the redeemScript 0x038a213afdfc551fc658e9a2a58a86e98d69b687, - // which is the same as the pkhash for the address 1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W. - // See https://github.com/bitpay/bitcore-node/issues/377 - var address = '321jRYeWBrLBWr2j1KYnAFGico3GUdd5q7'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.REDEEMSCRIPT; - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - start: 15, - end: 12, - queryMempool: true - }; - var createReadStreamCallCount = 0; - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); - am.getOutputs(address, args, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(1); - outputs[0].address.should.equal(address); - outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); - outputs[0].hashType.should.equal('scripthash'); - outputs[0].hashType.should.equal(constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); - outputs[0].outputIndex.should.equal(1); - outputs[0].satoshis.should.equal(4527773864); - outputs[0].script.should.equal('a914038a213afdfc551fc658e9a2a58a86e98d69b68787'); - outputs[0].height.should.equal(15); - done(); - }); - createReadStreamCallCount.should.equal(1); - var data = { - // note '68702', '02' meaning p2sh redeemScript, not p2pkh - // value is also the p2sh script, not p2pkh - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68702000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), - value: new Buffer('41f0de058a800000a914038a213afdfc551fc658e9a2a58a86e98d69b68787', 'hex') - }; - testStream.emit('data', data); - testStream.push(null); - }); - - it('should not print outputs for a p2pkh address, if the output was sent to a p2sh redeemScript', function(done) { - // This address has the redeemScript 0x038a213afdfc551fc658e9a2a58a86e98d69b687, - // which is the same as the pkhash for the address 1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W. - // See https://github.com/bitpay/bitcore-node/issues/377 - var address = '321jRYeWBrLBWr2j1KYnAFGico3GUdd5q7'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.REDEEMSCRIPT; - var testStream = new stream.Readable(); - testStream._read = function() { /* do nothing */ }; - var args = { - start: 15, - end: 12, - queryMempool: true - }; - var createReadStreamCallCount = 0; - - // Verifying that the db query is looking for a redeemScript, *not* a p2pkh - am.node.services.db.store = { - createReadStream: function(ops) { - var gt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); - ops.gt.toString('hex').should.equal(gt.toString('hex')); - var lt = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); - ops.lt.toString('hex').should.equal(lt.toString('hex')); - createReadStreamCallCount++; - return testStream; - } - }; - am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); - am.getOutputs(address, args, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(0); - done(); - }); - createReadStreamCallCount.should.equal(1); - testStream.push(null); - }); - }); - - describe('#_getOutputsMempool', function() { - var am; - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - var hashBuffer = bitcore.Address(address).hashBuffer; - var hashTypeBuffer = constants.HASH_TYPES.PUBKEY; - var db = { - tip: { - __height: 1 - } - }; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - }); - it('it will handle error', function(done) { - var testStream = new EventEmitter(); - am.mempoolIndex = {}; - am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - am._getOutputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - testStream.emit('error', new Error('readstreamerror')); - setImmediate(function() { - testStream.emit('close'); - }); - }); - it('it will parse data', function(done) { - var testStream = new EventEmitter(); - am.mempoolIndex = {}; - am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - - am._getOutputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { - if (err) { - throw err; - } - outputs.length.should.equal(1); - var output = outputs[0]; - output.address.should.equal(address); - output.hashType.should.equal('pubkeyhash'); - output.txid.should.equal(txid); - output.outputIndex.should.equal(outputIndex); - output.height.should.equal(-1); - output.satoshis.should.equal(3); - output.script.should.equal('ac'); - output.timestamp.should.equal(1452696715750); - output.confirmations.should.equal(0); - done(); - }); - - var txid = '5d32f0fff6871c377e00c16f48ebb5e89c723d0b9dd25f68fdda70c3392bee61'; - var txidBuffer = new Buffer(txid, 'hex'); - var outputIndex = 3; - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var keyData = Buffer.concat([ - constants.MEMPREFIXES.OUTPUTS, - hashBuffer, - hashTypeBuffer, - txidBuffer, - outputIndexBuffer - ]); - - var valueData = Buffer.concat([ - new Buffer('4008000000000000', 'hex'), - new Buffer('427523b78c1e6000', 'hex'), - new Buffer('ac', 'hex') - ]); - - // Note: key is not used currently - testStream.emit('data', { - key: keyData, - value: valueData - }); - setImmediate(function() { - testStream.emit('close'); - }); - }); - }); - - describe('#getUnspentOutputs', function() { - it('should concatenate utxos for multiple addresses, even those with none found', function(done) { - var addresses = { - 'addr1': ['utxo1', 'utxo2'], - 'addr2': new errors.NoOutputs(), - 'addr3': ['utxo3'] - }; - - var db = {}; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - services: { - db: db, - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { - var result = addresses[address]; - if(result instanceof Error) { - return callback(result); - } else { - return callback(null, result); - } - }; - - am.getUnspentOutputs(['addr1', 'addr2', 'addr3'], true, function(err, utxos) { - should.not.exist(err); - utxos.should.deep.equal(['utxo1', 'utxo2', 'utxo3']); - done(); - }); - }); - it('should give an error if an error occurred', function(done) { - var addresses = { - 'addr1': ['utxo1', 'utxo2'], - 'addr2': new Error('weird error'), - 'addr3': ['utxo3'] - }; - - var db = {}; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - db: db, - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { - var result = addresses[address]; - if(result instanceof Error) { - return callback(result); - } else { - return callback(null, result); - } - }; - - am.getUnspentOutputs(['addr1', 'addr2', 'addr3'], true, function(err, utxos) { - should.exist(err); - err.message.should.equal('weird error'); - done(); - }); - }); - - it('should also work for a single address', function(done) { - var addresses = { - 'addr1': ['utxo1', 'utxo2'], - 'addr2': new Error('weird error'), - 'addr3': ['utxo3'] - }; - - var db = {}; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - db: db, - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.getUnspentOutputsForAddress = function(address, queryMempool, callback) { - var result = addresses[address]; - if(result instanceof Error) { - return callback(result); - } else { - return callback(null, result); - } - }; - - am.getUnspentOutputs('addr1', true, function(err, utxos) { - should.not.exist(err); - utxos.should.deep.equal(['utxo1', 'utxo2']); - done(); - }); - }); - }); - - describe('#getUnspentOutputsForAddress', function() { - it('should filter out spent outputs', function(done) { - var outputs = [ - { - satoshis: 1000, - spent: false, - }, - { - satoshis: 2000, - spent: true - }, - { - satoshis: 3000, - spent: false - } - ]; - var i = 0; - - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getOutputs = sinon.stub().callsArgWith(2, null, outputs); - am.isUnspent = function(output, options, callback) { - callback(!outputs[i].spent); - i++; - }; - - am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(2); - outputs[0].satoshis.should.equal(1000); - outputs[1].satoshis.should.equal(3000); - done(); - }); - }); - it('should handle an error from getOutputs', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getOutputs = sinon.stub().callsArgWith(2, new Error('error')); - am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - it('should handle when there are no outputs', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getOutputs = sinon.stub().callsArgWith(2, null, []); - am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.exist(err); - err.should.be.instanceof(errors.NoOutputs); - outputs.length.should.equal(0); - done(); - }); - }); - }); - - describe('#isUnspent', function() { - var am; - - before(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - }); - - it('should give true when isSpent() gives false', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, false); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {}, function(unspent) { - unspent.should.equal(true); - done(); - }); - }); - - it('should give false when isSpent() gives true', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, true); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {},function(unspent) { - unspent.should.equal(false); - done(); - }); - }); - - it('should give false when isSpent() returns an error', function(done) { - am.isSpent = sinon.stub().callsArgWith(2, new Error('error')); - am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', {}, function(unspent) { - unspent.should.equal(false); - done(); - }); - }); - }); - - describe('#isSpent', function() { - var db = {}; - var testnode = { - network: Networks.testnet, - datadir: 'testdir', - db: db, - services: { - bitcoind: { - on: sinon.stub() - } - } - }; - it('should give true if bitcoind.isSpent gives true (with output info)', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var isSpent = sinon.stub().returns(true); - am.node.services.bitcoind = { - isSpent: isSpent, - on: sinon.stub() - }; - var output = { - txid: '4228d3f41051f914f71a1dcbbe4098e29a07cc2672fdadab0763d88ffd6ffa57', - outputIndex: 3 - }; - am.isSpent(output, {}, function(spent) { - isSpent.callCount.should.equal(1); - isSpent.args[0][0].should.equal(output.txid); - isSpent.args[0][1].should.equal(output.outputIndex); - spent.should.equal(true); - done(); - }); - }); - it('should give true if bitcoind.isSpent gives true (with input)', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var isSpent = sinon.stub().returns(true); - am.node.services.bitcoind = { - isSpent: isSpent, - on: sinon.stub() - }; - var txid = '4228d3f41051f914f71a1dcbbe4098e29a07cc2672fdadab0763d88ffd6ffa57'; - var output = { - prevTxId: new Buffer(txid, 'hex'), - outputIndex: 4 - }; - am.isSpent(output, {}, function(spent) { - isSpent.callCount.should.equal(1); - isSpent.args[0][0].should.equal(txid); - isSpent.args[0][1].should.equal(output.outputIndex); - spent.should.equal(true); - done(); - }); - }); - it('should give true if bitcoind.isSpent is false and mempoolSpentIndex is true', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.node.services.bitcoind = { - isSpent: sinon.stub().returns(false), - on: sinon.stub() - }; - var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'; - var outputIndex = 0; - var output = { - prevTxId: new Buffer(txid, 'hex'), - outputIndex: outputIndex - }; - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var spentKey = Buffer.concat([ - new Buffer(txid, 'hex'), - outputIndexBuffer - ]).toString('binary'); - am.mempoolSpentIndex[spentKey] = true; - am.isSpent(output, {queryMempool: true}, function(spent) { - spent.should.equal(true); - done(); - }); - }); - it('should give false if spent in mempool with queryMempool set to false', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.node.services.bitcoind = { - isSpent: sinon.stub().returns(false), - on: sinon.stub() - }; - var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'; - var outputIndex = 0; - var output = { - prevTxId: new Buffer(txid, 'hex'), - outputIndex: outputIndex - }; - var spentKey = [txid, outputIndex].join('-'); - am.mempoolSpentIndex[spentKey] = new Buffer(5); - am.isSpent(output, {queryMempool: false}, function(spent) { - spent.should.equal(false); - done(); - }); - }); - it('default to querying the mempool', function(done) { - var am = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - am.node.services.bitcoind = { - isSpent: sinon.stub().returns(false), - on: sinon.stub() - }; - var txidBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex'); - var outputIndex = 0; - var output = { - prevTxId: txidBuffer, - outputIndex: outputIndex - }; - var outputIndexBuffer = new Buffer(4); - outputIndexBuffer.writeUInt32BE(outputIndex); - var spentKey = Buffer.concat([ - txidBuffer, - outputIndexBuffer - ]).toString('binary'); - am.mempoolSpentIndex[spentKey] = true; - am.isSpent(output, {}, function(spent) { - spent.should.equal(true); - done(); - }); - }); - }); - - describe('#getAddressHistory', function() { - it('will call get on address history instance', function(done) { - function TestAddressHistory(args) { - args.node.should.equal(mocknode); - args.addresses.should.deep.equal([]); - args.options.should.deep.equal({}); - } - TestAddressHistory.prototype.get = sinon.stub().callsArg(0); - var TestAddressService = proxyquire('../../../lib/services/address', { - './history': TestAddressHistory - }); - var am = new TestAddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - am.getAddressHistory([], {}, function(err, history) { - TestAddressHistory.prototype.get.callCount.should.equal(1); - done(); - }); - }); - }); - describe('#updateMempoolIndex/#removeMempoolIndex', function() { - var am; - var tx = Transaction().fromBuffer(txBuf); - var clock; - - beforeEach(function() { - am = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - clock = sinon.useFakeTimers(); - }); - - afterEach(function() { - clock.restore(); - }); - - it('will update the input and output indexes', function() { - am.mempoolIndex = {}; - am.mempoolIndex.batch = function(operations, callback) { - callback.should.be.a('function'); - Object.keys(am.mempoolSpentIndex).length.should.equal(14); - Object.keys(am.mempoolAddressIndex).length.should.equal(5); - _.values(am.mempoolAddressIndex).should.deep.equal([1,1,12,1,1]); - for (var i = 0; i < operations.length; i++) { - operations[i].type.should.equal('put'); - } - var nowTime = new Date().getTime(); - var nowTimeBuffer = new Buffer(8); - nowTimeBuffer.writeDoubleBE(nowTime); - var expectedValue = '45202ffdeb8344af4dec07cddf0478485dc65cc7d08303e45959630c89b51ea200000002' + - nowTimeBuffer.toString('hex'); - operations[7].value.toString('hex').should.equal(expectedValue); - var matches = 0; - - - for (var j = 0; j < operations.length; j++) { - var match = Buffer.concat([ - constants.MEMPREFIXES.SPENTS, - bitcore.Address('1JT7KDYwT9JY9o2vyqcKNSJgTWeKfV3ui8').hashBuffer - ]).toString('hex'); - - if (operations[j].key.slice(0, 21).toString('hex') === match) { - matches++; - } - } - matches.should.equal(12); - }; - am.updateMempoolIndex(tx, true); - }); - - it('will remove the input and output indexes', function() { - am.mempoolIndex = {}; - am.mempoolIndex.batch = function(operations, callback) { - callback.should.be.a('function'); - Object.keys(am.mempoolSpentIndex).length.should.equal(0); - for (var i = 0; i < operations.length; i++) { - operations[i].type.should.equal('del'); - } - Object.keys(am.mempoolAddressIndex).length.should.equal(0); - }; - am.updateMempoolIndex(tx, false); - }); - - }); - - describe('#getAddressSummary', function() { - var clock; - beforeEach(function() { - clock = sinon.useFakeTimers(); - sinon.stub(log, 'warn'); - }); - afterEach(function() { - clock.restore(); - log.warn.restore(); - }); - it('will handle error from _getAddressConfirmedSummary', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var options = {}; - addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, new Error('test')); - addressService.getAddressSummary(address, options, function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - it('will handle error from _getAddressMempoolSummary', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var options = {}; - addressService._getAddressConfirmedSummary = sinon.stub().callsArg(2); - addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(2, new Error('test2')); - addressService.getAddressSummary(address, options, function(err) { - should.exist(err); - err.message.should.equal('test2'); - done(); - }); - }); - it('will pass cache and summary between functions correctly', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var options = {}; - var cache = {}; - var summary = {}; - addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); - addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(3, null, cache); - addressService._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache); - addressService._transformAddressSummaryFromResult = sinon.stub().returns(summary); - addressService.getAddressSummary(address, options, function(err, sum) { - addressService._getAddressConfirmedSummary.callCount.should.equal(1); - addressService._getAddressMempoolSummary.callCount.should.equal(1); - addressService._getAddressMempoolSummary.args[0][2].should.equal(cache); - addressService._setAndSortTxidsFromAppearanceIds.callCount.should.equal(1); - addressService._setAndSortTxidsFromAppearanceIds.args[0][0].should.equal(cache); - addressService._transformAddressSummaryFromResult.callCount.should.equal(1); - addressService._transformAddressSummaryFromResult.args[0][0].should.equal(cache); - sum.should.equal(summary); - done(); - }); - }); - it('will log if there is a slow query', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var addressService = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var options = {}; - var cache = {}; - var summary = {}; - addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); - addressService._getAddressConfirmedSummary = sinon.stub().callsArgWith(2, null, cache); - addressService._getAddressMempoolSummary = sinon.stub().callsArgWith(3, null, cache); - addressService._setAndSortTxidsFromAppearanceIds = sinon.stub().callsArgWith(1, null, cache); - addressService._transformAddressSummaryFromResult = sinon.stub().returns(summary); - addressService.getAddressSummary(address, options, function() { - log.warn.callCount.should.equal(1); - done(); - }); - clock.tick(6000); - }); - }); - - describe('#_getAddressConfirmedSummary', function() { - it('will pass arguments correctly', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = {}; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = {}; - as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, result); - as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, null, result); - as._getAddressConfirmedSummary(address, options, function(err) { - if (err) { - return done(err); - } - var expectedResult = { - appearanceIds: {}, - totalReceived: 0, - balance: 0, - unconfirmedAppearanceIds: {}, - unconfirmedBalance: 0 - }; - as._getAddressConfirmedInputsSummary.args[0][0].should.equal(address); - as._getAddressConfirmedInputsSummary.args[0][1].should.deep.equal(expectedResult); - as._getAddressConfirmedInputsSummary.args[0][2].should.deep.equal(options); - as._getAddressConfirmedOutputsSummary.args[0][0].should.equal(address); - as._getAddressConfirmedOutputsSummary.args[0][1].should.deep.equal(result); - as._getAddressConfirmedOutputsSummary.args[0][2].should.equal(options); - done(); - }); - }); - it('will pass error correctly (inputs)', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = {}; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = {}; - as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, new Error('test')); - as._getAddressConfirmedSummary(address, options, function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - it('will pass error correctly (outputs)', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = {}; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = {}; - as._getAddressConfirmedInputsSummary = sinon.stub().callsArgWith(3, null, result); - as._getAddressConfirmedOutputsSummary = sinon.stub().callsArgWith(3, new Error('test')); - as._getAddressConfirmedSummary(address, options, function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - }); - - describe('#_getAddressConfirmedInputsSummary', function() { - it('will stream inputs and collect txids', function(done) { - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = { - appearanceIds: {} - }; - var options = {}; - var txid = 'f2cfc19d13f0c12199f70e420d84e2b3b1d4e499702aa9d737f8c24559c9ec47'; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - as.createInputsStream = sinon.stub().returns(streamStub); - as._getAddressConfirmedInputsSummary(address, result, options, function(err, result) { - if (err) { - return done(err); - } - result.appearanceIds[txid].should.equal(10); - done(); - }); - - streamStub.emit('data', { - txid: txid, - height: 10 - }); - streamStub.push(null); - }); - it('handle stream error', function(done) { - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var cache = {}; - var options = {}; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - as.createInputsStream = sinon.stub().returns(streamStub); - as._getAddressConfirmedInputsSummary(address, cache, options, function(err, cache) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - - streamStub.emit('error', new Error('test')); - streamStub.push(null); - }); - }); - - describe('#_getAddressConfirmedOutputsSummary', function() { - it('will stream inputs and collect txids', function(done) { - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub(), - isSpent: sinon.stub().returns(false) - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = { - appearanceIds: {}, - unconfirmedAppearanceIds: {}, - balance: 0, - totalReceived: 0, - unconfirmedBalance: 0 - }; - - var options = { - queryMempool: true - }; - var txid = 'f2cfc19d13f0c12199f70e420d84e2b3b1d4e499702aa9d737f8c24559c9ec47'; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - - as.createOutputsStream = sinon.stub().returns(streamStub); - - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(new Buffer(txid, 'hex'), 2); - as.mempoolSpentIndex[spentIndexSyncKey] = true; - - as._getAddressConfirmedOutputsSummary(address, result, options, function(err, cache) { - if (err) { - return done(err); - } - result.appearanceIds[txid].should.equal(10); - result.balance.should.equal(1000); - result.totalReceived.should.equal(1000); - result.unconfirmedBalance.should.equal(-1000); - done(); - }); - - streamStub.emit('data', { - txid: txid, - height: 10, - outputIndex: 2, - satoshis: 1000 - }); - streamStub.push(null); - }); - it('handle stream error', function(done) { - var streamStub = new stream.Readable(); - streamStub._read = function() { /* do nothing */ }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = { - appearanceIds: {}, - unconfirmedAppearanceIds: {}, - balance: 0, - totalReceived: 0, - unconfirmedBalance: 0 - }; - - var options = {}; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - as.createOutputsStream = sinon.stub().returns(streamStub); - as._getAddressConfirmedOutputsSummary(address, result, options, function(err, cache) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - - streamStub.emit('error', new Error('test')); - streamStub.push(null); - }); - }); - - describe('#_setAndSortTxidsFromAppearanceIds', function() { - it('will sort correctly', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var result = { - appearanceIds: { - '22488dbb99aed86e7081ac480e3459fa40ccab7ee18bef98b84b3cdce6bf05be': 200, - '1c413601acbd608240fc635b95886c3c1f76ec8589c3392a58b5715ceb618e93': 100, - '206d3834c010d46a2cf478cb1c5fe252be41f683c8a738e3ebe27f1aae67f505': 101 - }, - unconfirmedAppearanceIds: { - 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681': 1452898347406, - 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693': 1452898331964, - 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345': 1452897902377, - 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a': 1452897971363, - 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9': 1452897923107 - } - }; - as._setAndSortTxidsFromAppearanceIds(result, function(err, result) { - if (err) { - return done(err); - } - should.exist(result.txids); - result.txids[0].should.equal('1c413601acbd608240fc635b95886c3c1f76ec8589c3392a58b5715ceb618e93'); - result.txids[1].should.equal('206d3834c010d46a2cf478cb1c5fe252be41f683c8a738e3ebe27f1aae67f505'); - result.txids[2].should.equal('22488dbb99aed86e7081ac480e3459fa40ccab7ee18bef98b84b3cdce6bf05be'); - result.unconfirmedTxids[0].should.equal('f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345'); - result.unconfirmedTxids[1].should.equal('f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9'); - result.unconfirmedTxids[2].should.equal('edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a'); - result.unconfirmedTxids[3].should.equal('ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693'); - result.unconfirmedTxids[4].should.equal('ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681'); - done(); - }); - }); - }); - - - describe('#_updateAddressIndex', function() { - it('should add using 2 keys', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - - _.values(as.mempoolAddressIndex).should.deep.equal([]); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index2', true); - as._updateAddressIndex('index2', true); - as.mempoolAddressIndex.should.deep.equal({ - "index1": 4, - "index2": 2 - }); - }); - - it('should add/remove using 2 keys', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: mocknode - }); - _.values(as.mempoolAddressIndex).should.deep.equal([]); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', true); - as._updateAddressIndex('index1', false); - - as._updateAddressIndex('index2', true); - as._updateAddressIndex('index2', true); - as._updateAddressIndex('index2', false); - as._updateAddressIndex('index2', false); - as.mempoolAddressIndex.should.deep.equal({ - "index1": 3 - }); - as._updateAddressIndex('index2', false); - as.mempoolAddressIndex.should.deep.equal({ - "index1": 3 - }); - }); - }); - - - describe('#_getAddressMempoolSummary', function() { - it('skip if options not enabled', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var resultBase = { - unconfirmedAppearanceIds: {}, - unconfirmedBalance: 0 - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = {}; - as._getAddressMempoolSummary(address, options, resultBase, function(err, result) { - if (err) { - return done(err); - } - Object.keys(result.unconfirmedAppearanceIds).length.should.equal(0); - result.unconfirmedBalance.should.equal(0); - done(); - }); - }); - it('include all txids and balance from inputs and outputs', function(done) { - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var resultBase = { - unconfirmedAppearanceIds: {}, - unconfirmedBalance: 0 - }; - var address = new bitcore.Address('12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'); - var options = { - queryMempool: true - }; - var mempoolInputs = [ - { - address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', - hashType: 'scripthash', - txid: '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', - inputIndex: 0, - timestamp: 1452874536321, - height: -1, - confirmations: 0 - } - ]; - var mempoolOutputs = [ - { - address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', - hashType: 'scripthash', - txid: '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d', - outputIndex: 0, - height: -1, - timestamp: 1452874521466, - satoshis: 131368318, - script: '76a9148c66db6e9f74b1db9c400eaa2aed3743417f38e688ac', - confirmations: 0 - }, - { - address: '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj', - hashType: 'scripthash', - txid: '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247', - outputIndex: 0, - height: -1, - timestamp: 1452874521466, - satoshis: 131368318, - script: '76a9148c66db6e9f74b1db9c400eaa2aed3743417f38e688ac', - confirmations: 0 - } - ]; - var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey( - new Buffer(mempoolOutputs[1].txid, 'hex'), - 0 - ); - as.mempoolSpentIndex[spentIndexSyncKey] = true; - - var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; - var addressIndex = encoding.encodeMempoolAddressIndexKey(address.hashBuffer, hashTypeBuffer); - as.mempoolAddressIndex[addressIndex] = 1; - - as._getInputsMempool = sinon.stub().callsArgWith(3, null, mempoolInputs); - as._getOutputsMempool = sinon.stub().callsArgWith(3, null, mempoolOutputs); - as._getAddressMempoolSummary(address, options, resultBase, function(err, result) { - if (err) { - return done(err); - } - var txid1 = '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8'; - var txid2 = '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d'; - var txid3 = '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247'; - result.unconfirmedAppearanceIds[txid1].should.equal(1452874536321); - result.unconfirmedAppearanceIds[txid2].should.equal(1452874521466); - result.unconfirmedAppearanceIds[txid3].should.equal(1452874521466); - result.unconfirmedBalance.should.equal(131368318); - done(); - }); - }); - }); - - describe('#_transformAddressSummaryFromResult', function() { - var result = { - totalReceived: 1000000, - balance: 500000, - txids: [ - '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', - 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92' - ], - appearanceIds: { - 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92': 100000, - '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8': 200000 - }, - unconfirmedAppearanceIds: { - '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d': 1452874536321, - '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247': 1452874521466 - }, - unconfirmedTxids: [ - '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247', - '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d' - ], - unconfirmedBalance: 500000 - }; - var testnode = { - network: Networks.testnet, - services: { - bitcoind: { - on: sinon.stub() - } - }, - datadir: 'testdir' - }; - it('will transform result into summary', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = {}; - var summary = as._transformAddressSummaryFromResult(result, options); - summary.totalReceived.should.equal(1000000); - summary.totalSpent.should.equal(500000); - summary.balance.should.equal(500000); - summary.appearances.should.equal(2); - summary.unconfirmedAppearances.should.equal(2); - summary.unconfirmedBalance.should.equal(500000); - summary.txids.length.should.equal(4); - }); - it('will omit txlist', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = { - noTxList: true - }; - var summary = as._transformAddressSummaryFromResult(result, options); - should.not.exist(summary.txids); - }); - it('will include full appearance ids', function() { - var as = new AddressService({ - mempoolMemoryIndex: true, - node: testnode - }); - var options = { - fullTxList: true - }; - var summary = as._transformAddressSummaryFromResult(result, options); - should.exist(summary.appearanceIds); - should.exist(summary.unconfirmedAppearanceIds); - }); - }); - -}); diff --git a/test/services/bitcoind.unit.js b/test/services/bitcoind.unit.js index 47156b50..f80b5f60 100644 --- a/test/services/bitcoind.unit.js +++ b/test/services/bitcoind.unit.js @@ -1,26 +1,41 @@ 'use strict'; +var path = require('path'); +var EventEmitter = require('events').EventEmitter; var should = require('chai').should(); +var crypto = require('crypto'); +var bitcore = require('bitcore-lib'); var sinon = require('sinon'); var proxyquire = require('proxyquire'); var fs = require('fs'); var sinon = require('sinon'); -var readFileSync = sinon.stub().returns(fs.readFileSync(__dirname + '/../data/bitcoin.conf')); + +var index = require('../../lib'); +var log = index.log; +var errors = index.errors; + +var Transaction = require('../../lib/transaction'); +var readFileSync = sinon.stub().returns(fs.readFileSync(path.resolve(__dirname, '../data/bitcoin.conf'))); var BitcoinService = proxyquire('../../lib/services/bitcoind', { fs: { readFileSync: readFileSync } }); +var defaultBitcoinConf = fs.readFileSync(path.resolve(__dirname, '../data/default.bitcoin.conf'), 'utf8'); describe('Bitcoin Service', function() { + var txhex = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000'; + var baseConfig = { node: { + network: bitcore.Networks.testnet + }, + spawn: { datadir: 'testdir', - network: { - name: 'regtest' - } + exec: 'testpath' } }; + describe('@constructor', function() { it('will create an instance', function() { var bitcoind = new BitcoinService(baseConfig); @@ -30,34 +45,172 @@ describe('Bitcoin Service', function() { var bitcoind = BitcoinService(baseConfig); should.exist(bitcoind); }); + it('will init caches', function() { + var bitcoind = new BitcoinService(baseConfig); + should.exist(bitcoind.utxosCache); + should.exist(bitcoind.txidsCache); + should.exist(bitcoind.balanceCache); + should.exist(bitcoind.summaryCache); + should.exist(bitcoind.transactionInfoCache); + + should.exist(bitcoind.transactionCache); + should.exist(bitcoind.rawTransactionCache); + should.exist(bitcoind.blockCache); + should.exist(bitcoind.rawBlockCache); + should.exist(bitcoind.blockHeaderCache); + should.exist(bitcoind.zmqKnownTransactions); + should.exist(bitcoind.zmqKnownBlocks); + should.exist(bitcoind.lastTip); + should.exist(bitcoind.lastTipTimeout); + }); + it('will init clients', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.should.deep.equal([]); + bitcoind.nodesIndex.should.equal(0); + bitcoind.nodes.push({client: sinon.stub()}); + should.exist(bitcoind.client); + }); + it('will set subscriptions', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.subscriptions.should.deep.equal({ + transaction: [], + block: [] + }); + }); }); + describe('@dependencies', function() { it('will have no dependencies', function() { BitcoinService.dependencies.should.deep.equal([]); }); }); - describe('#_loadConfiguration', function() { + + describe('#getAPIMethods', function() { + it('will return spec', function() { + var bitcoind = new BitcoinService(baseConfig); + var methods = bitcoind.getAPIMethods(); + should.exist(methods); + methods.length.should.equal(20); + }); + }); + + describe('#getPublishEvents', function() { + it('will return spec', function() { + var bitcoind = new BitcoinService(baseConfig); + var events = bitcoind.getPublishEvents(); + should.exist(events); + events.length.should.equal(2); + events[0].name.should.equal('bitcoind/transaction'); + events[0].scope.should.equal(bitcoind); + events[0].subscribe.should.be.a('function'); + events[0].unsubscribe.should.be.a('function'); + events[1].name.should.equal('bitcoind/block'); + events[1].scope.should.equal(bitcoind); + events[1].subscribe.should.be.a('function'); + events[1].unsubscribe.should.be.a('function'); + }); + it('will call subscribe/unsubscribe with correct args', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.subscribe = sinon.stub(); + bitcoind.unsubscribe = sinon.stub(); + var events = bitcoind.getPublishEvents(); + + events[0].subscribe('test'); + bitcoind.subscribe.args[0][0].should.equal('transaction'); + bitcoind.subscribe.args[0][1].should.equal('test'); + + events[0].unsubscribe('test'); + bitcoind.unsubscribe.args[0][0].should.equal('transaction'); + bitcoind.unsubscribe.args[0][1].should.equal('test'); + + events[1].subscribe('test'); + bitcoind.subscribe.args[1][0].should.equal('block'); + bitcoind.subscribe.args[1][1].should.equal('test'); + + events[1].unsubscribe('test'); + bitcoind.unsubscribe.args[1][0].should.equal('block'); + bitcoind.unsubscribe.args[1][1].should.equal('test'); + }); + }); + + describe('#subscribe', function() { + it('will push to subscriptions', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter = {}; + bitcoind.subscribe('block', emitter); + bitcoind.subscriptions.block[0].should.equal(emitter); + + var emitter2 = {}; + bitcoind.subscribe('transaction', emitter2); + bitcoind.subscriptions.transaction[0].should.equal(emitter2); + }); + }); + + describe('#unsubscribe', function() { + it('will remove item from subscriptions', function() { + var bitcoind = new BitcoinService(baseConfig); + var emitter1 = {}; + var emitter2 = {}; + var emitter3 = {}; + var emitter4 = {}; + var emitter5 = {}; + bitcoind.subscribe('block', emitter1); + bitcoind.subscribe('block', emitter2); + bitcoind.subscribe('block', emitter3); + bitcoind.subscribe('block', emitter4); + bitcoind.subscribe('block', emitter5); + bitcoind.subscriptions.block.length.should.equal(5); + + bitcoind.unsubscribe('block', emitter3); + bitcoind.subscriptions.block.length.should.equal(4); + bitcoind.subscriptions.block[0].should.equal(emitter1); + bitcoind.subscriptions.block[1].should.equal(emitter2); + bitcoind.subscriptions.block[2].should.equal(emitter4); + bitcoind.subscriptions.block[3].should.equal(emitter5); + }); + }); + + describe('#_getDefaultConfig', function() { + it('will generate config file from defaults', function() { + var bitcoind = new BitcoinService(baseConfig); + var config = bitcoind._getDefaultConfig(); + config.should.equal(defaultBitcoinConf); + }); + }); + + describe('#_loadSpawnConfiguration', function() { it('will parse a bitcoin.conf file', function() { var TestBitcoin = proxyquire('../../lib/services/bitcoind', { fs: { readFileSync: readFileSync, - existsSync: sinon.stub().returns(true) + existsSync: sinon.stub().returns(true), + writeFileSync: sinon.stub() }, mkdirp: { sync: sinon.stub() } }); var bitcoind = new TestBitcoin(baseConfig); - bitcoind._loadConfiguration({datadir: process.env.HOME + '/.bitcoin'}); - should.exist(bitcoind.configuration); - bitcoind.configuration.should.deep.equal({ - server: 1, - whitelist: '127.0.0.1', - txindex: 1, + bitcoind._loadSpawnConfiguration({datadir: process.env.HOME + '/.bitcoin'}); + should.exist(bitcoind.spawn.config); + bitcoind.spawn.config.should.deep.equal({ + addressindex: 1, + checkblocks: 144, + dbcache: 8192, + maxuploadtarget: 1024, port: 20000, + rpcport: 50001, rpcallowip: '127.0.0.1', rpcuser: 'bitcoin', - rpcpassword: 'local321' + rpcpassword: 'local321', + server: 1, + spentindex: 1, + timestampindex: 1, + txindex: 1, + upnp: 0, + whitelist: '127.0.0.1', + zmqpubhashblock: 'tcp://127.0.0.1:28332', + zmqpubrawtx: 'tcp://127.0.0.1:28332' }); }); it('should throw an exception if txindex isn\'t enabled in the configuration', function() { @@ -72,12 +225,12 @@ describe('Bitcoin Service', function() { }); var bitcoind = new TestBitcoin(baseConfig); (function() { - bitcoind._loadConfiguration({datadir: './test'}); - }).should.throw('Txindex option'); + bitcoind._loadSpawnConfiguration({datadir: './test'}); + }).should.throw(bitcore.errors.InvalidState); }); - it('should set https options if node https options are set', function() { + it('should NOT set https options if node https options are set', function() { var writeFileSync = function(path, config) { - config.should.equal('whitelist=127.0.0.1\ntxindex=1\nrpcssl=1\nrpcsslprivatekeyfile=key.pem\nrpcsslcertificatechainfile=cert.pem\n'); + config.should.equal(defaultBitcoinConf); }; var TestBitcoin = proxyquire('../../lib/services/bitcoind', { fs: { @@ -91,7 +244,6 @@ describe('Bitcoin Service', function() { }); var config = { node: { - datadir: 'testdir', network: { name: 'regtest' }, @@ -100,348 +252,1997 @@ describe('Bitcoin Service', function() { key: 'key.pem', cert: 'cert.pem' } + }, + spawn: { + datadir: 'testdir', + exec: 'testexec' } }; var bitcoind = new TestBitcoin(config); - bitcoind._loadConfiguration({datadir: process.env.HOME + '/.bitcoin'}); - }); - describe('reindex', function() { - var log = require('../../lib/').log; - var stub; - beforeEach(function() { - stub = sinon.stub(log, 'warn'); - }); - after(function() { - stub.restore(); - }); - it('should warn the user if reindex is set to 1 in the bitcoin.conf file', function() { - var readFileSync = function() { - return "txindex=1\nreindex=1"; - }; - var testbitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync, - existsSync: sinon.stub().returns(true) - }, - mkdirp: { - sync: sinon.stub() - }, - }); - var bitcoind = new testbitcoin(baseConfig); - bitcoind._loadConfiguration(); - stub.callCount.should.equal(1); - }); + bitcoind._loadSpawnConfiguration({datadir: process.env.HOME + '/.bitcoin'}); }); }); - describe('#_registerEventHandlers', function() { - it('will emit tx with transactions from bindings', function(done) { - var transaction = {}; - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - onTipUpdate: sinon.stub(), - startTxMon: sinon.stub().callsArgWith(0, [transaction]), - startTxMonLeave: sinon.stub().callsArgWith(0, [transaction]) - }; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind.on('tx', function(tx) { - tx.should.equal(transaction); - done(); - }); - bitcoind._registerEventHandlers(); + + describe('#_checkConfigIndexes', function() { + var stub; + beforeEach(function() { + stub = sinon.stub(log, 'warn'); }); - it('will emit tip from bindings', function(done) { - var height = 1; - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - syncPercentage: function() { - return height * 10; - }, - onTipUpdate: function(callback) { - if (height >= 10) { - return callback(undefined); - } - setImmediate(function() { - callback(height++); - }); - }, - startTxMon: sinon.stub(), - startTxMonLeave: sinon.stub() - }; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - var tipCallCount = 0; - bitcoind.on('tip', function(height) { - should.exist(height); - tipCallCount++; - if (height === 9) { - tipCallCount.should.equal(9); - done(); - } - }); - bitcoind._registerEventHandlers(); + after(function() { + stub.restore(); }); - }); - describe('#_onReady', function(done) { - var genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); - it('will emit ready and set the height and genesisBuffer', function(done) { - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - onTipUpdate: sinon.stub(), - startTxMon: sinon.stub(), - getInfo: sinon.stub().returns({ - blocks: 101 - }), - getBlock: sinon.stub().callsArgWith(1, null, genesisBuffer) - }; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._registerEventHandlers = sinon.stub(); - var result = {}; - var readyCallCount = 0; - bitcoind.on('ready', function() { - readyCallCount++; - }); - bitcoind._onReady(result, function(err) { - if (err) { - throw err; - } - bitcoind._registerEventHandlers.callCount.should.equal(1); - readyCallCount.should.equal(1); - bitcoind.genesisBuffer.should.equal(genesisBuffer); - bitcoind.height.should.equal(101); - done(); - }); - }); - }); - describe('#start', function() { - it('call bindings start with the correct arguments', function(done) { - var startCallCount = 0; - var start = function(obj, cb) { - startCallCount++; - obj.datadir.should.equal('testdir'); - obj.network.should.equal('regtest'); - cb(); + it('should warn the user if reindex is set to 1 in the bitcoin.conf file', function() { + var bitcoind = new BitcoinService(baseConfig); + var config = { + txindex: 1, + addressindex: 1, + spentindex: 1, + server: 1, + zmqpubrawtx: 1, + zmqpubhashblock: 1, + reindex: 1 }; - var onBlocksReady = sinon.stub().callsArg(0); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - start: start, - onBlocksReady: onBlocksReady - }; + var node = {}; + bitcoind._checkConfigIndexes(config, node); + log.warn.callCount.should.equal(1); + node._reindex.should.equal(true); + }); + }); + + describe('#_resetCaches', function() { + it('will reset LRU caches', function() { + var bitcoind = new BitcoinService(baseConfig); + var keys = []; + for (var i = 0; i < 10; i++) { + keys.push(crypto.randomBytes(32)); + bitcoind.transactionInfoCache.set(keys[i], {}); + bitcoind.utxosCache.set(keys[i], {}); + bitcoind.txidsCache.set(keys[i], {}); + bitcoind.balanceCache.set(keys[i], {}); + bitcoind.summaryCache.set(keys[i], {}); + } + bitcoind._resetCaches(); + should.equal(bitcoind.transactionInfoCache.get(keys[0]), undefined); + should.equal(bitcoind.utxosCache.get(keys[0]), undefined); + should.equal(bitcoind.txidsCache.get(keys[0]), undefined); + should.equal(bitcoind.balanceCache.get(keys[0]), undefined); + should.equal(bitcoind.summaryCache.get(keys[0]), undefined); + }); + }); + + describe('#_tryAll', function() { + it('will retry the number of bitcoind nodes', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.tryAllInterval = 1; + bitcoind.nodes.push({}); + bitcoind.nodes.push({}); + bitcoind.nodes.push({}); + var count = 0; + var func = function(callback) { + count++; + if (count <= 2) { + callback(new Error('test')); + } else { + callback(); } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._loadConfiguration = sinon.stub(); - bitcoind._onReady = sinon.stub().callsArg(1); - bitcoind.start(function(err) { - should.not.exist(err); - bitcoind._loadConfiguration.callCount.should.equal(1); - startCallCount.should.equal(1); - onBlocksReady.callCount.should.equal(1); - bitcoind._onReady.callCount.should.equal(1); + }; + bitcoind._tryAll(function(next) { + func(next); + }, function() { + count.should.equal(3); done(); }); }); - it('will give an error from bindings.start', function(done) { - var start = sinon.stub().callsArgWith(1, new Error('test')); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - start: start - }; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._loadConfiguration = sinon.stub(); - bitcoind.start(function(err) { + it('will get error if all fail', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.tryAllInterval = 1; + bitcoind.nodes.push({}); + bitcoind.nodes.push({}); + bitcoind.nodes.push({}); + var count = 0; + var func = function(callback) { + count++; + callback(new Error('test')); + }; + bitcoind._tryAll(function(next) { + func(next); + }, function(err) { should.exist(err); err.message.should.equal('test'); + count.should.equal(3); done(); }); }); - it('will give an error from bindings.onBlocksReady', function(done) { - var start = sinon.stub().callsArgWith(1, null); - var onBlocksReady = sinon.stub().callsArgWith(0, new Error('test')); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - start: start, - onBlocksReady: onBlocksReady - }; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._onReady = sinon.stub().callsArg(1); - bitcoind._loadConfiguration = sinon.stub(); - bitcoind.start(function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); + }); + + describe('#_wrapRPCError', function() { + it('will convert bitcoind-rpc error object into JavaScript error', function() { + var bitcoind = new BitcoinService(baseConfig); + var error = bitcoind._wrapRPCError({message: 'Test error', code: -1}); + error.should.be.an.instanceof(errors.RPCError); + error.code.should.equal(-1); + error.message.should.equal('Test error'); }); - describe('reindex', function() { - var log = require('../../lib/').log; - var info; - beforeEach(function() { - info = sinon.stub(log, 'info'); - }); - afterEach(function() { - info.restore(); - }); - it('will wait for a reindex to complete before calling the callback.', function(done) { - var start = sinon.stub().callsArgWith(1, null); - var onBlocksReady = sinon.stub().callsArg(0); - var percentage = 98; - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync + }); + + describe('#_initChain', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('will set height and genesis buffer', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var genesisBuffer = new Buffer([]); + bitcoind.getRawBlock = sinon.stub().callsArgWith(1, null, genesisBuffer); + bitcoind.nodes.push({ + client: { + getBestBlockHash: function(callback) { + callback(null, { + result: 'bestblockhash' + }); }, - bindings: function(name) { - return { - start: start, - onBlocksReady: onBlocksReady, - syncPercentage: function() { - return percentage; - } - }; + getBlock: function(hash, callback) { + if (hash === 'bestblockhash') { + callback(null, { + result: { + height: 5000 + } + }); + } + }, + getBlockHash: function(num, callback) { + callback(null, { + result: 'genesishash' + }); } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind._reindex = true; - bitcoind._reindexWait = 1; - bitcoind._onReady = sinon.stub().callsArg(1); - bitcoind._loadConfiguration = sinon.stub(); - bitcoind.start(function() { - info.callCount.should.be.within(2,3); - bitcoind._reindex.should.be.false; + } + }); + bitcoind._initChain(function() { + log.info.callCount.should.equal(1); + bitcoind.getRawBlock.callCount.should.equal(1); + bitcoind.getRawBlock.args[0][0].should.equal('genesishash'); + bitcoind.height.should.equal(5000); + bitcoind.genesisBuffer.should.equal(genesisBuffer); + done(); + }); + }); + }); + + describe('#_getNetworkOption', function() { + afterEach(function() { + bitcore.Networks.disableRegtest(); + baseConfig.node.network = bitcore.Networks.testnet; + }); + it('return --testnet for testnet', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.node.network = bitcore.Networks.testnet; + bitcoind._getNetworkOption().should.equal('--testnet'); + }); + it('return --regtest for testnet', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.node.network = bitcore.Networks.testnet; + bitcore.Networks.enableRegtest(); + bitcoind._getNetworkOption().should.equal('--regtest'); + }); + it('return undefined for livenet', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.node.network = bitcore.Networks.livenet; + bitcore.Networks.enableRegtest(); + should.equal(bitcoind._getNetworkOption(), undefined); + }); + }); + + describe('#_zmqBlockHandler', function() { + it('will emit block', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + bitcoind._rapidProtectedUpdateTip = sinon.stub(); + bitcoind.on('block', function(block) { + block.should.equal(message); + done(); + }); + bitcoind._zmqBlockHandler(node, message); + }); + it('will not emit same block twice', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + bitcoind._rapidProtectedUpdateTip = sinon.stub(); + bitcoind.on('block', function(block) { + block.should.equal(message); + done(); + }); + bitcoind._zmqBlockHandler(node, message); + bitcoind._zmqBlockHandler(node, message); + }); + it('will call function to update tip', function() { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + bitcoind._rapidProtectedUpdateTip = sinon.stub(); + bitcoind._zmqBlockHandler(node, message); + bitcoind._rapidProtectedUpdateTip.callCount.should.equal(1); + bitcoind._rapidProtectedUpdateTip.args[0][0].should.equal(node); + bitcoind._rapidProtectedUpdateTip.args[0][1].should.equal(message); + }); + it('will emit to subscribers', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + bitcoind._rapidProtectedUpdateTip = sinon.stub(); + var emitter = new EventEmitter(); + bitcoind.subscriptions.block.push(emitter); + emitter.on('bitcoind/block', function(blockHash) { + blockHash.should.equal(message.toString('hex')); + done(); + }); + bitcoind._zmqBlockHandler(node, message); + }); + }); + + describe('#_rapidProtectedUpdateTip', function() { + it('will limit tip updates with rapid calls', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var callCount = 0; + bitcoind._updateTip = function() { + callCount++; + callCount.should.be.within(1, 2); + if (callCount > 1) { + done(); + } + }; + var node = {}; + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + var count = 0; + function repeat() { + bitcoind._rapidProtectedUpdateTip(node, message); + count++; + if (count < 50) { + repeat(); + } + } + repeat(); + }); + }); + + describe('#_updateTip', function() { + var sandbox = sinon.sandbox.create(); + var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex'); + beforeEach(function() { + sandbox.stub(log, 'error'); + sandbox.stub(log, 'info'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('log and emit rpc error from get block', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub(); + bitcoind.on('error', function(err) { + err.code.should.equal(-1); + err.message.should.equal('Test error'); + log.error.callCount.should.equal(1); + done(); + }); + var node = { + client: { + getBlock: sinon.stub().callsArgWith(1, {message: 'Test error', code: -1}) + } + }; + bitcoind._updateTip(node, message); + }); + it('emit synced if percentage is 100', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 100); + bitcoind.on('synced', function() { + done(); + }); + var node = { + client: { + getBlock: sinon.stub() + } + }; + bitcoind._updateTip(node, message); + }); + it('NOT emit synced if percentage is less than 100', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 99); + bitcoind.on('synced', function() { + throw new Error('Synced called'); + }); + var node = { + client: { + getBlock: sinon.stub() + } + }; + bitcoind._updateTip(node, message); + log.info.callCount.should.equal(1); + done(); + }); + it('log and emit error from syncPercentage', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, new Error('test')); + bitcoind.on('error', function(err) { + log.error.callCount.should.equal(1); + err.message.should.equal('test'); + done(); + }); + var node = { + client: { + getBlock: sinon.stub() + } + }; + bitcoind._updateTip(node, message); + }); + it('reset caches and set height', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub(); + bitcoind._resetCaches = sinon.stub(); + bitcoind.on('tip', function(height) { + bitcoind._resetCaches.callCount.should.equal(1); + height.should.equal(10); + bitcoind.height.should.equal(10); + done(); + }); + var node = { + client: { + getBlock: sinon.stub().callsArgWith(1, null, { + result: { + height: 10 + } + }) + } + }; + bitcoind._updateTip(node, message); + }); + it('will NOT update twice for the same hash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub(); + bitcoind._resetCaches = sinon.stub(); + bitcoind.on('tip', function() { + done(); + }); + var node = { + client: { + getBlock: sinon.stub().callsArgWith(1, null, { + result: { + height: 10 + } + }) + } + }; + bitcoind._updateTip(node, message); + bitcoind._updateTip(node, message); + }); + }); + + describe('#_zmqTransactionHandler', function() { + it('will emit to subscribers', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedBuffer = new Buffer('abcdef', 'hex'); + var emitter = new EventEmitter(); + bitcoind.subscriptions.transaction.push(emitter); + emitter.on('bitcoind/transaction', function(hex) { + hex.should.be.a('string'); + hex.should.equal(expectedBuffer.toString('hex')); + done(); + }); + var node = {}; + bitcoind._zmqTransactionHandler(node, expectedBuffer); + }); + it('will NOT emit to subscribers more than once for the same tx', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedBuffer = new Buffer('abcdef', 'hex'); + var emitter = new EventEmitter(); + bitcoind.subscriptions.transaction.push(emitter); + emitter.on('bitcoind/transaction', function() { + done(); + }); + var node = {}; + bitcoind._zmqTransactionHandler(node, expectedBuffer); + bitcoind._zmqTransactionHandler(node, expectedBuffer); + }); + it('will emit "tx" event', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedBuffer = new Buffer('abcdef', 'hex'); + bitcoind.on('tx', function(buffer) { + buffer.should.be.instanceof(Buffer); + buffer.toString('hex').should.equal(expectedBuffer.toString('hex')); + done(); + }); + var node = {}; + bitcoind._zmqTransactionHandler(node, expectedBuffer); + }); + it('will NOT emit "tx" event more than once for the same tx', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedBuffer = new Buffer('abcdef', 'hex'); + bitcoind.on('tx', function() { + done(); + }); + var node = {}; + bitcoind._zmqTransactionHandler(node, expectedBuffer); + bitcoind._zmqTransactionHandler(node, expectedBuffer); + }); + }); + + describe('#_subscribeZmqEvents', function() { + it('will call subscribe on zmq socket', function() { + var bitcoind = new BitcoinService(baseConfig); + var node = { + zmqSubSocket: { + subscribe: sinon.stub(), + on: sinon.stub() + } + }; + bitcoind._subscribeZmqEvents(node); + node.zmqSubSocket.subscribe.callCount.should.equal(2); + node.zmqSubSocket.subscribe.args[0][0].should.equal('hashblock'); + node.zmqSubSocket.subscribe.args[1][0].should.equal('rawtx'); + }); + it('will call relevant handler for rawtx topics', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._zmqTransactionHandler = sinon.stub(); + var node = { + zmqSubSocket: new EventEmitter() + }; + node.zmqSubSocket.subscribe = sinon.stub(); + bitcoind._subscribeZmqEvents(node); + node.zmqSubSocket.on('message', function() { + bitcoind._zmqTransactionHandler.callCount.should.equal(1); + done(); + }); + var topic = new Buffer('rawtx', 'utf8'); + var message = new Buffer('abcdef', 'hex'); + node.zmqSubSocket.emit('message', topic, message); + }); + it('will call relevant handler for hashblock topics', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._zmqBlockHandler = sinon.stub(); + var node = { + zmqSubSocket: new EventEmitter() + }; + node.zmqSubSocket.subscribe = sinon.stub(); + bitcoind._subscribeZmqEvents(node); + node.zmqSubSocket.on('message', function() { + bitcoind._zmqBlockHandler.callCount.should.equal(1); + done(); + }); + var topic = new Buffer('hashblock', 'utf8'); + var message = new Buffer('abcdef', 'hex'); + node.zmqSubSocket.emit('message', topic, message); + }); + }); + + describe('#_initZmqSubSocket', function() { + }); + + describe('#_checkReindex', function() { + var sandbox = sinon.sandbox.create(); + before(function() { + sandbox.stub(log, 'info'); + }); + after(function() { + sandbox.restore(); + }); + it('give error from client syncpercentage', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._reindexWait = 1; + var node = { + _reindex: true, + client: { + syncPercentage: sinon.stub().callsArgWith(0, {code: -1 , message: 'Test error'}) + } + }; + bitcoind._checkReindex(node, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('will wait until syncpercentage is 100 percent', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._reindexWait = 1; + var percent = 90; + var node = { + _reindex: true, + client: { + syncPercentage: function(callback) { + callback(null, percent++); + } + } + }; + bitcoind._checkReindex(node, function() { + node._reindex.should.equal(false); + log.info.callCount.should.equal(11); + done(); + }); + }); + }); + + describe('#_loadTipFromNode', function() { + }); + + describe('#_spawnChildProcess', function() { + }); + + describe('#_connectProcess', function() { + }); + + describe('#start', function() { + }); + + describe('#isSynced', function() { + it('will give error from syncPercentage', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, new Error('test')); + bitcoind.isSynced(function(err) { + should.exist(err); + err.message.should.equal('test'); + done(); + }); + }); + it('will give "true" if percentage is 100.00', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 100.00); + bitcoind.isSynced(function(err, synced) { + if (err) { + return done(err); + } + synced.should.equal(true); + done(); + }); + }); + it('will give "true" if percentage is 99.98', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 99.98); + bitcoind.isSynced(function(err, synced) { + if (err) { + return done(err); + } + synced.should.equal(true); + done(); + }); + }); + it('will give "false" if percentage is 99.49', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 99.49); + bitcoind.isSynced(function(err, synced) { + if (err) { + return done(err); + } + synced.should.equal(false); + done(); + }); + }); + it('will give "false" if percentage is 1', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.syncPercentage = sinon.stub().callsArgWith(0, null, 1); + bitcoind.isSynced(function(err, synced) { + if (err) { + return done(err); + } + synced.should.equal(false); + done(); + }); + }); + }); + + describe('#syncPercentage', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockchainInfo = sinon.stub().callsArgWith(0, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getBlockchainInfo: getBlockchainInfo + } + }); + bitcoind.syncPercentage(function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client getInfo and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockchainInfo = sinon.stub().callsArgWith(0, null, { + result: { + verificationprogress: '0.983821387' + } + }); + bitcoind.nodes.push({ + client: { + getBlockchainInfo: getBlockchainInfo + } + }); + bitcoind.syncPercentage(function(err, percentage) { + if (err) { + return done(err); + } + percentage.should.equal(98.3821387); + done(); + }); + }); + }); + + describe('#_normalizeAddressArg', function() { + it('will turn single address into array', function() { + var bitcoind = new BitcoinService(baseConfig); + var args = bitcoind._normalizeAddressArg('address'); + args.should.deep.equal(['address']); + }); + it('will keep an array as an array', function() { + var bitcoind = new BitcoinService(baseConfig); + var args = bitcoind._normalizeAddressArg(['address', 'address']); + args.should.deep.equal(['address', 'address']); + }); + }); + + describe('#getAddressBalance', function() { + }); + + describe('#getAddressUnspentOutputs', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressUtxos: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + var options = {}; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('will give results from client getaddressutxos', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 1, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 7679241, + height: 207111 + } + ]; + bitcoind.nodes.push({ + client: { + getAddressUtxos: sinon.stub().callsArgWith(1, null, { + result: expectedUtxos + }) + } + }); + var options = {}; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(1); + utxos.should.deep.equal(expectedUtxos); + done(); + }); + }); + it('will use cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var expectedUtxos = [ + { + address: '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo', + txid: '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0', + outputIndex: 1, + script: '76a914f399b4b8894f1153b96fce29f05e6e116eb4c21788ac', + satoshis: 7679241, + height: 207111 + } + ]; + var getAddressUtxos = sinon.stub().callsArgWith(1, null, { + result: expectedUtxos + }); + bitcoind.nodes.push({ + client: { + getAddressUtxos: getAddressUtxos + } + }); + var options = {}; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(1); + utxos.should.deep.equal(expectedUtxos); + getAddressUtxos.callCount.should.equal(1); + bitcoind.getAddressUnspentOutputs(address, options, function(err, utxos) { + if (err) { + return done(err); + } + utxos.length.should.equal(1); + utxos.should.deep.equal(expectedUtxos); + getAddressUtxos.callCount.should.equal(1); done(); }); - setTimeout(function() { - percentage = 100; - }, 2); }); }); }); - describe('#stop', function() { - it('will call bindings stop', function() { - var stop = sinon.stub().callsArgWith(0, null, 'status'); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - stop: stop - }; + + describe('#_getBalanceFromMempool', function() { + }); + + describe('#_getTxidsMempool', function() { + }); + + describe('#_getHeightRangeQuery', function() { + }); + + describe('#getAddressTxids', function() { + it('will give rpc error from mempool query', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) } }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind.stop(function(err, status) { - stop.callCount.should.equal(1); - should.not.exist(err); - }); - }); - it('will give an error from bindings stop', function() { - var stop = sinon.stub().callsArgWith(0, new Error('test')); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - return { - stop: stop - }; - } - }); - var bitcoind = new TestBitcoin(baseConfig); - bitcoind.stop(function(err) { - stop.callCount.should.equal(1); + var options = {}; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err) { should.exist(err); - err.message.should.equal('test'); + err.should.be.instanceof(errors.RPCError); + }); + }); + it('will give rpc error from txids query', function() { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressTxids: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + }); + }); + it('will get txid results', function(done) { + var expectedTxids = [ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + 'f637384e9f81f18767ea50e00bce58fc9848b6588a1130529eebba22a410155f', + 'f3c1ba3ef86a0420d6102e40e2cfc8682632ab95d09d86a27f5d466b9fa9da47', + '56fafeb01961831b926558d040c246b97709fd700adcaa916541270583e8e579', + 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2', + 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345', + 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9', + 'edc080f2084eed362aa488ccc873a24c378dc0979aa29b05767517b70569414a', + 'ed11a08e3102f9610bda44c80c46781d97936a4290691d87244b1b345b39a693', + 'ec94d845c603f292a93b7c829811ac624b76e52b351617ca5a758e9d61a11681' + ]; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressTxids: sinon.stub().callsArgWith(1, null, { + result: expectedTxids.reverse() + }) + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + txids.length.should.equal(expectedTxids.length); + txids.should.deep.equal(expectedTxids); + done(); + }); + }); + it('will get txid results from cache', function(done) { + var expectedTxids = [ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce' + ]; + var bitcoind = new BitcoinService(baseConfig); + var getAddressTxids = sinon.stub().callsArgWith(1, null, { + result: expectedTxids.reverse() + }); + bitcoind.nodes.push({ + client: { + getAddressTxids: getAddressTxids + } + }); + var options = { + queryMempool: false + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + txids.should.deep.equal(expectedTxids); + + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + txids.should.deep.equal(expectedTxids); + done(); + }); + }); + }); + it('will get txid results WITHOUT cache if rangeQuery and exclude mempool', function(done) { + var expectedTxids = [ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce' + ]; + var bitcoind = new BitcoinService(baseConfig); + var getAddressMempool = sinon.stub(); + var getAddressTxids = sinon.stub().callsArgWith(1, null, { + result: expectedTxids.reverse() + }); + bitcoind.nodes.push({ + client: { + getAddressTxids: getAddressTxids, + getAddressMempool: getAddressMempool + } + }); + var options = { + queryMempool: true, // start and end will exclude mempool + start: 4, + end: 2 + }; + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + getAddressMempool.callCount.should.equal(0); + txids.should.deep.equal(expectedTxids); + + bitcoind.getAddressTxids(address, options, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(2); + getAddressMempool.callCount.should.equal(0); + txids.should.deep.equal(expectedTxids); + done(); + }); + }); + }); + it('will get txid results from cache and live mempool', function(done) { + var expectedTxids = [ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce' + ]; + var bitcoind = new BitcoinService(baseConfig); + var getAddressTxids = sinon.stub().callsArgWith(1, null, { + result: expectedTxids.reverse() + }); + var getAddressMempool = sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2' + }, + { + txid: 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345' + }, + { + txid: 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9' + } + ] + }); + bitcoind.nodes.push({ + client: { + getAddressTxids: getAddressTxids, + getAddressMempool: getAddressMempool + } + }); + var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + bitcoind.getAddressTxids(address, {queryMempool: false}, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + txids.should.deep.equal(expectedTxids); + + bitcoind.getAddressTxids(address, {queryMempool: true}, function(err, txids) { + if (err) { + return done(err); + } + getAddressTxids.callCount.should.equal(1); + txids.should.deep.equal([ + 'f35e7e2a2334e845946f3eaca76890d9a68f4393ccc9fe37a0c2fb035f66d2e9', // mempool + 'f71bccef3a8f5609c7f016154922adbfe0194a96fb17a798c24077c18d0a9345', // mempool + 'bc992ad772eb02864db07ef248d31fb3c6826d25f1153ebf8c79df9b7f70fcf2', // mempool + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce' // confirmed + ]); + done(); + }); }); }); }); - describe('proxy methods', function() { - var proxyMethods = [ - ['isSynced', 0], - ['syncPercentage', 0], - ['getBlock', 2], - ['isSpent', 2], - ['getBlockIndex', 1], - ['isMainChain', 1], - ['estimateFee', 1], - ['sendTransaction', 2], - ['getTransaction', 3], - ['getTransactionWithBlockInfo', 3], - ['getMempoolTransactions', 0], - ['addMempoolUncheckedTransaction', 1], - ['getBestBlockHash', 0], - ['getNextBlockHash', 1], - ['getInfo', 0] - ]; + describe('#_getConfirmationDetail', function() { + var sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(log, 'warn'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('should get 1 confirmation', function() { + var tx = new Transaction(txhex); + tx.__height = 10; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.height = 10; + var confirmations = bitcoind._getConfirmationsDetail(tx); + confirmations.should.equal(1); + }); + it('should get 2 confirmation', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = new Transaction(txhex); + bitcoind.height = 11; + tx.__height = 10; + var confirmations = bitcoind._getConfirmationsDetail(tx); + confirmations.should.equal(2); + }); + it('should get 0 confirmation with overflow', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = new Transaction(txhex); + bitcoind.height = 3; + tx.__height = 10; + var confirmations = bitcoind._getConfirmationsDetail(tx); + log.warn.callCount.should.equal(1); + confirmations.should.equal(0); + }); + it('should get 1000 confirmation', function() { + var bitcoind = new BitcoinService(baseConfig); + var tx = new Transaction(txhex); + bitcoind.height = 1000; + tx.__height = 1; + var confirmations = bitcoind._getConfirmationsDetail(tx); + confirmations.should.equal(1000); + }); + }); - proxyMethods.forEach(function(x) { - it('pass ' + x[1] + ' argument(s) to ' + x[0], function() { - - var stub = sinon.stub(); - var TestBitcoin = proxyquire('../../lib/services/bitcoind', { - fs: { - readFileSync: readFileSync - }, - bindings: function(name) { - name.should.equal('bitcoind.node'); - var methods = {}; - methods[x[0]] = stub; - return methods; + describe('#_getAddressDetailsForTransaction', function() { + it('will calculate details for the transaction', function(done) { + /* jshint sub:true */ + var tx = bitcore.Transaction({ + 'hash': 'b12b3ae8489c5a566b629a3c62ce4c51c3870af550fb5dc77d715b669a91343c', + 'version': 1, + 'inputs': [ + { + 'prevTxId': 'a2b7ea824a92f4a4944686e67ec1001bc8785348b8c111c226f782084077b543', + 'outputIndex': 0, + 'sequenceNumber': 4294967295, + 'script': '47304402201b81c933297241960a57ae1b2952863b965ac8c9ec7466ff0b715712d27548d50220576e115b63864f003889443525f47c7cf0bc1e2b5108398da085b221f267ba2301210229766f1afa25ca499a51f8e01c292b0255a21a41bb6685564a1607a811ffe924', + 'scriptString': '71 0x304402201b81c933297241960a57ae1b2952863b965ac8c9ec7466ff0b715712d27548d50220576e115b63864f003889443525f47c7cf0bc1e2b5108398da085b221f267ba2301 33 0x0229766f1afa25ca499a51f8e01c292b0255a21a41bb6685564a1607a811ffe924', + 'output': { + 'satoshis': 1000000000, + 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' + } } + ], + 'outputs': [ + { + 'satoshis': 100000000, + 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' + }, + { + 'satoshis': 200000000, + 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' + }, + { + 'satoshis': 50000000, + 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' + }, + { + 'satoshis': 300000000, + 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' + }, + { + 'satoshis': 349990000, + 'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac' + } + ], + 'nLockTime': 0 + }); + var bitcoind = new BitcoinService(baseConfig); + var addresses = ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']; + var details = bitcoind._getAddressDetailsForTransaction(tx, addresses); + should.exist(details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']); + details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].inputIndexes.should.deep.equal([0]); + details.addresses['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].outputIndexes.should.deep.equal([ + 0, 1, 2, 3, 4 + ]); + details.satoshis.should.equal(-10000); + done(); + }); + }); + + describe('#_getDetailedTransaction', function() { + it('will get detailed transaction info', function(done) { + var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; + var tx = { + populateInputs: sinon.stub().callsArg(2), + __height: 20, + __timestamp: 1453134151, + isCoinbase: sinon.stub().returns(false), + getFee: sinon.stub().returns(1000) + }; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getTransactionWithBlockInfo = sinon.stub().callsArgWith(1, null, tx); + bitcoind.height = 300; + bitcoind._getAddressDetailsForTransaction = sinon.stub().returns({ + addresses: {}, + satoshis: 1000, + }); + bitcoind._getDetailedTransaction(txid, {}, function(err) { + if (err) { + return done(err); + } + done(); + }); + }); + it('give error from getTransactionWithBlockInfo', function(done) { + var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getTransactionWithBlockInfo = sinon.stub().callsArgWith(1, new Error('test')); + bitcoind._getDetailedTransaction(txid, {}, function(err) { + err.should.be.instanceof(Error); + done(); + }); + }); + it('give error from populateInputs', function(done) { + var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; + var tx = { + populateInputs: sinon.stub().callsArgWith(2, new Error('test')), + }; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getTransactionWithBlockInfo = sinon.stub().callsArgWith(1, null, tx); + bitcoind._getDetailedTransaction(txid, {}, function(err) { + err.should.be.instanceof(Error); + done(); + }); + }); + + it('will correct detailed info', function(done) { + // block #314159 + // txid 30169e8bf78bc27c4014a7aba3862c60e2e3cce19e52f1909c8255e4b7b3174e + // outputIndex 1 + var txAddress = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'; + var txString = '0100000001a08ee59fcd5d86fa170abb6d925d62d5c5c476359681b70877c04f270c4ef246000000008a47304402203fb9b476bb0c37c9b9ed5784ebd67ae589492be11d4ae1612be29887e3e4ce750220741ef83781d1b3a5df8c66fa1957ad0398c733005310d7d9b1d8c2310ef4f74c0141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02b0a75fac000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac20badc02000000001976a914809dc14496f99b6deb722cf46d89d22f4beb8efd88ac00000000'; + var previousTxString = '010000000155532fad2869bb951b0bd646a546887f6ee668c4c0ee13bf3f1c4bce6d6e3ed9000000008c4930460221008540795f4ef79b1d2549c400c61155ca5abbf3089c84ad280e1ba6db2a31abce022100d7d162175483d51174d40bba722e721542c924202a0c2970b07e680b51f3a0670141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02f0af3caf000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac80969800000000001976a91421277e65777760d1f3c7c982ba14ed8f934f005888ac00000000'; + var transaction = new Transaction(); + var previousTransaction = new Transaction(); + previousTransaction.fromString(previousTxString); + var previousTransactionTxid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; + transaction.fromString(txString); + var txid = transaction.hash; + transaction.__blockHash = '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45'; + transaction.__height = 314159; + transaction.__timestamp = 1407292005; + var bitcoind = new BitcoinService(baseConfig); + bitcoind.height = 314159; + bitcoind.getTransactionWithBlockInfo = sinon.stub().callsArgWith(1, null, transaction); + bitcoind.getTransaction = function(prevTxid, callback) { + prevTxid.should.equal(previousTransactionTxid); + setImmediate(function() { + callback(null, previousTransaction); + }); + }; + var transactionInfo = { + addresses: {}, + txid: txid, + timestamp: 1407292005, + satoshis: 48020000, + address: txAddress + }; + transactionInfo.addresses[txAddress] = {}; + transactionInfo.addresses[txAddress].outputIndexes = [1]; + transactionInfo.addresses[txAddress].inputIndexes = []; + bitcoind._getAddressDetailsForTransaction = sinon.stub().returns(transactionInfo); + bitcoind._getDetailedTransaction(txid, {}, function(err, info) { + if (err) { + return done(err); + } + info.addresses[txAddress].should.deep.equal({ + outputIndexes: [1], + inputIndexes: [] + }); + info.satoshis.should.equal(48020000); + info.height.should.equal(314159); + info.confirmations.should.equal(1); + info.timestamp.should.equal(1407292005); + info.fees.should.equal(20000); + info.tx.should.equal(transaction); + done(); + }); + }); + }); + + describe('#_getAddressStrings', function() { + }); + + describe('#_paginateTxids', function() { + it('slice txids based on "from" and "to" (3 to 30)', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var paginated = bitcoind._paginateTxids(txids, 3, 30); + paginated.should.deep.equal([3, 4, 5, 6, 7, 8, 9, 10]); + }); + it('slice txids based on "from" and "to" (0 to 3)', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var paginated = bitcoind._paginateTxids(txids, 0, 3); + paginated.should.deep.equal([0, 1, 2]); + }); + it('slice txids based on "from" and "to" (0 to 1)', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var paginated = bitcoind._paginateTxids(txids, 0, 1); + paginated.should.deep.equal([0]); + }); + it('will throw error if "from" is greater than "to"', function() { + var bitcoind = new BitcoinService(baseConfig); + var txids = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + (function() { + var paginated = bitcoind._paginateTxids(txids, 1, 0); + }).should.throw('"from" is expected to be less than "to"'); + }); + }); + + describe('#getAddressHistory', function() { + var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; + it('will give an error if length of addresses is too long', function(done) { + var addresses = []; + for (var i = 0; i < 101; i++) { + addresses.push(address); + } + var bitcoind = new BitcoinService(baseConfig); + bitcoind.maxAddressesQuery = 100; + bitcoind.getAddressHistory(addresses, {}, function(err) { + should.exist(err); + err.message.match(/Maximum/); + done(); + }); + }); + it('give error from getAddressTxids', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, new Error('test')); + bitcoind.getAddressHistory('address', {}, function(err) { + should.exist(err); + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will paginate', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind._getDetailedTransaction = function(txid, options, callback) { + callback(null, txid); + }; + var txids = ['one', 'two', 'three', 'four']; + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, txids); + bitcoind.getAddressHistory('address', {from: 1, to: 3}, function(err, data) { + if (err) { + return done(err); + } + data.items.length.should.equal(2); + data.items.should.deep.equal(['two', 'three']); + done(); + }); + }); + }); + + describe('#getAddressSummary', function() { + var txid1 = '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8'; + var txid2 = '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d'; + var txid3 = '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247'; + var memtxid1 = 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92'; + var memtxid2 = 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce'; + + it('will handle error from getAddressTxids', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', + } + ] + }) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, new Error('test')); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, {}); + var address = ''; + var options = {}; + bitcoind.getAddressSummary(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will handle error from getAddressBalance', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', + } + ] + }) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, {}); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, new Error('test'), {}); + var address = ''; + var options = {}; + bitcoind.getAddressSummary(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(Error); + err.message.should.equal('test'); + done(); + }); + }); + it('will handle error from client getAddressMempool', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, {}); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, {}); + var address = ''; + var options = {}; + bitcoind.getAddressSummary(address, options, function(err) { + should.exist(err); + err.should.be.instanceof(Error); + err.message.should.equal('Test error'); + done(); + }); + }); + it('should set all properties', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: memtxid1, + satoshis: -1000000 + }, + { + txid: memtxid2, + satoshis: 99999 + } + ] + }) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, [txid1, txid2, txid3]); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, { + received: 30 * 1e8, + balance: 20 * 1e8 + }); + var address = '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj'; + var options = {}; + bitcoind.getAddressSummary(address, options, function(err, summary) { + summary.appearances.should.equal(3); + summary.totalReceived.should.equal(3000000000); + summary.totalSpent.should.equal(1000000000); + summary.balance.should.equal(2000000000); + summary.unconfirmedAppearances.should.equal(2); + summary.unconfirmedBalance.should.equal(-900001); + summary.txids.should.deep.equal([ + 'e9dcf22807db77ac0276b03cc2d3a8b03c4837db8ac6650501ef45af1c807cce', + 'b1bfa8dbbde790cb46b9763ef3407c1a21c8264b67bfe224f462ec0e1f569e92', + '70d9d441d7409aace8e0ffe24ff0190407b2fcb405799a266e0327017288d1f8', + '35fafaf572341798b2ce2858755afa7c8800bb6b1e885d3e030b81255b5e172d', + '57b7842afc97a2b46575b490839df46e9273524c6ea59ba62e1e86477cf25247' + ]); + done(); + }); + }); + it('will get from cache with noTxList', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getAddressMempool: sinon.stub().callsArgWith(1, null, { + result: [ + { + txid: memtxid1, + satoshis: -1000000 + }, + { + txid: memtxid2, + satoshis: 99999 + } + ] + }) + } + }); + bitcoind.getAddressTxids = sinon.stub().callsArgWith(2, null, [txid1, txid2, txid3]); + bitcoind.getAddressBalance = sinon.stub().callsArgWith(2, null, { + received: 30 * 1e8, + balance: 20 * 1e8 + }); + var address = '3NbU8XzUgKyuCgYgZEKsBtUvkTm2r7Xgwj'; + var options = { + noTxList: true + }; + function checkSummary(summary) { + summary.appearances.should.equal(3); + summary.totalReceived.should.equal(3000000000); + summary.totalSpent.should.equal(1000000000); + summary.balance.should.equal(2000000000); + summary.unconfirmedAppearances.should.equal(2); + summary.unconfirmedBalance.should.equal(-900001); + should.not.exist(summary.txids); + } + bitcoind.getAddressSummary(address, options, function(err, summary) { + checkSummary(summary); + bitcoind.getAddressTxids.callCount.should.equal(1); + bitcoind.getAddressBalance.callCount.should.equal(1); + bitcoind.getAddressSummary(address, options, function(err, summary) { + checkSummary(summary); + bitcoind.getAddressTxids.callCount.should.equal(1); + bitcoind.getAddressBalance.callCount.should.equal(1); + done(); + }); + }); + }); + }); + + describe('#getRawBlock', function() { + var blockhash = '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'; + var blockhex = '0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; + it('will give rcp error from client getblockhash', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getBlockHash: sinon.stub().callsArgWith(1, {code: -1, message: 'Test error'}) + } + }); + bitcoind.getRawBlock(10, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('will give rcp error from client getblock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getBlock: sinon.stub().callsArgWith(2, {code: -1, message: 'Test error'}) + } + }); + bitcoind.getRawBlock(blockhash, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('will try all nodes for getblock', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockWithError = sinon.stub().callsArgWith(2, {code: -1, message: 'Test error'}); + bitcoind.tryAllInterval = 1; + bitcoind.nodes.push({ + client: { + getBlock: getBlockWithError + } + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlockWithError + } + }); + bitcoind.nodes.push({ + client: { + getBlock: sinon.stub().callsArgWith(2, null, { + result: blockhex + }) + } + }); + bitcoind.getRawBlock(blockhash, function(err, buffer) { + if (err) { + return done(err); + } + buffer.should.be.instanceof(Buffer); + getBlockWithError.callCount.should.equal(2); + done(); + }); + }); + it('will get block from cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockhex + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlock + } + }); + bitcoind.getRawBlock(blockhash, function(err, buffer) { + if (err) { + return done(err); + } + buffer.should.be.instanceof(Buffer); + getBlock.callCount.should.equal(1); + bitcoind.getRawBlock(blockhash, function(err, buffer) { + if (err) { + return done(err); + } + buffer.should.be.instanceof(Buffer); + getBlock.callCount.should.equal(1); + done(); + }); + }); + }); + it('will get block by height', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlock = sinon.stub().callsArgWith(2, null, { + result: blockhex + }); + var getBlockHash = sinon.stub().callsArgWith(1, null, { + result: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' + }); + bitcoind.nodes.push({ + client: { + getBlock: getBlock, + getBlockHash: getBlockHash + } + }); + bitcoind.getRawBlock(0, function(err, buffer) { + if (err) { + return done(err); + } + buffer.should.be.instanceof(Buffer); + getBlock.callCount.should.equal(1); + getBlockHash.callCount.should.equal(1); + done(); + }); + }); + }); + + describe('#getBlock', function() { + }); + + describe('#getBlockHashesByTimestamp', function() { + it('should give an rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBlockHashes = sinon.stub().callsArgWith(2, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getBlockHashes: getBlockHashes + } + }); + bitcoind.getBlockHashesByTimestamp(1441911000, 1441914000, function(err, hashes) { + should.exist(err); + err.message.should.equal('error'); + done(); + }); + }); + it('should get the correct block hashes', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var block1 = '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b'; + var block2 = '000000000383752a55a0b2891ce018fd0fdc0b6352502772b034ec282b4a1bf6'; + var getBlockHashes = sinon.stub().callsArgWith(2, null, { + result: [block2, block1] + }); + bitcoind.nodes.push({ + client: { + getBlockHashes: getBlockHashes + } + }); + bitcoind.getBlockHashesByTimestamp(1441914000, 1441911000, function(err, hashes) { + should.not.exist(err); + hashes.should.deep.equal([block2, block1]); + done(); + }); + }); + }); + + describe('#getBlockHeader', function() { + }); + + describe('#estimateFee', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var estimateFee = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + estimateFee: estimateFee + } + }); + bitcoind.estimateFee(1, function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client estimateFee and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var estimateFee = sinon.stub().callsArgWith(1, null, { + result: -1 + }); + bitcoind.nodes.push({ + client: { + estimateFee: estimateFee + } + }); + bitcoind.estimateFee(1, function(err, feesPerKb) { + if (err) { + return done(err); + } + feesPerKb.should.equal(-1); + done(); + }); + }); + }); + + describe('#sendTransaction', function(done) { + var tx = bitcore.Transaction(txhex); + it('will give rpc error', function() { + var bitcoind = new BitcoinService(baseConfig); + var sendRawTransaction = sinon.stub().callsArgWith(2, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + sendRawTransaction: sendRawTransaction + } + }); + bitcoind.sendTransaction(txhex, function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + }); + }); + it('will send to client and get hash', function() { + var bitcoind = new BitcoinService(baseConfig); + var sendRawTransaction = sinon.stub().callsArgWith(2, null, { + result: tx.hash + }); + bitcoind.nodes.push({ + client: { + sendRawTransaction: sendRawTransaction + } + }); + bitcoind.sendTransaction(txhex, function(err, hash) { + if (err) { + return done(err); + } + hash.should.equal(tx.hash); + }); + }); + it('will send to client with absurd fees and get hash', function() { + var bitcoind = new BitcoinService(baseConfig); + var sendRawTransaction = sinon.stub().callsArgWith(2, null, { + result: tx.hash + }); + bitcoind.nodes.push({ + client: { + sendRawTransaction: sendRawTransaction + } + }); + bitcoind.sendTransaction(txhex, {allowAbsurdFees: true}, function(err, hash) { + if (err) { + return done(err); + } + hash.should.equal(tx.hash); + }); + }); + }); + + describe('#getRawTransaction', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getRawTransaction('txid', function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will try all nodes', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.tryAllInterval = 1; + var getRawTransactionWithError = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + var getRawTransaction = sinon.stub().callsArgWith(1, null, { + result: txhex + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransactionWithError + } + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransactionWithError + } + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getRawTransaction('txid', function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx); + tx.should.be.an.instanceof(Buffer); + done(); + }); + }); + it('will get from cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(1, null, { + result: txhex + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getRawTransaction('txid', function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx); + tx.should.be.an.instanceof(Buffer); + + bitcoind.getRawTransaction('txid', function(err, tx) { + should.exist(tx); + tx.should.be.an.instanceof(Buffer); + getRawTransaction.callCount.should.equal(1); + done(); + }); + }); + }); + }); + + describe('#getTransaction', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getTransaction('txid', function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will try all nodes', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.tryAllInterval = 1; + var getRawTransactionWithError = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + var getRawTransaction = sinon.stub().callsArgWith(1, null, { + result: txhex + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransactionWithError + } + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransactionWithError + } + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getTransaction('txid', function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx); + tx.should.be.an.instanceof(bitcore.Transaction); + done(); + }); + }); + it('will get from cache', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getRawTransaction = sinon.stub().callsArgWith(1, null, { + result: txhex + }); + bitcoind.nodes.push({ + client: { + getRawTransaction: getRawTransaction + } + }); + bitcoind.getTransaction('txid', function(err, tx) { + if (err) { + return done(err); + } + should.exist(tx); + tx.should.be.an.instanceof(bitcore.Transaction); + + bitcoind.getTransaction('txid', function(err, tx) { + should.exist(tx); + tx.should.be.an.instanceof(bitcore.Transaction); + getRawTransaction.callCount.should.equal(1); + done(); }); - var bitcoind = new TestBitcoin(baseConfig); - var args = []; - for (var i = 0; i < x[1]; i++) { - args.push(i); - } - - bitcoind[x[0]].apply(bitcoind, args); - stub.callCount.should.equal(1); - stub.args[0].length.should.equal(x[1]); }); }); }); + describe('#getTransactionWithBlockInfo', function() { + var txBuffer = new Buffer('01000000016f95980911e01c2c664b3e78299527a47933aac61a515930a8fe0213d1ac9abe01000000da0047304402200e71cda1f71e087c018759ba3427eb968a9ea0b1decd24147f91544629b17b4f0220555ee111ed0fc0f751ffebf097bdf40da0154466eb044e72b6b3dcd5f06807fa01483045022100c86d6c8b417bff6cc3bbf4854c16bba0aaca957e8f73e19f37216e2b06bb7bf802205a37be2f57a83a1b5a8cc511dc61466c11e9ba053c363302e7b99674be6a49fc0147522102632178d046673c9729d828cfee388e121f497707f810c131e0d3fc0fe0bd66d62103a0951ec7d3a9da9de171617026442fcd30f34d66100fab539853b43f508787d452aeffffffff0240420f000000000017a9148a31d53a448c18996e81ce67811e5fb7da21e4468738c9d6f90000000017a9148ce5408cfeaddb7ccb2545ded41ef478109454848700000000', 'hex'); + var info = { + blockHash: '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6', + height: 530482, + timestamp: 1439559434000, + buffer: txBuffer + }; + + it('should give a transaction with height and timestamp', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, {code: -1, message: 'Test error'}) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getTransactionWithBlockInfo(txid, function(err) { + should.exist(err); + err.should.be.instanceof(errors.RPCError); + done(); + }); + }); + it('should give a transaction with height and timestamp', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.nodes.push({ + client: { + getRawTransaction: sinon.stub().callsArgWith(2, null, { + result: { + hex: txBuffer.toString('hex'), + blockhash: info.blockHash, + height: info.height, + time: info.timestamp, + vout: [ + { + spentTxId: 'txid', + spentIndex: 2, + spentHeight: 100 + } + ] + } + }) + } + }); + var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f'; + bitcoind.getTransactionWithBlockInfo(txid, function(err, tx) { + // TODO verify additional info + should.exist(tx); + done(); + }); + }); + }); + + describe('#getBestBlockHash', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getBestBlockHash: getBestBlockHash + } + }); + bitcoind.getBestBlockHash(function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client getInfo and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getBestBlockHash = sinon.stub().callsArgWith(0, null, { + result: 'besthash' + }); + bitcoind.nodes.push({ + client: { + getBestBlockHash: getBestBlockHash + } + }); + bitcoind.getBestBlockHash(function(err, hash) { + if (err) { + return done(err); + } + should.exist(hash); + hash.should.equal('besthash'); + done(); + }); + }); + }); + + describe('#getSpentInfo', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getSpentInfo = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getSpentInfo: getSpentInfo + } + }); + bitcoind.getSpentInfo({}, function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will empty object when not found', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getSpentInfo = sinon.stub().callsArgWith(1, {message: 'test', code: -5}); + bitcoind.nodes.push({ + client: { + getSpentInfo: getSpentInfo + } + }); + bitcoind.getSpentInfo({}, function(err, info) { + should.not.exist(err); + info.should.deep.equal({}); + done(); + }); + }); + it('will call client getSpentInfo and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getSpentInfo = sinon.stub().callsArgWith(1, null, { + result: { + txid: 'txid', + index: 10, + height: 101 + } + }); + bitcoind.nodes.push({ + client: { + getSpentInfo: getSpentInfo + } + }); + bitcoind.getSpentInfo({}, function(err, info) { + if (err) { + return done(err); + } + info.txid.should.equal('txid'); + info.index.should.equal(10); + info.height.should.equal(101); + done(); + }); + }); + }); + + describe('#getInfo', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var getInfo = sinon.stub().callsArgWith(0, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + getInfo: getInfo + } + }); + bitcoind.getInfo(function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client getInfo and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.node.getNetworkName = sinon.stub().returns('testnet'); + var getInfo = sinon.stub().callsArgWith(0, null, { + result: {} + }); + bitcoind.nodes.push({ + client: { + getInfo: getInfo + } + }); + bitcoind.getInfo(function(err, info) { + if (err) { + return done(err); + } + should.exist(info); + should.exist(info.network); + info.network.should.equal('testnet'); + done(); + }); + }); + }); + + describe('#generateBlock', function() { + it('will give rpc error', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var generate = sinon.stub().callsArgWith(1, {message: 'error', code: -1}); + bitcoind.nodes.push({ + client: { + generate: generate + } + }); + bitcoind.generateBlock(10, function(err) { + should.exist(err); + err.should.be.an.instanceof(errors.RPCError); + done(); + }); + }); + it('will call client generate and give result', function(done) { + var bitcoind = new BitcoinService(baseConfig); + var generate = sinon.stub().callsArgWith(1, null, { + result: ['hash'] + }); + bitcoind.nodes.push({ + client: { + generate: generate + } + }); + bitcoind.generateBlock(10, function(err, hashes) { + if (err) { + return done(err); + } + hashes.length.should.equal(1); + hashes[0].should.equal('hash'); + done(); + }); + }); + }); + + describe('#stop', function() { + it('will callback if spawn is not set', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.stop(done); + }); + it('will exit spawned process', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.spawn = {}; + bitcoind.spawn.process = new EventEmitter(); + bitcoind.spawn.process.kill = sinon.stub(); + bitcoind.stop(done); + bitcoind.spawn.process.kill.callCount.should.equal(1); + bitcoind.spawn.process.kill.args[0][0].should.equal('SIGINT'); + bitcoind.spawn.process.emit('exit', 0); + }); + it('will give error with non-zero exit status code', function(done) { + var bitcoind = new BitcoinService(baseConfig); + bitcoind.spawn = {}; + bitcoind.spawn.process = new EventEmitter(); + bitcoind.spawn.process.kill = sinon.stub(); + bitcoind.stop(function(err) { + err.should.be.instanceof(Error); + err.code.should.equal(1); + done(); + }); + bitcoind.spawn.process.kill.callCount.should.equal(1); + bitcoind.spawn.process.kill.args[0][0].should.equal('SIGINT'); + bitcoind.spawn.process.emit('exit', 1); + }); + }); + }); diff --git a/test/services/db.unit.js b/test/services/db.unit.js deleted file mode 100644 index 60568cce..00000000 --- a/test/services/db.unit.js +++ /dev/null @@ -1,1038 +0,0 @@ -'use strict'; - -var should = require('chai').should(); -var sinon = require('sinon'); -var EventEmitter = require('events').EventEmitter; -var proxyquire = require('proxyquire'); -var index = require('../../'); -var DB = index.services.DB; -var blockData = require('../data/livenet-345003.json'); -var bitcore = require('bitcore-lib'); -var Networks = bitcore.Networks; -var Block = bitcore.Block; -var BufferUtil = bitcore.util.buffer; -var Transaction = bitcore.Transaction; -var transactionData = require('../data/bitcoin-transactions.json'); -var chainHashes = require('../data/hashes.json'); -var chainData = require('../data/testnet-blocks.json'); -var errors = index.errors; -var memdown = require('memdown'); -var levelup = require('levelup'); - -describe('DB Service', function() { - - function hexlebuf(hexString){ - return BufferUtil.reverse(new Buffer(hexString, 'hex')); - } - - function lebufhex(buf) { - return BufferUtil.reverse(buf).toString('hex'); - } - - var baseConfig = { - node: { - network: Networks.testnet, - datadir: 'testdir' - }, - store: memdown - }; - - var genesisBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex'); - - - describe('#_setDataPath', function() { - it('should set the database path', function() { - var config = { - node: { - network: Networks.livenet, - datadir: process.env.HOME + '/.bitcoin' - }, - store: memdown - }; - var db = new DB(config); - db.dataPath.should.equal(process.env.HOME + '/.bitcoin/bitcore-node.db'); - }); - it('should load the db for testnet', function() { - var config = { - node: { - network: Networks.testnet, - datadir: process.env.HOME + '/.bitcoin' - }, - store: memdown - }; - var db = new DB(config); - db.dataPath.should.equal(process.env.HOME + '/.bitcoin/testnet3/bitcore-node.db'); - }); - it('error with unknown network', function() { - var config = { - node: { - network: 'unknown', - datadir: process.env.HOME + '/.bitcoin' - }, - store: memdown - }; - (function() { - var db = new DB(config); - }).should.throw('Unknown network'); - }); - it('should load the db with regtest', function() { - // Switch to use regtest - Networks.enableRegtest(); - var regtest = Networks.get('regtest'); - var config = { - node: { - network: regtest, - datadir: process.env.HOME + '/.bitcoin' - }, - store: memdown - }; - var db = new DB(config); - db.dataPath.should.equal(process.env.HOME + '/.bitcoin/regtest/bitcore-node.db'); - Networks.disableRegtest(); - }); - }); - - describe('#_checkVersion', function() { - var config = { - node: { - network: Networks.get('testnet'), - datadir: 'testdir' - }, - store: memdown - }; - it('will handle an error while retrieving the tip', function() { - var db = new DB(config); - db.store = {}; - db.store.get = sinon.stub().callsArgWith(2, new Error('test')); - db._checkVersion(function(err) { - should.exist(err); - err.message.should.equal('test'); - }); - }); - it('will handle an error while retrieving the version', function() { - var db = new DB(config); - db.store = {}; - db.store.get = function() {}; - var callCount = 0; - sinon.stub(db.store, 'get', function(key, options, callback) { - if (callCount === 1) { - return callback(new Error('test')); - } - callCount++; - setImmediate(callback); - }); - db._checkVersion(function(err) { - should.exist(err); - err.message.should.equal('test'); - }); - }); - it('will NOT check the version if a tip is not found', function(done) { - var db = new DB(config); - db.store = {}; - db.store.get = sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()); - db._checkVersion(done); - }); - it('will NOT give an error if the versions match', function(done) { - var db = new DB(config); - db.store = {}; - db.store.get = function() {}; - var callCount = 0; - sinon.stub(db.store, 'get', function(key, options, callback) { - if (callCount === 1) { - var versionBuffer = new Buffer(new Array(4)); - versionBuffer.writeUInt32BE(2); - return callback(null, versionBuffer); - } - callCount++; - setImmediate(callback); - }); - db.version = 2; - db._checkVersion(done); - }); - it('will give an error if the versions do NOT match', function(done) { - var db = new DB(config); - db.store = {}; - db.store.get = function() {}; - var callCount = 0; - sinon.stub(db.store, 'get', function(key, options, callback) { - if (callCount === 1) { - var versionBuffer = new Buffer(new Array(4)); - versionBuffer.writeUInt32BE(2); - return callback(null, versionBuffer); - } - callCount++; - setImmediate(callback); - }); - db.version = 3; - db._checkVersion(function(err) { - should.exist(err); - err.message.should.match(/^The version of the database/); - done(); - }); - }); - it('will default to version 1 if the version is NOT found', function(done) { - var db = new DB(config); - db.store = {}; - db.store.get = function() {}; - var callCount = 0; - sinon.stub(db.store, 'get', function(key, options, callback) { - if (callCount === 1) { - return callback(new levelup.errors.NotFoundError()); - } - callCount++; - setImmediate(callback); - }); - db.version = 1; - db._checkVersion(done); - }); - }); - - describe('#_setVersion', function() { - var config = { - node: { - network: Networks.get('testnet'), - datadir: 'testdir' - }, - store: memdown - }; - it('will give an error from the store', function(done) { - var db = new DB(config); - db.store = {}; - db.store.put = sinon.stub().callsArgWith(2, new Error('test')); - db._setVersion(function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - it('will set the version', function(done) { - var db = new DB(config); - db.store = {}; - db.store.put = sinon.stub().callsArgWith(2, null); - db.version = 5; - db._setVersion(function(err) { - if (err) { - return done(err); - } - db.store.put.args[0][0].should.deep.equal(new Buffer('ff', 'hex')); - db.store.put.args[0][1].should.deep.equal(new Buffer('00000005', 'hex')); - done(); - }); - }); - }); - - describe('#start', function() { - var TestDB; - - before(function() { - TestDB = proxyquire('../../lib/services/db', { - fs: { - existsSync: sinon.stub().returns(true) - }, - levelup: sinon.stub() - }); - }); - - it('should emit ready', function(done) { - var db = new TestDB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - on: sinon.spy(), - genesisBuffer: genesisBuffer - }; - db.loadTip = sinon.stub().callsArg(0); - db.connectBlock = sinon.stub().callsArg(1); - db._checkVersion = sinon.stub().callsArg(0); - db._setVersion = sinon.stub().callsArg(0); - db.sync = sinon.stub(); - var readyFired = false; - db.on('ready', function() { - readyFired = true; - }); - db.start(function() { - readyFired.should.equal(true); - done(); - }); - }); - - it('will call sync when there is a new tip', function(done) { - var db = new TestDB(baseConfig); - db.node.services = {}; - db.node.services.bitcoind = new EventEmitter(); - db.node.services.bitcoind.genesisBuffer = genesisBuffer; - db.loadTip = sinon.stub().callsArg(0); - db.connectBlock = sinon.stub().callsArg(1); - db._checkVersion = sinon.stub().callsArg(0); - db._setVersion = sinon.stub().callsArg(0); - db.sync = sinon.stub(); - db.start(function() { - db.sync = function() { - done(); - }; - db.node.services.bitcoind.emit('tip', 10); - }); - }); - - it('will not call sync when there is a new tip and shutting down', function(done) { - var db = new TestDB(baseConfig); - db.node.services = {}; - db.node.services.bitcoind = new EventEmitter(); - db.node.services.bitcoind.syncPercentage = sinon.spy(); - db.node.services.bitcoind.genesisBuffer = genesisBuffer; - db.loadTip = sinon.stub().callsArg(0); - db.connectBlock = sinon.stub().callsArg(1); - db._checkVersion = sinon.stub().callsArg(0); - db._setVersion = sinon.stub().callsArg(0); - db.node.stopping = true; - db.sync = sinon.stub(); - db.start(function() { - db.sync.callCount.should.equal(1); - db.node.services.bitcoind.once('tip', function() { - db.sync.callCount.should.equal(1); - done(); - }); - db.node.services.bitcoind.emit('tip', 10); - }); - }); - - }); - - describe('#stop', function() { - it('should wait until db has stopped syncing before closing leveldb', function(done) { - var db = new DB(baseConfig); - db.store = { - close: sinon.stub().callsArg(0) - }; - db.bitcoindSyncing = true; - - db.stop(function(err) { - should.not.exist(err); - done(); - }); - - setTimeout(function() { - db.bitcoindSyncing = false; - }, 15); - }); - }); - - describe('#getTransaction', function() { - it('will return a NotFound error', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransaction: sinon.stub().callsArgWith(2, null, null) - }; - var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; - db.getTransaction(txid, true, function(err) { - err.should.be.instanceof(errors.Transaction.NotFound); - done(); - }); - }); - it('will return an error from bitcoind', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransaction: sinon.stub().callsArgWith(2, new Error('test error')) - }; - var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; - db.getTransaction(txid, true, function(err) { - err.message.should.equal('test error'); - done(); - }); - }); - it('will return an error from bitcoind', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransaction: sinon.stub().callsArgWith(2, null, new Buffer(transactionData[0].hex, 'hex')) - }; - var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623'; - db.getTransaction(txid, true, function(err, tx) { - if (err) { - throw err; - } - should.exist(tx); - done(); - }); - }); - }); - - describe('#loadTip', function() { - it('genesis block if no metadata is found in the db', function(done) { - var db = new DB(baseConfig); - db.genesis = Block.fromBuffer(genesisBuffer); - db.store = { - get: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()) - }; - db.connectBlock = sinon.stub().callsArg(1); - db.sync = sinon.stub(); - db.loadTip(function() { - should.exist(db.tip); - db.tip.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'); - done(); - }); - }); - - it('tip from the database if it exists', function(done) { - var node = { - network: Networks.testnet, - datadir: 'testdir', - services: { - bitcoind: { - genesisBuffer: genesisBuffer, - on: sinon.stub(), - getBlockIndex: sinon.stub().returns({height: 1}) - } - } - }; - var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; - var tip = Block.fromBuffer(genesisBuffer); - var db = new DB({node: node}); - db.store = { - get: sinon.stub().callsArgWith(2, null, new Buffer(tipHash, 'hex')) - }; - db.getBlock = sinon.stub().callsArgWith(1, null, tip); - db.sync = sinon.stub(); - db.loadTip(function() { - should.exist(db.tip); - db.tip.hash.should.equal(tipHash); - db.tip.__height.should.equal(1); - done(); - }); - }); - - it('give error if levelup error', function(done) { - var node = { - network: Networks.testnet, - datadir: 'testdir', - services: { - bitcoind: { - genesisBuffer: genesisBuffer, - on: sinon.stub() - } - } - }; - var db = new DB({node: node}); - db.store = { - get: sinon.stub().callsArgWith(2, new Error('test')) - }; - db.loadTip(function(err) { - should.exist(err); - err.message.should.equal('test'); - done(); - }); - }); - - it('should try 3 times before giving error from getBlock', function(done) { - var node = { - network: Networks.testnet, - datadir: 'testdir', - services: { - bitcoind: { - genesisBuffer: genesisBuffer, - on: sinon.stub(), - getBlockIndex: sinon.stub().returns({height: 1}) - } - } - }; - var db = new DB({node: node}); - db.retryInterval = 10; - var tipHash = '00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206'; - db.store = { - get: sinon.stub().callsArgWith(2, null, new Buffer(tipHash, 'hex')) - }; - db.getBlock = sinon.stub().callsArgWith(1, new Error('test')); - db.loadTip(function(err) { - should.exist(err); - db.getBlock.callCount.should.equal(3); - err.message.should.equal('test'); - done(); - }); - }); - }); - - describe('#getBlock', function() { - var db = new DB(baseConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var expectedBlock = Block.fromBuffer(blockBuffer); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer) - }; - - it('should get the block from bitcoin daemon', function(done) { - db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) { - should.not.exist(err); - block.hash.should.equal(expectedBlock.hash); - done(); - }); - }); - it('should give an error when bitcoind.js gives an error', function(done) { - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = {}; - db.node.services.bitcoind.getBlock = sinon.stub().callsArgWith(1, new Error('error')); - db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - }); - - describe('#getBlockHashesByTimestamp', function() { - it('should get the correct block hashes', function(done) { - var db = new DB(baseConfig); - var readStream = new EventEmitter(); - db.store = { - createReadStream: sinon.stub().returns(readStream) - }; - - var block1 = { - hash: '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b', - timestamp: 1441911909 - }; - - var block2 = { - hash: '000000000383752a55a0b2891ce018fd0fdc0b6352502772b034ec282b4a1bf6', - timestamp: 1441913112 - }; - - db.getBlockHashesByTimestamp(1441914000, 1441911000, function(err, hashes) { - should.not.exist(err); - hashes.should.deep.equal([block2.hash, block1.hash]); - done(); - }); - - readStream.emit('data', { - key: db._encodeBlockIndexKey(block2.timestamp), - value: db._encodeBlockIndexValue(block2.hash) - }); - - readStream.emit('data', { - key: db._encodeBlockIndexKey(block1.timestamp), - value: db._encodeBlockIndexValue(block1.hash) - }); - - readStream.emit('close'); - }); - - it('should give an error if the stream has an error', function(done) { - var db = new DB(baseConfig); - var readStream = new EventEmitter(); - db.store = { - createReadStream: sinon.stub().returns(readStream) - }; - - db.getBlockHashesByTimestamp(1441911000, 1441914000, function(err, hashes) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - - readStream.emit('error', new Error('error')); - - readStream.emit('close'); - }); - - it('should give an error if the timestamp is out of range', function(done) { - var db = new DB(baseConfig); - var readStream = new EventEmitter(); - db.store = { - createReadStream: sinon.stub().returns(readStream) - }; - - db.getBlockHashesByTimestamp(-1, -5, function(err, hashes) { - should.exist(err); - err.message.should.equal('Invalid Argument: timestamp out of bounds'); - done(); - }); - }); - }); - - describe('#getPrevHash', function() { - it('should return prevHash from bitcoind', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getBlockIndex: sinon.stub().returns({ - prevHash: 'prevhash' - }) - }; - - db.getPrevHash('hash', function(err, prevHash) { - should.not.exist(err); - prevHash.should.equal('prevhash'); - done(); - }); - }); - - it('should give an error if bitcoind could not find it', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getBlockIndex: sinon.stub().returns(null) - }; - - db.getPrevHash('hash', function(err, prevHash) { - should.exist(err); - done(); - }); - }); - }); - - describe('#getTransactionWithBlockInfo', function() { - it('should give a transaction with height and timestamp', function(done) { - var txBuffer = new Buffer('01000000016f95980911e01c2c664b3e78299527a47933aac61a515930a8fe0213d1ac9abe01000000da0047304402200e71cda1f71e087c018759ba3427eb968a9ea0b1decd24147f91544629b17b4f0220555ee111ed0fc0f751ffebf097bdf40da0154466eb044e72b6b3dcd5f06807fa01483045022100c86d6c8b417bff6cc3bbf4854c16bba0aaca957e8f73e19f37216e2b06bb7bf802205a37be2f57a83a1b5a8cc511dc61466c11e9ba053c363302e7b99674be6a49fc0147522102632178d046673c9729d828cfee388e121f497707f810c131e0d3fc0fe0bd66d62103a0951ec7d3a9da9de171617026442fcd30f34d66100fab539853b43f508787d452aeffffffff0240420f000000000017a9148a31d53a448c18996e81ce67811e5fb7da21e4468738c9d6f90000000017a9148ce5408cfeaddb7ccb2545ded41ef478109454848700000000', 'hex'); - var info = { - blockHash: '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6', - height: 530482, - timestamp: 1439559434000, - buffer: txBuffer - }; - - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, info) - }; - - db.getTransactionWithBlockInfo('2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f', true, function(err, tx) { - should.not.exist(err); - tx.__blockHash.should.equal(info.blockHash); - tx.__height.should.equal(info.height); - tx.__timestamp.should.equal(info.timestamp); - done(); - }); - }); - it('should give an error if one occurred', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('error')) - }; - - db.getTransactionWithBlockInfo('tx', true, function(err, tx) { - should.exist(err); - done(); - }); - }); - }); - - describe('#sendTransaction', function() { - it('should handle a basic serialized transaction hex string', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - sendTransaction: sinon.stub().returns('txid') - }; - - var tx = 'hexstring'; - db.sendTransaction(tx, function(err, txid) { - should.not.exist(err); - txid.should.equal('txid'); - done(); - }); - }); - it('should give the txid on success', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - sendTransaction: sinon.stub().returns('txid') - }; - - var tx = new Transaction(); - tx.serialize = sinon.stub().returns('txstring'); - db.sendTransaction(tx, function(err, txid) { - should.not.exist(err); - tx.serialize.callCount.should.equal(1); - txid.should.equal('txid'); - done(); - }); - }); - it('should give an error if bitcoind threw an error', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - sendTransaction: sinon.stub().throws(new Error('error')) - }; - - var tx = new Transaction(); - tx.serialize = sinon.stub().returns('txstring'); - db.sendTransaction(tx, function(err, txid) { - tx.serialize.callCount.should.equal(1); - should.exist(err); - done(); - }); - }); - }); - - describe('#estimateFee', function() { - it('should pass along the fee from bitcoind', function(done) { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - db.node.services.bitcoind = { - estimateFee: sinon.stub().returns(1000) - }; - - db.estimateFee(5, function(err, fee) { - should.not.exist(err); - fee.should.equal(1000); - db.node.services.bitcoind.estimateFee.args[0][0].should.equal(5); - done(); - }); - }); - }); - - describe('#connectBlock', function() { - it('should remove block from mempool and call blockHandler with true', function(done) { - var db = new DB(baseConfig); - db.mempool = { - removeBlock: sinon.stub() - }; - db.runAllBlockHandlers = sinon.stub().callsArg(2); - db.connectBlock({hash: 'hash'}, function(err) { - should.not.exist(err); - db.runAllBlockHandlers.args[0][1].should.equal(true); - done(); - }); - }); - }); - - describe('#disconnectBlock', function() { - it('should call blockHandler with false', function(done) { - var db = new DB(baseConfig); - db.runAllBlockHandlers = sinon.stub().callsArg(2); - db.disconnectBlock({hash: 'hash'}, function(err) { - should.not.exist(err); - db.runAllBlockHandlers.args[0][1].should.equal(false); - done(); - }); - }); - }); - - describe('#runAllBlockHandlers', function() { - var db = new DB(baseConfig); - var Service1 = function() {}; - Service1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']); - var Service2 = function() {}; - Service2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']); - var Service3 = function() {}; - var Service4 = function() {}; - Service4.prototype.blockHandler = sinon.stub().callsArgWith(2, null, 'bad-value'); - db.node = {}; - db.node.services = { - service1: new Service1(), - service2: new Service2() - }; - db.store = { - batch: sinon.stub().callsArg(1) - }; - - var block = { - hash: '00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863', - header: { - timestamp: 1441906365 - } - }; - - it('should call blockHandler in all services and perform operations', function(done) { - db.runAllBlockHandlers(block, true, function(err) { - should.not.exist(err); - var tipOp = { - type: 'put', - key: DB.PREFIXES.TIP, - value: new Buffer('00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863', 'hex') - } - var blockOp = { - type: 'put', - key: db._encodeBlockIndexKey(1441906365), - value: db._encodeBlockIndexValue('00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863') - }; - db.store.batch.args[0][0].should.deep.equal([tipOp, blockOp, 'op1', 'op2', 'op3', 'op4', 'op5']); - done(); - }); - }); - - it('should give an error if one of the services gives an error', function(done) { - var Service3 = function() {}; - Service3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error')); - db.node.services.service3 = new Service3(); - - db.runAllBlockHandlers(block, true, function(err) { - should.exist(err); - done(); - }); - }); - - it('should not give an error if a service does not have blockHandler', function(done) { - db.node = {}; - db.node.services = { - service3: new Service3() - }; - - db.runAllBlockHandlers(block, true, function(err) { - should.not.exist(err); - done(); - }); - }); - - it('should throw an error if blockHandler gives unexpected result', function() { - db.node = {}; - db.node.services = { - service4: new Service4() - }; - - (function() { - db.runAllBlockHandlers(block, true, function(err) { - should.not.exist(err); - }); - }).should.throw('bitcore.ErrorInvalidArgument'); - }); - }); - - describe('#getAPIMethods', function() { - it('should return the correct db methods', function() { - var db = new DB(baseConfig); - db.node = {}; - db.node.services = {}; - var methods = db.getAPIMethods(); - methods.length.should.equal(6); - }); - }); - - describe('#findCommonAncestor', function() { - it('will find an ancestor 6 deep', function(done) { - var db = new DB(baseConfig); - db.tip = { - hash: chainHashes[chainHashes.length - 1] - }; - - var expectedAncestor = chainHashes[chainHashes.length - 6]; - - var mainBlocks = {}; - for(var i = chainHashes.length - 1; i > chainHashes.length - 10; i--) { - var hash = chainHashes[i]; - var prevHash = hexlebuf(chainHashes[i - 1]); - mainBlocks[hash] = { - header: { - prevHash: prevHash - } - }; - } - - var forkedBlocks = { - 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82': { - header: { - prevHash: hexlebuf('76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a') - }, - hash: 'd7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82' - }, - '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a': { - header: { - prevHash: hexlebuf('f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c') - }, - hash: '76d920dbd83beca9fa8b2f346d5c5a81fe4a350f4b355873008229b1e6f8701a' - }, - 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c': { - header: { - prevHash: hexlebuf('2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31') - }, - hash: 'f0a0d76a628525243c8af7606ee364741ccd5881f0191bbe646c8a4b2853e60c' - }, - '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31': { - header: { - prevHash: hexlebuf('adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453') - }, - hash: '2f72b809d5ccb750c501abfdfa8c4c4fad46b0b66c088f0568d4870d6f509c31' - }, - 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453': { - header: { - prevHash: hexlebuf('3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618') - }, - hash: 'adf66e6ae10bc28fc22bc963bf43e6b53ef4429269bdb65038927acfe66c5453' - }, - '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618': { - header: { - prevHash: hexlebuf(expectedAncestor) - }, - hash: '3ea12707e92eed024acf97c6680918acc72560ec7112cf70ac213fb8bb4fa618' - } - }; - db.node.services = {}; - db.node.services.bitcoind = { - getBlockIndex: function(hash) { - var forkedBlock = forkedBlocks[hash]; - var mainBlock = mainBlocks[hash]; - var prevHash; - if (forkedBlock && forkedBlock.header.prevHash) { - prevHash = BufferUtil.reverse(forkedBlock.header.prevHash).toString('hex'); - } else if (mainBlock && mainBlock.header.prevHash){ - prevHash = BufferUtil.reverse(mainBlock.header.prevHash).toString('hex'); - } else { - return null; - } - return { - prevHash: prevHash - }; - } - }; - var block = forkedBlocks['d7fa6f3d5b2fe35d711e6aca5530d311b8c6e45f588a65c642b8baf4b4441d82']; - db.findCommonAncestor(block, function(err, ancestorHash) { - if (err) { - throw err; - } - ancestorHash.should.equal(expectedAncestor); - done(); - }); - }); - }); - - describe('#syncRewind', function() { - it('will undo blocks 6 deep', function() { - var db = new DB(baseConfig); - var ancestorHash = chainHashes[chainHashes.length - 6]; - db.tip = { - __height: 10, - hash: chainHashes[chainHashes.length], - header: { - prevHash: hexlebuf(chainHashes[chainHashes.length - 1]) - } - }; - db.emit = sinon.stub(); - db.getBlock = function(hash, callback) { - setImmediate(function() { - for(var i = chainHashes.length; i > 0; i--) { - var block = { - hash: chainHashes[i], - header: { - prevHash: hexlebuf(chainHashes[i - 1]) - } - }; - if (chainHashes[i] === hash) { - callback(null, block); - } - } - }); - }; - db.node.services = {}; - db.disconnectBlock = function(block, callback) { - setImmediate(callback); - }; - db.findCommonAncestor = function(block, callback) { - setImmediate(function() { - callback(null, ancestorHash); - }); - }; - var forkedBlock = {}; - db.syncRewind(forkedBlock, function(err) { - if (err) { - throw err; - } - db.tip.__height.should.equal(4); - db.tip.hash.should.equal(ancestorHash); - }); - }); - }); - - describe('#sync', function() { - var node = new EventEmitter(); - var syncConfig = { - node: node, - store: memdown - }; - syncConfig.node.network = Networks.testnet; - syncConfig.node.datadir = 'testdir'; - it('will get and add block up to the tip height', function(done) { - var db = new DB(syncConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var block = Block.fromBuffer(blockBuffer); - db.node.services = {}; - db.node.services.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true), - height: 1 - }; - db.tip = { - __height: 0, - hash: lebufhex(block.header.prevHash) - }; - db.emit = sinon.stub(); - db.cache = { - hashes: {} - }; - db.connectBlock = function(block, callback) { - db.tip.__height += 1; - callback(); - }; - db.node.once('synced', function() { - done(); - }); - db.sync(); - }); - it('will exit and emit error with error from bitcoind.getBlock', function(done) { - var db = new DB(syncConfig); - db.node.services = {}; - db.node.services.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, new Error('test error')), - height: 1 - }; - db.tip = { - __height: 0 - }; - db.node.on('error', function(err) { - err.message.should.equal('test error'); - done(); - }); - db.sync(); - }); - it('will stop syncing when the node is stopping', function(done) { - var db = new DB(syncConfig); - var blockBuffer = new Buffer(blockData, 'hex'); - var block = Block.fromBuffer(blockBuffer); - db.node.services = {}; - db.node.services.bitcoind = { - getBlock: sinon.stub().callsArgWith(1, null, blockBuffer), - isSynced: sinon.stub().returns(true), - height: 1 - }; - db.tip = { - __height: 0, - hash: block.prevHash - }; - db.emit = sinon.stub(); - db.cache = { - hashes: {} - }; - db.connectBlock = function(block, callback) { - db.tip.__height += 1; - callback(); - }; - db.node.stopping = true; - var synced = false; - db.node.once('synced', function() { - synced = true; - }); - db.sync(); - setTimeout(function() { - synced.should.equal(false); - done(); - }, 10); - }); - }); - -}); diff --git a/test/transaction.unit.js b/test/transaction.unit.js index eee529a4..0a78c8c5 100644 --- a/test/transaction.unit.js +++ b/test/transaction.unit.js @@ -4,9 +4,44 @@ var should = require('chai').should(); var sinon = require('sinon'); var bitcoinlib = require('../'); var Transaction = bitcoinlib.Transaction; -var levelup = require('levelup'); describe('Bitcoin Transaction', function() { + + describe('#populateSpentInfo', function() { + it('will call db.getSpentInfo with correct arguments', function(done) { + var tx = new Transaction(); + tx.to('1AGNa15ZQXAZUgFiqJ2i7Z2DPU2J6hW62i', 1000); + tx.to('3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', 2000); + var expectedHash = tx.hash; + var expectedIndex = 2; + var expectedHeight = 300000; + var db = { + getSpentInfo: sinon.stub().callsArgWith(1, null, { + txid: expectedHash, + index: expectedIndex, + height: expectedHeight + }) + }; + tx.populateSpentInfo(db, {}, function(err) { + if (err) { + return done(err); + } + db.getSpentInfo.args[0][0].txid.should.equal(tx.hash); + db.getSpentInfo.args[0][0].index.should.equal(0); + tx.outputs[0].__spentTxId.should.equal(expectedHash); + tx.outputs[0].__spentIndex.should.equal(expectedIndex); + tx.outputs[0].__spentHeight.should.equal(expectedHeight); + + db.getSpentInfo.args[1][0].txid.should.equal(tx.hash); + db.getSpentInfo.args[1][0].index.should.equal(1); + tx.outputs[1].__spentTxId.should.equal(expectedHash); + tx.outputs[1].__spentIndex.should.equal(expectedIndex); + tx.outputs[1].__spentHeight.should.equal(expectedHeight); + done(); + }); + }); + }); + describe('#populateInputs', function() { it('will call _populateInput with transactions', function() { var tx = new Transaction(); @@ -22,6 +57,17 @@ describe('Bitcoin Transaction', function() { tx._populateInput.args[0][2].should.equal(transactions); }); }); + it('will skip coinbase transactions', function() { + var tx = new Transaction(); + tx.isCoinbase = sinon.stub().returns(true); + tx._populateInput = sinon.stub().callsArg(3); + tx.inputs = ['input']; + var transactions = []; + var db = {}; + tx.populateInputs(db, transactions, function(err) { + tx._populateInput.callCount.should.equal(0); + }); + }); }); describe('#_populateInput', function() { @@ -29,6 +75,15 @@ describe('Bitcoin Transaction', function() { prevTxId: new Buffer('d6cffbb343a6a41eeaa199478c985493843bfe6a59d674a5c188787416cbcda3', 'hex'), outputIndex: 0 }; + it('should give an error if the input does not have a prevTxId', function(done) { + var badInput = {}; + var tx = new Transaction(); + tx._populateInput({}, badInput, [], function(err) { + should.exist(err); + err.message.should.equal('Input is expected to have prevTxId as a buffer'); + done(); + }); + }); it('should give an error if the input does not have a valid prevTxId', function(done) { var badInput = { prevTxId: 'bad' @@ -43,7 +98,7 @@ describe('Bitcoin Transaction', function() { it('if an error happened it should pass it along', function(done) { var tx = new Transaction(); var db = { - getTransaction: sinon.stub().callsArgWith(2, new Error('error')) + getTransaction: sinon.stub().callsArgWith(1, new Error('error')) }; tx._populateInput(db, input, [], function(err) { should.exist(err); @@ -54,7 +109,7 @@ describe('Bitcoin Transaction', function() { it('should return an error if the transaction for the input does not exist', function(done) { var tx = new Transaction(); var db = { - getTransaction: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()) + getTransaction: sinon.stub().callsArgWith(1, null, null) }; tx._populateInput(db, input, [], function(err) { should.exist(err); @@ -65,7 +120,7 @@ describe('Bitcoin Transaction', function() { it('should look through poolTransactions if database does not have transaction', function(done) { var tx = new Transaction(); var db = { - getTransaction: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError()) + getTransaction: sinon.stub().callsArgWith(1, null, null) }; var transactions = [ { @@ -79,12 +134,12 @@ describe('Bitcoin Transaction', function() { done(); }); }); - it('should not return an error if an error did not occur', function(done) { + it('should set the output on the input', function(done) { var prevTx = new Transaction(); prevTx.outputs = ['output']; var tx = new Transaction(); var db = { - getTransaction: sinon.stub().callsArgWith(2, null, prevTx) + getTransaction: sinon.stub().callsArgWith(1, null, prevTx) }; tx._populateInput(db, input, [], function(err) { should.not.exist(err); @@ -94,27 +149,4 @@ describe('Bitcoin Transaction', function() { }); }); - describe('#_checkSpent', function() { - it('should return an error if input was spent', function(done) { - var tx = new Transaction(); - var db = { - isSpentDB: sinon.stub().callsArgWith(1, true) - }; - tx._checkSpent(db, [], 'input', function(err) { - should.exist(err); - err.message.should.equal('Input already spent'); - done(); - }); - }); - it('should not return an error if input was unspent', function(done) { - var tx = new Transaction(); - var db = { - isSpentDB: sinon.stub().callsArgWith(1, false) - }; - tx._checkSpent(db, [], 'input', function(err) { - should.not.exist(err); - done(); - }); - }); - }); });