diff --git a/bin/start.js b/bin/start.js index 4da2476c..7b65c58c 100644 --- a/bin/start.js +++ b/bin/start.js @@ -77,11 +77,7 @@ node.on('ready', function() { } if(result) { - if(result.toJSON) { - response.result = result.toJSON(); - } else { - response.result = result; - } + response.result = result; } socketCallback(response); @@ -114,11 +110,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(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..4d36af54 100644 --- a/lib/block.js +++ b/lib/block.js @@ -48,8 +48,9 @@ Block.fromBufferReader = function(br) { return new Block(obj); }; -Block.prototype.toObject = function() { +Block.prototype.toObject = Block.prototype.toJSON = function() { return { + hash: this.hash, version: this.version, prevHash: this.prevHash, merkleRoot: this.merkleRoot, @@ -60,10 +61,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..cdb01423 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,28 @@ 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; + + setImmediate(function() { + callback(null, self.bitcoind.estimateFee(blocks)); + }); +} + DB.prototype.validateBlockData = function(block, callback) { // bitcoind does the validation setImmediate(callback); @@ -195,6 +232,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.block[i].emit('block', block.hash); + } + async.eachSeries( this.modules, function(module, next) { @@ -222,7 +264,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 +276,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 +301,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..c61e6786 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,16 +45,16 @@ 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 = {}; @@ -447,13 +496,21 @@ AddressModule.prototype.getAddressHistory = function(address, queryMempool, call 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: confirmations, timestamp: transaction.__timestamp, + fees: transaction.getFee(), outputIndexes: [], inputIndexes: [], - transaction: transaction + tx: transaction }; callback(null, txinfos[transaction.hash]); @@ -490,7 +547,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..39b9537b 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++) { diff --git a/package.json b/package.json index fa7abc50..1fb066b8 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "dependencies": { "async": "1.3.0", "bindings": "^1.2.1", - "bitcore": "^0.12.15", - "chainlib": "^0.1.1", + "bitcore": "^0.13.0", + "chainlib": "^0.1.3", "errno": "^0.1.2", "memdown": "^1.0.0", "mkdirp": "0.5.0", 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..e4081a8e 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,112 @@ 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() { + 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}); @@ -301,7 +422,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 +446,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..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; @@ -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 + } } }; @@ -490,6 +495,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 = [ { @@ -514,7 +607,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 +618,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 +627,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); @@ -628,14 +721,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', @@ -644,7 +739,8 @@ describe('AddressModule', function() { inputIndex: 1, height: 4, timestamp: 1438289041844, - satoshis: 3000 + satoshis: 3000, + getFee: sinon.stub().returns(1000) }, ]; @@ -659,7 +755,8 @@ describe('AddressModule', function() { satoshis: 5000 } } - ] + ], + getFee: sinon.stub().returns(1000) }, { txid: 'tx5', @@ -672,7 +769,8 @@ describe('AddressModule', function() { satoshis: 3000 } } - ] + ], + getFee: sinon.stub().returns(1000) } ]; @@ -689,6 +787,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); } } @@ -702,6 +801,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); } } @@ -709,6 +809,11 @@ describe('AddressModule', function() { }, bitcoind: { on: sinon.stub() + }, + chain: { + tip: { + __height: 1 + } } }; var am = new AddressModule({db: db}); @@ -733,26 +838,31 @@ 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[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[2].transaction.hash.should.equal('tx3'); + 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[3].transaction.hash.should.equal('tx4'); + 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[4].transaction.hash.should.equal('tx5'); + 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(); }); }); 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(); });