From 78bc054bfaf4c7071c9760d963bd6182297685b3 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Thu, 6 Aug 2015 16:19:36 -0400 Subject: [PATCH 1/8] changes for BWS integration --- bin/start.js | 13 ++--- example/client.js | 13 +++-- integration/regtest.js | 3 +- lib/block.js | 4 -- lib/bus.js | 59 ++++++++++++---------- lib/db.js | 90 +++++++++++++++++++++++++++++++++- lib/modules/address.js | 95 +++++++++++++++++++++++++++--------- lib/node.js | 11 +---- lib/transaction.js | 38 ++++++++++++++- lib/utils.js | 45 +++++++++++++++++ src/libbitcoind.cc | 5 +- test/bus.unit.js | 84 +++++++++++++++++++++++-------- test/db.unit.js | 4 +- test/modules/address.unit.js | 70 ++++++++++++++------------ test/node.unit.js | 4 +- 15 files changed, 404 insertions(+), 134 deletions(-) create mode 100644 lib/utils.js diff --git a/bin/start.js b/bin/start.js index 4da2476c..ea53eaca 100644 --- a/bin/start.js +++ b/bin/start.js @@ -5,6 +5,7 @@ var chainlib = require('chainlib'); var socketio = require('socket.io'); var log = chainlib.log; log.debug = function() {}; +var utils = require('../lib/utils'); var configuration = { datadir: process.env.BITCORENODE_DIR || '~/.bitcoin', @@ -77,11 +78,7 @@ node.on('ready', function() { } if(result) { - if(result.toJSON) { - response.result = result.toJSON(); - } else { - response.result = result; - } + response.result = utils.expandObject(result); } socketCallback(response); @@ -114,11 +111,7 @@ node.on('ready', function() { var results = []; for(var i = 0; i < arguments.length; i++) { - if(arguments[i].toJSON) { - results.push(arguments[i].toJSON()); - } else { - results.push(arguments[i]); - } + results.push(utils.expandObject(arguments[i])); } var params = [event.name].concat(results); diff --git a/example/client.js b/example/client.js index 88a1cf9c..ded2b61e 100644 --- a/example/client.js +++ b/example/client.js @@ -11,7 +11,7 @@ socket.on('disconnect', function(){ var message = { method: 'getOutputs', - params: ['1HTxCVrXuthad6YW5895K98XmVsdMvvBSw', true] + params: ['2NChMRHVCxTPq9KeyvHQUSbfLaQY55Zzzp8', true] }; socket.send(message, function(response) { @@ -37,8 +37,13 @@ socket.send(message2, function(response) { console.log(response.result); }); -socket.on('transaction', function(address, block) { - console.log(address, block); +socket.on('transaction', function(obj) { + console.log(JSON.stringify(obj, null, 2)); }); -socket.emit('subscribe', 'transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); \ No newline at end of file +socket.on('address/transaction', function(obj) { + console.log(JSON.stringify(obj, null, 2)); +}); + +socket.emit('subscribe', 'transaction'); +socket.emit('subscribe', 'address/transaction', ['13FMwCYz3hUhwPcaWuD2M1U2KzfTtvLM89']); \ No newline at end of file diff --git a/integration/regtest.js b/integration/regtest.js index 1fcd219c..d309da6b 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -306,7 +306,8 @@ describe('Daemon Binding Functionality', function() { var outputs = bitcoind.getMempoolOutputs(changeAddress); var expected = [ { - script: 'OP_DUP OP_HASH160 073b7eae2823efa349e3b9155b8a735526463a0f OP_EQUALVERIFY OP_CHECKSIG', + address: 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up', + script: '76a914073b7eae2823efa349e3b9155b8a735526463a0f88ac', satoshis: 40000, txid: tx.hash, outputIndex: 1 diff --git a/lib/block.js b/lib/block.js index e8499c31..6e5e9c13 100644 --- a/lib/block.js +++ b/lib/block.js @@ -60,10 +60,6 @@ Block.prototype.toObject = function() { }; }; -Block.prototype.toJSON = function() { - return JSON.stringify(this.toObject()); -}; - Block.prototype.headerToBufferWriter = function(bw) { /* jshint maxstatements: 20 */ diff --git a/lib/bus.js b/lib/bus.js index 8772f3f8..325c09e1 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -11,44 +11,53 @@ function Bus(params) { util.inherits(Bus, events.EventEmitter); Bus.prototype.subscribe = function(name) { - for (var i = 0; i < this.db.modules.length; i++) { + var events = this.db.getPublishEvents(); + + for(var i = 0; i < this.db.modules.length; i++) { var mod = this.db.modules[i]; - var events = mod.getPublishEvents(); - for (var j = 0; j < events.length; j++) { - var event = events[j]; - var params = Array.prototype.slice.call(arguments).slice(1); - params.unshift(this); - if (name === event.name) { - event.subscribe.apply(event.scope, params); - } + events = events.concat(mod.getPublishEvents()); + } + + for (var j = 0; j < events.length; j++) { + var event = events[j]; + var params = Array.prototype.slice.call(arguments).slice(1); + params.unshift(this); + if (name === event.name) { + event.subscribe.apply(event.scope, params); } } }; Bus.prototype.unsubscribe = function(name) { - for (var i = 0; i < this.db.modules.length; i++) { + var events = this.db.getPublishEvents(); + + for(var i = 0; i < this.db.modules.length; i++) { var mod = this.db.modules[i]; - var events = mod.getPublishEvents(); - for (var j = 0; j < events.length; j++) { - var event = events[j]; - var params = Array.prototype.slice.call(arguments).slice(1); - params.unshift(this); - if (name === event.name) { - event.unsubscribe.apply(event.scope, params); - } + events = events.concat(mod.getPublishEvents()); + } + + for (var j = 0; j < events.length; j++) { + var event = events[j]; + var params = Array.prototype.slice.call(arguments).slice(1); + params.unshift(this); + if (name === event.name) { + event.unsubscribe.apply(event.scope, params); } } }; Bus.prototype.close = function() { - // Unsubscribe from all events - for (var i = 0; i < this.db.modules.length; i++) { + var events = this.db.getPublishEvents(); + + for(var i = 0; i < this.db.modules.length; i++) { var mod = this.db.modules[i]; - var events = mod.getPublishEvents(); - for (var j = 0; j < events.length; j++) { - var event = events[j]; - event.unsubscribe.call(event.scope, this); - } + events = events.concat(mod.getPublishEvents()); + } + + // Unsubscribe from all events + for (var j = 0; j < events.length; j++) { + var event = events[j]; + event.unsubscribe.call(event.scope, this); } }; diff --git a/lib/db.js b/lib/db.js index 2420533e..efae1f4f 100644 --- a/lib/db.js +++ b/lib/db.js @@ -32,10 +32,25 @@ function DB(options) { this.modules = []; + this.subscriptions = { + transaction: [], + block: [] + }; } util.inherits(DB, BaseDB); +DB.prototype.initialize = function() { + // Add all db option modules + if(this._modules && this._modules.length) { + for(var i = 0; i < this._modules.length; i++) { + this.addModule(this._modules[i]); + } + } + this.bitcoind.on('tx', this.transactionHandler.bind(this)); + this.emit('ready'); +} + DB.prototype.getBlock = function(hash, callback) { var self = this; @@ -91,6 +106,34 @@ DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback }); }; +DB.prototype.sendTransaction = function(tx, callback) { + if(tx instanceof this.Transaction) { + tx = tx.toString(); + } + $.checkArgument(typeof tx === 'string', 'Argument must be a hex string or Transaction'); + + try { + var txid = this.bitcoind.sendTransaction(tx); + return callback(null, txid); + } catch(err) { + return callback(err); + } +}; + +DB.prototype.estimateFee = function(blocks, callback) { + var self = this; + + // For some reason getting fee for 1 block returns -1 + // Until this is resolved, just make it 2 blocks + if(blocks === 1) { + blocks = 2; + } + + setImmediate(function() { + callback(null, self.bitcoind.estimateFee(blocks)); + }); +} + DB.prototype.validateBlockData = function(block, callback) { // bitcoind does the validation setImmediate(callback); @@ -195,6 +238,11 @@ DB.prototype.blockHandler = function(block, add, callback) { var self = this; var operations = []; + // Notify block subscribers + for(var i = 0; i < this.subscriptions.block.length; i++) { + this.subscriptions.transaction[i].emit('block', block.hash); + } + async.eachSeries( this.modules, function(module, next) { @@ -222,7 +270,9 @@ DB.prototype.blockHandler = function(block, add, callback) { DB.prototype.getAPIMethods = function() { var methods = [ ['getBlock', this, this.getBlock, 1], - ['getTransaction', this, this.getTransaction, 2] + ['getTransaction', this, this.getTransaction, 2], + ['sendTransaction', this, this.sendTransaction, 1], + ['estimateFee', this, this.estimateFee, 1] ]; for(var i = 0; i < this.modules.length; i++) { @@ -232,6 +282,23 @@ DB.prototype.getAPIMethods = function() { return methods; }; +DB.prototype.getPublishEvents = function() { + return [ + { + name: 'transaction', + scope: this, + subscribe: this.subscribe.bind(this, 'transaction'), + unsubscribe: this.unsubscribe.bind(this, 'transaction') + }, + { + name: 'block', + scope: this, + subscribe: this.subscribe.bind(this, 'block'), + unsubscribe: this.unsubscribe.bind(this, 'block') + } + ]; +}; + DB.prototype.addModule = function(Module) { var module = new Module({ db: this @@ -240,4 +307,25 @@ DB.prototype.addModule = function(Module) { this.modules.push(module); }; +DB.prototype.subscribe = function(name, emitter) { + this.subscriptions[name].push(emitter); +}; + +DB.prototype.unsubscribe = function(name, emitter) { + var index = this.subscriptions[name].indexOf(emitter); + if(index > -1) { + this.subscriptions[name].splice(index, 1); + } +}; + +DB.prototype.transactionHandler = function(txInfo) { + var tx = bitcore.Transaction().fromBuffer(txInfo.buffer); + for(var i = 0; i < this.subscriptions.transaction.length; i++) { + this.subscriptions.transaction[i].emit('transaction', { + rejected: !txInfo.mempool, + tx: tx + }); + } +}; + module.exports = DB; diff --git a/lib/modules/address.js b/lib/modules/address.js index caac9cef..abf3689c 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -18,8 +18,8 @@ var AddressModule = function(options) { BaseModule.call(this, options); this.subscriptions = {}; - this.subscriptions.transaction = {}; - this.subscriptions.balance = {}; + this.subscriptions['address/transaction'] = {}; + this.subscriptions['address/balance'] = {}; this.db.bitcoind.on('tx', this.transactionHandler.bind(this)); @@ -45,17 +45,17 @@ AddressModule.prototype.getAPIMethods = function() { AddressModule.prototype.getPublishEvents = function() { return [ { - name: 'transaction', + name: 'address/transaction', scope: this, - subscribe: this.subscribe.bind(this, 'transaction'), - unsubscribe: this.unsubscribe.bind(this, 'transaction') + subscribe: this.subscribe.bind(this, 'address/transaction'), + unsubscribe: this.unsubscribe.bind(this, 'address/transaction') }, { - name: 'balance', + name: 'address/balance', scope: this, - subscribe: this.subscribe.bind(this, 'balance'), - unsubscribe: this.unsubscribe.bind(this, 'balance') - } + subscribe: this.subscribe.bind(this, 'address/balance'), + unsubscribe: this.unsubscribe.bind(this, 'address/balance') + }, ]; }; @@ -123,7 +123,6 @@ AddressModule.prototype.transactionHandler = function(txInfo) { for (var key in messages) { this.transactionEventHandler(messages[key]); } - }; AddressModule.prototype.blockHandler = function(block, addOutput, callback) { @@ -234,24 +233,24 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { * @param {Boolean} [obj.rejected] - If the transaction was not accepted in the mempool */ AddressModule.prototype.transactionEventHandler = function(obj) { - if(this.subscriptions.transaction[obj.address]) { - var emitters = this.subscriptions.transaction[obj.address]; - for(var k = 0; k < emitters.length; k++) { - emitters[k].emit('transaction', obj); + if(this.subscriptions['address/transaction'][obj.address]) { + var emitters = this.subscriptions['address/transaction'][obj.address]; + for(var i = 0; i < emitters.length; i++) { + emitters[i].emit('address/transaction', obj); } } }; AddressModule.prototype.balanceEventHandler = function(block, address) { - if(this.subscriptions.balance[address]) { - var emitters = this.subscriptions.balance[address]; + if(this.subscriptions['address/balance'][address]) { + var emitters = this.subscriptions['address/balance'][address]; this.getBalance(address, true, function(err, balance) { if(err) { return this.emit(err); } for(var i = 0; i < emitters.length; i++) { - emitters[i].emit('balance', address, balance, block); + emitters[i].emit('address/balance', address, balance, block); } }); } @@ -338,9 +337,11 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback address: addressStr, txid: key[3], outputIndex: Number(key[4]), + timestamp: key[2], satoshis: Number(value[0]), script: value[1], - blockHeight: Number(value[2]) + blockHeight: Number(value[2]), + confirmations: self.db.chain.tip.__height - Number(value[2]) + 1 }; outputs.push(output); @@ -371,7 +372,32 @@ AddressModule.prototype.getOutputs = function(addressStr, queryMempool, callback }; -AddressModule.prototype.getUnspentOutputs = function(address, queryMempool, callback) { +AddressModule.prototype.getUnspentOutputs = function(addresses, queryMempool, callback) { + var self = this; + + if(!Array.isArray(addresses)) { + addresses = [addresses]; + } + + var utxos = []; + + async.eachSeries(addresses, function(address, next) { + self.getUnspentOutputsForAddress(address, queryMempool, function(err, unspents) { + if(err && err instanceof errors.NoOutputs) { + return next(); + } else if(err) { + return next(err); + } + + utxos = utxos.concat(unspents); + next(); + }); + }, function(err) { + callback(err, utxos); + }); +}; + +AddressModule.prototype.getUnspentOutputsForAddress = function(address, queryMempool, callback) { var self = this; @@ -427,7 +453,30 @@ AddressModule.prototype.getSpendInfoForOutput = function(txid, outputIndex, call }); }; -AddressModule.prototype.getAddressHistory = function(address, queryMempool, callback) { +AddressModule.prototype.getAddressHistory = function(addresses, queryMempool, callback) { + var self = this; + + if(!Array.isArray(addresses)) { + addresses = [addresses]; + } + + var history = []; + + async.eachSeries(addresses, function(address, next) { + self.getAddressHistoryForAddress(address, queryMempool, function(err, h) { + if(err) { + return next(err); + } + + history = history.concat(h); + next(); + }); + }, function(err) { + callback(err, history); + }); +}; + +AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMempool, callback) { var self = this; var txinfos = {}; @@ -448,12 +497,14 @@ AddressModule.prototype.getAddressHistory = function(address, queryMempool, call } txinfos[transaction.hash] = { + address: address, satoshis: 0, height: transaction.__height, + confirmations: self.db.chain.tip.__height - transaction.__height + 1, timestamp: transaction.__timestamp, outputIndexes: [], inputIndexes: [], - transaction: transaction + tx: transaction }; callback(null, txinfos[transaction.hash]); @@ -490,7 +541,7 @@ AddressModule.prototype.getAddressHistory = function(address, queryMempool, call } txinfo.inputIndexes.push(spendInfo.inputIndex); - txinfo.satoshis -= txinfo.transaction.inputs[spendInfo.inputIndex].output.satoshis; + txinfo.satoshis -= txinfo.tx.inputs[spendInfo.inputIndex].output.satoshis; next(); }); }); diff --git a/lib/node.js b/lib/node.js index 48e5f022..a4f6bee7 100644 --- a/lib/node.js +++ b/lib/node.js @@ -39,7 +39,7 @@ Node.prototype.getAllAPIMethods = function() { }; Node.prototype.getAllPublishEvents = function() { - var events = []; + var events = this.db.getPublishEvents(); for (var i = 0; i < this.db.modules.length; i++) { var mod = this.db.modules[i]; events = events.concat(mod.getPublishEvents()); @@ -407,15 +407,6 @@ Node.prototype._initializeDatabase = function() { // Database this.db.on('ready', function() { - - // Add all db option modules - var modules = self.db._modules; - if(modules && modules.length) { - for(var i = 0; i < modules.length; i++) { - self.db.addModule(modules[i]); - } - } - log.info('Bitcoin Database Ready'); self.chain.initialize(); }); diff --git a/lib/transaction.js b/lib/transaction.js index 3ba209b4..a971837f 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -7,6 +7,7 @@ var chainlib = require('chainlib'); var BaseTransaction = chainlib.Transaction; var BaseDatabase = chainlib.DB; var levelup = chainlib.deps.levelup; +var _ = bitcore.deps._; Transaction.prototype.populateInputs = function(db, poolTransactions, callback) { var self = this; @@ -29,7 +30,7 @@ Transaction.prototype._populateInput = function(db, input, poolTransactions, cal return callback(new Error('Input is expected to have prevTxId as a buffer')); } var txid = input.prevTxId.toString('hex'); - db.getTransaction(txid, false, function(err, prevTx) { + db.getTransaction(txid, true, function(err, prevTx) { if(err instanceof levelup.errors.NotFoundError) { // Check the pool for transaction for(var i = 0; i < poolTransactions.length; i++) { @@ -64,4 +65,39 @@ Transaction.manyToBuffer = function(transactions) { return BaseTransaction.manyToBuffer(transactions); }; +/** + * Override Bitcore's toObject() to include populated inputs and txid + */ +Transaction.prototype.toObject = function toObject() { + var inputs = []; + this.inputs.forEach(function(input) { + var inputObj = input.toObject(); + if(input.output) { + inputObj.output = input.output.toObject(); + } + inputs.push(inputObj); + }); + var outputs = []; + this.outputs.forEach(function(output) { + outputs.push(output.toObject()); + }); + var obj = { + txid: this.id, + version: this.version, + inputs: inputs, + outputs: outputs, + nLockTime: this.nLockTime + }; + if (this._changeScript) { + obj.changeScript = this._changeScript.toString(); + } + if (!_.isUndefined(this._changeIndex)) { + obj.changeIndex = this._changeIndex; + } + if (!_.isUndefined(this._fee)) { + obj.fee = this._fee; + } + return obj; +}; + module.exports = Transaction; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..ffdf34b9 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,45 @@ +'use strict'; + +var chainlib = require('chainlib'); +var utils = chainlib.utils; + +/** + * Bitcore's API does not implement toJSON in the standard way. + * This causes issues when doing a JSON.stringify on an object + * which contains Bitcore objects. This custom implmentation + * of stringify accounts for Bitcore objects. + * @param {Object} obj + * @return {String} json + */ +utils.stringify = function(obj) { + return JSON.stringify(utils.expandObject(obj)); +} + +utils.expandObject = function(obj) { + if(Array.isArray(obj)) { + var expandedArray = []; + for(var i = 0; i < obj.length; i++) { + expandedArray.push(utils.expandObject(obj[i])); + } + + return expandedArray; + } else if(typeof obj === 'function' || typeof obj === 'object') { + if(obj.toObject) { + return obj.toObject(); + } else if(obj.toJSON) { + return obj.toJSON(); + } else { + var expandedObj = {}; + + for(var key in obj) { + expandedObj[key] = utils.expandObject(obj[key]); + } + + return expandedObj; + } + } else { + return obj; + } +}; + +module.exports = utils; \ No newline at end of file diff --git a/src/libbitcoind.cc b/src/libbitcoind.cc index 5753eaa8..428e4ae6 100644 --- a/src/libbitcoind.cc +++ b/src/libbitcoind.cc @@ -1566,7 +1566,10 @@ NAN_METHOD(GetMempoolOutputs) { Local output = NanNew(); - output->Set(NanNew("script"), NanNew(script.ToString())); + output->Set(NanNew("address"), NanNew(psz)); + + std::string scriptHex = HexStr(script.begin(), script.end()); + output->Set(NanNew("script"), NanNew(scriptHex)); uint64_t satoshis = txout.nValue; output->Set(NanNew("satoshis"), NanNew(satoshis)); // can't go above 2 ^ 53 -1 diff --git a/test/bus.unit.js b/test/bus.unit.js index a0d4f249..44c4c662 100644 --- a/test/bus.unit.js +++ b/test/bus.unit.js @@ -7,68 +7,107 @@ var Bus = require('../lib/bus'); describe('Bus', function() { describe('#subscribe', function() { - it('will call modules subscribe function with the correct arguments', function() { - var subscribe = sinon.spy(); + it('will call db and modules subscribe function with the correct arguments', function() { + var subscribeDb = sinon.spy(); + var subscribeModule = sinon.spy(); var db = { + getPublishEvents: sinon.stub().returns([ + { + name: 'dbtest', + scope: this, + subscribe: subscribeDb + } + ] + ), modules: [ { getPublishEvents: sinon.stub().returns([ { name: 'test', scope: this, - subscribe: subscribe, + subscribe: subscribeModule, } ]) } ] }; var bus = new Bus({db: db}); + bus.subscribe('dbtest', 'a', 'b', 'c'); bus.subscribe('test', 'a', 'b', 'c'); - subscribe.callCount.should.equal(1); - subscribe.args[0][0].should.equal(bus); - subscribe.args[0][1].should.equal('a'); - subscribe.args[0][2].should.equal('b'); - subscribe.args[0][3].should.equal('c'); + subscribeModule.callCount.should.equal(1); + subscribeDb.callCount.should.equal(1); + subscribeDb.args[0][0].should.equal(bus); + subscribeDb.args[0][1].should.equal('a'); + subscribeDb.args[0][2].should.equal('b'); + subscribeDb.args[0][3].should.equal('c'); + subscribeModule.args[0][0].should.equal(bus); + subscribeModule.args[0][1].should.equal('a'); + subscribeModule.args[0][2].should.equal('b'); + subscribeModule.args[0][3].should.equal('c'); }); }); describe('#unsubscribe', function() { - it('will call modules unsubscribe function with the correct arguments', function() { - var unsubscribe = sinon.spy(); + it('will call db and modules unsubscribe function with the correct arguments', function() { + var unsubscribeDb = sinon.spy(); + var unsubscribeModule = sinon.spy(); var db = { + getPublishEvents: sinon.stub().returns([ + { + name: 'dbtest', + scope: this, + unsubscribe: unsubscribeDb + } + ] + ), modules: [ { getPublishEvents: sinon.stub().returns([ { name: 'test', scope: this, - unsubscribe: unsubscribe + unsubscribe: unsubscribeModule, } ]) } ] }; var bus = new Bus({db: db}); + bus.unsubscribe('dbtest', 'a', 'b', 'c'); bus.unsubscribe('test', 'a', 'b', 'c'); - unsubscribe.callCount.should.equal(1); - unsubscribe.args[0][0].should.equal(bus); - unsubscribe.args[0][1].should.equal('a'); - unsubscribe.args[0][2].should.equal('b'); - unsubscribe.args[0][3].should.equal('c'); + unsubscribeModule.callCount.should.equal(1); + unsubscribeDb.callCount.should.equal(1); + unsubscribeDb.args[0][0].should.equal(bus); + unsubscribeDb.args[0][1].should.equal('a'); + unsubscribeDb.args[0][2].should.equal('b'); + unsubscribeDb.args[0][3].should.equal('c'); + unsubscribeModule.args[0][0].should.equal(bus); + unsubscribeModule.args[0][1].should.equal('a'); + unsubscribeModule.args[0][2].should.equal('b'); + unsubscribeModule.args[0][3].should.equal('c'); }); }); describe('#close', function() { it('will unsubscribe from all events', function() { - var unsubscribe = sinon.spy(); + var unsubscribeDb = sinon.spy(); + var unsubscribeModule = sinon.spy(); var db = { + getPublishEvents: sinon.stub().returns([ + { + name: 'dbtest', + scope: this, + unsubscribe: unsubscribeDb + } + ] + ), modules: [ { getPublishEvents: sinon.stub().returns([ { name: 'test', scope: this, - unsubscribe: unsubscribe + unsubscribe: unsubscribeModule } ]) } @@ -78,9 +117,12 @@ describe('Bus', function() { var bus = new Bus({db: db}); bus.close(); - unsubscribe.callCount.should.equal(1); - unsubscribe.args[0].length.should.equal(1); - unsubscribe.args[0][0].should.equal(bus); + unsubscribeDb.callCount.should.equal(1); + unsubscribeModule.callCount.should.equal(1); + unsubscribeDb.args[0].length.should.equal(1); + unsubscribeDb.args[0][0].should.equal(bus); + unsubscribeModule.args[0].length.should.equal(1); + unsubscribeModule.args[0][0].should.equal(bus); }); }); diff --git a/test/db.unit.js b/test/db.unit.js index b1790bdb..b52a8827 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -301,7 +301,7 @@ describe('Bitcoin DB', function() { var db = new DB({store: memdown}); db.modules = []; var methods = db.getAPIMethods(); - methods.length.should.equal(2); + methods.length.should.equal(4); }); it('should also return modules API methods', function() { @@ -325,7 +325,7 @@ describe('Bitcoin DB', function() { db.modules = [module1, module2]; var methods = db.getAPIMethods(); - methods.length.should.equal(5); + methods.length.should.equal(7); }); }); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index ce6ad882..bb528588 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -257,7 +257,7 @@ describe('AddressModule', function() { it('will emit a transaction if there is a subscriber', function(done) { var am = new AddressModule({db: mockdb}); var emitter = new EventEmitter(); - am.subscriptions.transaction = { + am.subscriptions['address/transaction'] = { '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter] }; var block = { @@ -265,7 +265,7 @@ describe('AddressModule', function() { timestamp: new Date() }; var tx = {}; - emitter.on('transaction', function(obj) { + emitter.on('address/transaction', function(obj) { obj.address.should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); obj.tx.should.equal(tx); obj.timestamp.should.equal(block.timestamp); @@ -287,13 +287,13 @@ describe('AddressModule', function() { it('will emit a balance if there is a subscriber', function(done) { var am = new AddressModule({db: mockdb}); var emitter = new EventEmitter(); - am.subscriptions.balance = { + am.subscriptions['address/balance'] = { '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter] }; var block = {}; var balance = 1000; am.getBalance = sinon.stub().callsArgWith(2, null, balance); - emitter.on('balance', function(address, bal, b) { + emitter.on('address/balance', function(address, bal, b) { address.should.equal('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'); bal.should.equal(balance); b.should.equal(block); @@ -309,33 +309,33 @@ describe('AddressModule', function() { var emitter = new EventEmitter(); var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'; - var name = 'transaction'; + var name = 'address/transaction'; am.subscribe(name, emitter, [address]); - am.subscriptions.transaction[address].should.deep.equal([emitter]); + am.subscriptions['address/transaction'][address].should.deep.equal([emitter]); var address2 = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; am.subscribe(name, emitter, [address2]); - am.subscriptions.transaction[address2].should.deep.equal([emitter]); + am.subscriptions['address/transaction'][address2].should.deep.equal([emitter]); var emitter2 = new EventEmitter(); am.subscribe(name, emitter2, [address]); - am.subscriptions.transaction[address].should.deep.equal([emitter, emitter2]); + am.subscriptions['address/transaction'][address].should.deep.equal([emitter, emitter2]); }); it('will add an emitter to the subscribers array (balance)', function() { var am = new AddressModule({db: mockdb}); var emitter = new EventEmitter(); - var name = 'balance'; + var name = 'address/balance'; var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'; am.subscribe(name, emitter, [address]); - am.subscriptions.balance[address].should.deep.equal([emitter]); + am.subscriptions['address/balance'][address].should.deep.equal([emitter]); var address2 = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; am.subscribe(name, emitter, [address2]); - am.subscriptions.balance[address2].should.deep.equal([emitter]); + am.subscriptions['address/balance'][address2].should.deep.equal([emitter]); var emitter2 = new EventEmitter(); am.subscribe(name, emitter2, [address]); - am.subscriptions.balance[address].should.deep.equal([emitter, emitter2]); + am.subscriptions['address/balance'][address].should.deep.equal([emitter, emitter2]); }); }); @@ -345,31 +345,31 @@ describe('AddressModule', function() { var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'; - am.subscriptions.transaction[address] = [emitter, emitter2]; - var name = 'transaction'; + am.subscriptions['address/transaction'][address] = [emitter, emitter2]; + var name = 'address/transaction'; am.unsubscribe(name, emitter, [address]); - am.subscriptions.transaction[address].should.deep.equal([emitter2]); + am.subscriptions['address/transaction'][address].should.deep.equal([emitter2]); }); it('will remove emitter from subscribers array (balance)', function() { var am = new AddressModule({db: mockdb}); var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); var address = '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N'; - var name = 'balance'; - am.subscriptions.balance[address] = [emitter, emitter2]; + var name = 'address/balance'; + am.subscriptions['address/balance'][address] = [emitter, emitter2]; am.unsubscribe(name, emitter, [address]); - am.subscriptions.balance[address].should.deep.equal([emitter2]); + am.subscriptions['address/balance'][address].should.deep.equal([emitter2]); }); it('should unsubscribe from all addresses if no addresses are specified', function() { var am = new AddressModule({db: mockdb}); var emitter = new EventEmitter(); var emitter2 = new EventEmitter(); - am.subscriptions.balance = { + am.subscriptions['address/balance'] = { '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W': [emitter, emitter2], '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter2, emitter] }; - am.unsubscribe('balance', emitter); - am.subscriptions.balance.should.deep.equal({ + am.unsubscribe('address/balance', emitter); + am.subscriptions['address/balance'].should.deep.equal({ '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W': [emitter2], '1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N': [emitter2] }); @@ -408,6 +408,11 @@ describe('AddressModule', function() { var db = { bitcoind: { on: sinon.stub() + }, + chain: { + tip: { + __height: 1 + } } }; @@ -489,7 +494,7 @@ describe('AddressModule', function() { }); }); - describe('#getUnspentOutputs', function() { + describe('#getUnspentOutputsForAddress', function() { it('should filter out spent outputs', function(done) { var outputs = [ { @@ -514,7 +519,7 @@ describe('AddressModule', function() { i++; }; - am.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { + am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { should.not.exist(err); outputs.length.should.equal(2); outputs[0].satoshis.should.equal(1000); @@ -525,7 +530,7 @@ describe('AddressModule', function() { it('should handle an error from getOutputs', function(done) { var am = new AddressModule({db: mockdb}); am.getOutputs = sinon.stub().callsArgWith(2, new Error('error')); - am.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { + am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { should.exist(err); err.message.should.equal('error'); done(); @@ -534,7 +539,7 @@ describe('AddressModule', function() { it('should handle when there are no outputs', function(done) { var am = new AddressModule({db: mockdb}); am.getOutputs = sinon.stub().callsArgWith(2, null, []); - am.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { + am.getUnspentOutputsForAddress('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { should.exist(err); err.should.be.instanceof(errors.NoOutputs); outputs.length.should.equal(0); @@ -709,6 +714,11 @@ describe('AddressModule', function() { }, bitcoind: { on: sinon.stub() + }, + chain: { + tip: { + __height: 1 + } } }; var am = new AddressModule({db: db}); @@ -733,23 +743,23 @@ describe('AddressModule', function() { it('should give transaction history for an address', function(done) { am.getAddressHistory('address', true, function(err, history) { should.not.exist(err); - history[0].transaction.hash.should.equal('tx1'); + history[0].tx.hash.should.equal('tx1'); history[0].satoshis.should.equal(5000); history[0].height.should.equal(1); history[0].timestamp.should.equal(1438289011844); - history[1].transaction.hash.should.equal('tx2'); + history[1].tx.hash.should.equal('tx2'); history[1].satoshis.should.equal(-5000); history[1].height.should.equal(2); history[1].timestamp.should.equal(1438289021844); - history[2].transaction.hash.should.equal('tx3'); + history[2].tx.hash.should.equal('tx3'); history[2].satoshis.should.equal(2000); history[2].height.should.equal(3); history[2].timestamp.should.equal(1438289031844); - history[3].transaction.hash.should.equal('tx4'); + history[3].tx.hash.should.equal('tx4'); history[3].satoshis.should.equal(3000); history[3].height.should.equal(4); history[3].timestamp.should.equal(1438289041844); - history[4].transaction.hash.should.equal('tx5'); + history[4].tx.hash.should.equal('tx5'); history[4].satoshis.should.equal(-3000); history[4].height.should.equal(5); history[4].timestamp.should.equal(1438289051844); diff --git a/test/node.unit.js b/test/node.unit.js index b6de7661..674f3ed4 100644 --- a/test/node.unit.js +++ b/test/node.unit.js @@ -71,6 +71,7 @@ describe('Bitcoind Node', function() { it('should return modules publish events', function() { var node = new Node({}); var db = { + getPublishEvents: sinon.stub().returns(['db1', 'db2']), modules: [ { getPublishEvents: sinon.stub().returns(['mda1', 'mda2']) @@ -83,7 +84,7 @@ describe('Bitcoind Node', function() { node.db = db; var events = node.getAllPublishEvents(); - events.should.deep.equal(['mda1', 'mda2', 'mdb1', 'mdb2']); + events.should.deep.equal(['db1', 'db2', 'mda1', 'mda2', 'mdb1', 'mdb2']); }); }); describe('#_loadConfiguration', function() { @@ -462,7 +463,6 @@ describe('Bitcoind Node', function() { setImmediate(function() { chainlib.log.info.callCount.should.equal(1); chainlib.log.info.restore(); - node.db.addModule.callCount.should.equal(1); node.chain.initialize.callCount.should.equal(1); done(); }); From bf4cec6bf5c20f3eb8476260afc0e2febc00cb84 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 14 Aug 2015 13:41:56 -0400 Subject: [PATCH 2/8] use latest bitcore --- bin/start.js | 5 ++--- lib/block.js | 2 +- lib/transaction.js | 35 ----------------------------------- lib/utils.js | 45 --------------------------------------------- package.json | 2 +- 5 files changed, 4 insertions(+), 85 deletions(-) delete mode 100644 lib/utils.js diff --git a/bin/start.js b/bin/start.js index ea53eaca..7b65c58c 100644 --- a/bin/start.js +++ b/bin/start.js @@ -5,7 +5,6 @@ var chainlib = require('chainlib'); var socketio = require('socket.io'); var log = chainlib.log; log.debug = function() {}; -var utils = require('../lib/utils'); var configuration = { datadir: process.env.BITCORENODE_DIR || '~/.bitcoin', @@ -78,7 +77,7 @@ node.on('ready', function() { } if(result) { - response.result = utils.expandObject(result); + response.result = result; } socketCallback(response); @@ -111,7 +110,7 @@ node.on('ready', function() { var results = []; for(var i = 0; i < arguments.length; i++) { - results.push(utils.expandObject(arguments[i])); + results.push(arguments[i]); } var params = [event.name].concat(results); diff --git a/lib/block.js b/lib/block.js index 6e5e9c13..e03d13fc 100644 --- a/lib/block.js +++ b/lib/block.js @@ -48,7 +48,7 @@ Block.fromBufferReader = function(br) { return new Block(obj); }; -Block.prototype.toObject = function() { +Block.prototype.toObject = Block.prototype.toJSON = function() { return { version: this.version, prevHash: this.prevHash, diff --git a/lib/transaction.js b/lib/transaction.js index a971837f..39b9537b 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -65,39 +65,4 @@ Transaction.manyToBuffer = function(transactions) { return BaseTransaction.manyToBuffer(transactions); }; -/** - * Override Bitcore's toObject() to include populated inputs and txid - */ -Transaction.prototype.toObject = function toObject() { - var inputs = []; - this.inputs.forEach(function(input) { - var inputObj = input.toObject(); - if(input.output) { - inputObj.output = input.output.toObject(); - } - inputs.push(inputObj); - }); - var outputs = []; - this.outputs.forEach(function(output) { - outputs.push(output.toObject()); - }); - var obj = { - txid: this.id, - version: this.version, - inputs: inputs, - outputs: outputs, - nLockTime: this.nLockTime - }; - if (this._changeScript) { - obj.changeScript = this._changeScript.toString(); - } - if (!_.isUndefined(this._changeIndex)) { - obj.changeIndex = this._changeIndex; - } - if (!_.isUndefined(this._fee)) { - obj.fee = this._fee; - } - return obj; -}; - module.exports = Transaction; diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index ffdf34b9..00000000 --- a/lib/utils.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -var chainlib = require('chainlib'); -var utils = chainlib.utils; - -/** - * Bitcore's API does not implement toJSON in the standard way. - * This causes issues when doing a JSON.stringify on an object - * which contains Bitcore objects. This custom implmentation - * of stringify accounts for Bitcore objects. - * @param {Object} obj - * @return {String} json - */ -utils.stringify = function(obj) { - return JSON.stringify(utils.expandObject(obj)); -} - -utils.expandObject = function(obj) { - if(Array.isArray(obj)) { - var expandedArray = []; - for(var i = 0; i < obj.length; i++) { - expandedArray.push(utils.expandObject(obj[i])); - } - - return expandedArray; - } else if(typeof obj === 'function' || typeof obj === 'object') { - if(obj.toObject) { - return obj.toObject(); - } else if(obj.toJSON) { - return obj.toJSON(); - } else { - var expandedObj = {}; - - for(var key in obj) { - expandedObj[key] = utils.expandObject(obj[key]); - } - - return expandedObj; - } - } else { - return obj; - } -}; - -module.exports = utils; \ No newline at end of file diff --git a/package.json b/package.json index fa7abc50..953cb7c2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "dependencies": { "async": "1.3.0", "bindings": "^1.2.1", - "bitcore": "^0.12.15", + "bitcore": "git+http://github.com/bitpay/bitcore.git#master", "chainlib": "^0.1.1", "errno": "^0.1.2", "memdown": "^1.0.0", From 738574d6b1d4c41ec4969e29b44df48fa25fd55f Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 14 Aug 2015 16:28:51 -0400 Subject: [PATCH 3/8] add fees to address history --- lib/modules/address.js | 1 + test/modules/address.unit.js | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/modules/address.js b/lib/modules/address.js index abf3689c..5f4fecf1 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -502,6 +502,7 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem height: transaction.__height, confirmations: self.db.chain.tip.__height - transaction.__height + 1, timestamp: transaction.__timestamp, + fees: transaction.getFee(), outputIndexes: [], inputIndexes: [], tx: transaction diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index bb528588..bda520da 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -633,14 +633,16 @@ describe('AddressModule', function() { inputIndex: 0, height: 1, timestamp: 1438289011844, - satoshis: 5000 + satoshis: 5000, + getFee: sinon.stub().returns(1000) }, { txid: 'tx3', outputIndex: 1, height: 3, timestamp: 1438289031844, - satoshis: 2000 + satoshis: 2000, + getFee: sinon.stub().returns(1000) }, { txid: 'tx4', @@ -649,7 +651,8 @@ describe('AddressModule', function() { inputIndex: 1, height: 4, timestamp: 1438289041844, - satoshis: 3000 + satoshis: 3000, + getFee: sinon.stub().returns(1000) }, ]; @@ -664,7 +667,8 @@ describe('AddressModule', function() { satoshis: 5000 } } - ] + ], + getFee: sinon.stub().returns(1000) }, { txid: 'tx5', @@ -677,7 +681,8 @@ describe('AddressModule', function() { satoshis: 3000 } } - ] + ], + getFee: sinon.stub().returns(1000) } ]; @@ -694,6 +699,7 @@ describe('AddressModule', function() { transaction.hash = txid; transaction.__height = incoming[i].height; transaction.__timestamp = incoming[i].timestamp; + transaction.getFee = incoming[i].getFee; return callback(null, transaction); } } @@ -707,6 +713,7 @@ describe('AddressModule', function() { transaction.__height = outgoing[i].height; transaction.__timestamp = outgoing[i].timestamp; transaction.inputs = outgoing[i].inputs; + transaction.getFee = outgoing[i].getFee; return callback(null, transaction); } } @@ -747,22 +754,27 @@ describe('AddressModule', function() { history[0].satoshis.should.equal(5000); history[0].height.should.equal(1); history[0].timestamp.should.equal(1438289011844); + history[0].fees.should.equal(1000); history[1].tx.hash.should.equal('tx2'); history[1].satoshis.should.equal(-5000); history[1].height.should.equal(2); history[1].timestamp.should.equal(1438289021844); + history[1].fees.should.equal(1000); history[2].tx.hash.should.equal('tx3'); history[2].satoshis.should.equal(2000); history[2].height.should.equal(3); history[2].timestamp.should.equal(1438289031844); + history[2].fees.should.equal(1000); history[3].tx.hash.should.equal('tx4'); history[3].satoshis.should.equal(3000); history[3].height.should.equal(4); history[3].timestamp.should.equal(1438289041844); + history[3].fees.should.equal(1000); history[4].tx.hash.should.equal('tx5'); history[4].satoshis.should.equal(-3000); history[4].height.should.equal(5); history[4].timestamp.should.equal(1438289051844); + history[4].fees.should.equal(1000); done(); }); }); From 43ec2d3b3402aa35db9d4d841be22b712ba083d6 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Fri, 14 Aug 2015 16:44:40 -0400 Subject: [PATCH 4/8] add more tests --- lib/modules/address.js | 2 +- test/db.unit.js | 136 +++++++++++++++++++++++++++++++++++ test/modules/address.unit.js | 94 +++++++++++++++++++++++- 3 files changed, 228 insertions(+), 4 deletions(-) diff --git a/lib/modules/address.js b/lib/modules/address.js index 5f4fecf1..7bddbe7c 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -55,7 +55,7 @@ AddressModule.prototype.getPublishEvents = function() { scope: this, subscribe: this.subscribe.bind(this, 'address/balance'), unsubscribe: this.unsubscribe.bind(this, 'address/balance') - }, + } ]; }; diff --git a/test/db.unit.js b/test/db.unit.js index b52a8827..82e00076 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -10,10 +10,25 @@ var errors = bitcoindjs.errors; var memdown = require('memdown'); var inherits = require('util').inherits; var BaseModule = require('../lib/module'); +var bitcore = require('bitcore'); +var Transaction = bitcore.Transaction; describe('Bitcoin DB', function() { var coinbaseAmount = 50 * 1e8; + describe('#initialize', function() { + it('should emit ready', function(done) { + var db = new DB({store: memdown}); + db._modules = ['mod1', 'mod2']; + db.bitcoind = { + on: sinon.spy() + }; + db.addModule = sinon.spy(); + db.on('ready', done); + db.initialize(); + }); + }); + describe('#getTransaction', function() { it('will return a NotFound error', function(done) { var db = new DB({store: memdown}); @@ -89,6 +104,127 @@ describe('Bitcoin DB', function() { }); }); + describe('#getPrevHash', function() { + it('should return prevHash from bitcoind', function(done) { + var db = new DB({store: memdown}); + db.bitcoind = { + getBlockIndex: sinon.stub().returns({ + prevHash: 'prevhash' + }) + }; + + db.getPrevHash('hash', function(err, prevHash) { + should.not.exist(err); + prevHash.should.equal('prevhash'); + done(); + }); + }); + + it('should give an error if bitcoind could not find it', function(done) { + var db = new DB({store: memdown}); + db.bitcoind = { + getBlockIndex: sinon.stub().returns(null) + }; + + db.getPrevHash('hash', function(err, prevHash) { + should.exist(err); + done(); + }); + }); + }); + + describe('#getTransactionWithBlockInfo', function() { + it('should give a transaction with height and timestamp', function(done) { + var txBuffer = new Buffer('01000000016f95980911e01c2c664b3e78299527a47933aac61a515930a8fe0213d1ac9abe01000000da0047304402200e71cda1f71e087c018759ba3427eb968a9ea0b1decd24147f91544629b17b4f0220555ee111ed0fc0f751ffebf097bdf40da0154466eb044e72b6b3dcd5f06807fa01483045022100c86d6c8b417bff6cc3bbf4854c16bba0aaca957e8f73e19f37216e2b06bb7bf802205a37be2f57a83a1b5a8cc511dc61466c11e9ba053c363302e7b99674be6a49fc0147522102632178d046673c9729d828cfee388e121f497707f810c131e0d3fc0fe0bd66d62103a0951ec7d3a9da9de171617026442fcd30f34d66100fab539853b43f508787d452aeffffffff0240420f000000000017a9148a31d53a448c18996e81ce67811e5fb7da21e4468738c9d6f90000000017a9148ce5408cfeaddb7ccb2545ded41ef478109454848700000000', 'hex'); + var info = { + height: 530482, + timestamp: 1439559434000, + buffer: txBuffer + }; + + var db = new DB({store: memdown}); + db.bitcoind = { + getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, info) + }; + + db.getTransactionWithBlockInfo('2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f', true, function(err, tx) { + should.not.exist(err); + tx.__height.should.equal(info.height); + tx.__timestamp.should.equal(info.timestamp); + done(); + }); + }); + it('should give an error if one occurred', function(done) { + var db = new DB({store: memdown}); + db.bitcoind = { + getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('error')) + }; + + db.getTransactionWithBlockInfo('tx', true, function(err, tx) { + should.exist(err); + done(); + }); + }); + }); + + describe('#sendTransaction', function() { + it('should give the txid on success', function(done) { + var db = new DB({store: memdown}); + db.bitcoind = { + sendTransaction: sinon.stub().returns('txid') + }; + + var tx = new Transaction(); + db.sendTransaction(tx, function(err, txid) { + should.not.exist(err); + txid.should.equal('txid'); + done(); + }); + }); + it('should give an error if bitcoind threw an error', function(done) { + var db = new DB({store: memdown}); + db.bitcoind = { + sendTransaction: sinon.stub().throws(new Error('error')) + }; + + var tx = new Transaction(); + db.sendTransaction(tx, function(err, txid) { + should.exist(err); + done(); + }); + }); + }); + + describe("#estimateFee", function() { + // To accommodate weird bitcoind behavior where 1 block always results in -1 + it('should set blocks to 2 if 1 was passed in', function(done) { + var db = new DB({store: memdown}); + db.bitcoind = { + estimateFee: sinon.stub().returns(1000) + }; + + db.estimateFee(1, function(err, fee) { + should.not.exist(err); + fee.should.equal(1000); + db.bitcoind.estimateFee.args[0][0].should.equal(2); + done(); + }); + }); + + it('should pass along the fee from bitcoind', function(done) { + var db = new DB({store: memdown}); + db.bitcoind = { + estimateFee: sinon.stub().returns(1000) + }; + + db.estimateFee(5, function(err, fee) { + should.not.exist(err); + fee.should.equal(1000); + db.bitcoind.estimateFee.args[0][0].should.equal(5); + done(); + }); + }); + }); describe('#buildGenesisData', function() { it('build genisis data', function() { var db = new DB({path: 'path', store: memdown}); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index bda520da..4b8653aa 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -2,12 +2,12 @@ var should = require('chai').should(); var sinon = require('sinon'); -var bitcoindjs = require('../../'); -var AddressModule = bitcoindjs.modules.AddressModule; +var bitcorenode = require('../../'); +var AddressModule = bitcorenode.modules.AddressModule; var blockData = require('../data/livenet-345003.json'); var bitcore = require('bitcore'); var EventEmitter = require('events').EventEmitter; -var errors = bitcoindjs.errors; +var errors = bitcorenode.errors; var chainlib = require('chainlib'); var levelup = chainlib.deps.levelup; @@ -494,6 +494,94 @@ describe('AddressModule', function() { }); }); + 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 = { + bitcoind: { + on: sinon.spy() + } + }; + var am = new AddressModule({db: db}); + 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 = { + bitcoind: { + on: sinon.spy() + } + }; + var am = new AddressModule({db: db}); + 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 = { + bitcoind: { + on: sinon.spy() + } + }; + var am = new AddressModule({db: db}); + 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 = [ From d6d9c6a9753b196ca4be522994d851a8964e5a28 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Tue, 18 Aug 2015 11:58:12 -0400 Subject: [PATCH 5/8] take out modifying behavior to estimateFee --- lib/db.js | 6 ------ test/db.unit.js | 15 --------------- 2 files changed, 21 deletions(-) diff --git a/lib/db.js b/lib/db.js index efae1f4f..c41acf9e 100644 --- a/lib/db.js +++ b/lib/db.js @@ -123,12 +123,6 @@ DB.prototype.sendTransaction = function(tx, callback) { DB.prototype.estimateFee = function(blocks, callback) { var self = this; - // For some reason getting fee for 1 block returns -1 - // Until this is resolved, just make it 2 blocks - if(blocks === 1) { - blocks = 2; - } - setImmediate(function() { callback(null, self.bitcoind.estimateFee(blocks)); }); diff --git a/test/db.unit.js b/test/db.unit.js index 82e00076..e4081a8e 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -196,21 +196,6 @@ describe('Bitcoin DB', function() { }); describe("#estimateFee", function() { - // To accommodate weird bitcoind behavior where 1 block always results in -1 - it('should set blocks to 2 if 1 was passed in', function(done) { - var db = new DB({store: memdown}); - db.bitcoind = { - estimateFee: sinon.stub().returns(1000) - }; - - db.estimateFee(1, function(err, fee) { - should.not.exist(err); - fee.should.equal(1000); - db.bitcoind.estimateFee.args[0][0].should.equal(2); - done(); - }); - }); - it('should pass along the fee from bitcoind', function(done) { var db = new DB({store: memdown}); db.bitcoind = { From eacb04f4f9dd1400bfcab91647fb79611d31946a Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Tue, 18 Aug 2015 16:16:07 -0400 Subject: [PATCH 6/8] fix block subscription --- lib/block.js | 1 + lib/db.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/block.js b/lib/block.js index e03d13fc..4d36af54 100644 --- a/lib/block.js +++ b/lib/block.js @@ -50,6 +50,7 @@ Block.fromBufferReader = function(br) { Block.prototype.toObject = Block.prototype.toJSON = function() { return { + hash: this.hash, version: this.version, prevHash: this.prevHash, merkleRoot: this.merkleRoot, diff --git a/lib/db.js b/lib/db.js index c41acf9e..cdb01423 100644 --- a/lib/db.js +++ b/lib/db.js @@ -234,7 +234,7 @@ DB.prototype.blockHandler = function(block, add, callback) { // Notify block subscribers for(var i = 0; i < this.subscriptions.block.length; i++) { - this.subscriptions.transaction[i].emit('block', block.hash); + this.subscriptions.block[i].emit('block', block.hash); } async.eachSeries( From e15473f625b97a9a3f76be48bdf7b04a24b8075e Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Wed, 19 Aug 2015 12:20:20 -0400 Subject: [PATCH 7/8] use latest bitcore --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 953cb7c2..1fb066b8 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "dependencies": { "async": "1.3.0", "bindings": "^1.2.1", - "bitcore": "git+http://github.com/bitpay/bitcore.git#master", - "chainlib": "^0.1.1", + "bitcore": "^0.13.0", + "chainlib": "^0.1.3", "errno": "^0.1.2", "memdown": "^1.0.0", "mkdirp": "0.5.0", From 2dc79efc0d16c57fcfc658c144fe423bb073200e Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Wed, 19 Aug 2015 14:57:46 -0400 Subject: [PATCH 8/8] make 0 confirmations work --- lib/modules/address.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/modules/address.js b/lib/modules/address.js index 7bddbe7c..c61e6786 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -496,11 +496,16 @@ AddressModule.prototype.getAddressHistoryForAddress = function(address, queryMem return callback(err); } + var confirmations = 0; + if(transaction.__height >= 0) { + confirmations = self.db.chain.tip.__height - transaction.__height; + } + txinfos[transaction.hash] = { address: address, satoshis: 0, height: transaction.__height, - confirmations: self.db.chain.tip.__height - transaction.__height + 1, + confirmations: confirmations, timestamp: transaction.__timestamp, fees: transaction.getFee(), outputIndexes: [],