diff --git a/docs/services/db.md b/docs/services/db.md index 9a8d1eba..aafb7e63 100644 --- a/docs/services/db.md +++ b/docs/services/db.md @@ -32,3 +32,14 @@ node.getBlock(blockHash, function(err, block) { //... }); ``` + +Get Block Hashes by Timestamp Range + +```js +var time1 = 1441911000; // Notice time is in seconds not milliseconds +var time2 = 1441914000; + +node.getBlockHashesByTimestamp(time1, time2, function(err, hashes) { + //... +}); +``` diff --git a/lib/services/db.js b/lib/services/db.js index 62ec179c..181bbe1c 100644 --- a/lib/services/db.js +++ b/lib/services/db.js @@ -60,6 +60,10 @@ util.inherits(DB, Service); DB.dependencies = ['bitcoind']; +DB.PREFIXES = { + BLOCKS: 'blk' +}; + DB.prototype._setDataPath = function() { $.checkState(this.node.datadir, 'Node is expected to have a "datadir" property'); var regtest = Networks.get('regtest'); @@ -177,6 +181,7 @@ DB.prototype.transactionHandler = function(txInfo) { DB.prototype.getAPIMethods = function() { var methods = [ ['getBlock', this, this.getBlock, 1], + ['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2], ['getTransaction', this, this.getTransaction, 2], ['getTransactionWithBlockInfo', this, this.getTransactionWithBlockInfo, 2], ['sendTransaction', this, this.sendTransaction, 1], @@ -194,6 +199,37 @@ DB.prototype.getBlock = function(hash, callback) { }); }; +DB.prototype.getBlockHashesByTimestamp = function(start, end, callback) { + var hashes = []; + + var stream = this.store.createReadStream({ + start: [DB.PREFIXES.BLOCKS, start].join('-'), + end: [DB.PREFIXES.BLOCKS, end].join('-') + }); + + stream.on('data', function(data) { + hashes.push(data.value); + }); + + var error; + + stream.on('error', function(streamError) { + if (streamError) { + error = streamError; + } + }); + + stream.on('close', function() { + if (error) { + return callback(error); + } + + callback(null, hashes); + }); + + return stream; +}; + DB.prototype.getTransaction = function(txid, queryMempool, callback) { this.node.services.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) { if (err) { @@ -371,6 +407,13 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) { this.subscriptions.block[i].emit('block', block.hash); } + // Update block index + operations.push({ + type: add ? 'put' : 'del', + key: [DB.PREFIXES.BLOCKS, block.header.timestamp].join('-'), + value: block.hash + }); + async.eachSeries( this.node.services, function(mod, next) { diff --git a/test/services/db.unit.js b/test/services/db.unit.js index 5102b9d2..9bfc25b1 100644 --- a/test/services/db.unit.js +++ b/test/services/db.unit.js @@ -369,6 +369,62 @@ describe('DB Service', function() { }); }); + describe('#getBlockHashesByTimestamp', function() { + it('should get the correct block hashes', function(done) { + var db = new DB(baseConfig); + var readStream = new EventEmitter(); + db.store = { + createReadStream: sinon.stub().returns(readStream) + }; + + var block1 = { + hash: '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b', + timestamp: 1441911909 + }; + + var block2 = { + hash: '000000000383752a55a0b2891ce018fd0fdc0b6352502772b034ec282b4a1bf6', + timestamp: 1441913112 + }; + + db.getBlockHashesByTimestamp(1441911000, 1441914000, function(err, hashes) { + should.not.exist(err); + hashes.should.deep.equal([block1.hash, block2.hash]); + done(); + }); + + readStream.emit('data', { + key: 'blk-' + block1.timestamp, + value: block1.hash + }); + + readStream.emit('data', { + key: 'blk-' + block2.timestamp, + value: block2.hash + }); + + readStream.emit('close'); + }); + + it('should give an error if the stream has an error', function(done) { + var db = new DB(baseConfig); + var readStream = new EventEmitter(); + db.store = { + createReadStream: sinon.stub().returns(readStream) + }; + + db.getBlockHashesByTimestamp(1441911000, 1441914000, function(err, hashes) { + should.exist(err); + err.message.should.equal('error'); + done(); + }); + + readStream.emit('error', new Error('error')); + + readStream.emit('close'); + }); + }); + describe('#getPrevHash', function() { it('should return prevHash from bitcoind', function(done) { var db = new DB(baseConfig); @@ -650,10 +706,22 @@ describe('DB Service', function() { batch: sinon.stub().callsArg(1) }; + var block = { + hash: '00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863', + header: { + timestamp: 1441906365 + } + }; + it('should call blockHandler in all services and perform operations', function(done) { - db.runAllBlockHandlers('block', true, function(err) { + db.runAllBlockHandlers(block, true, function(err) { should.not.exist(err); - db.store.batch.args[0][0].should.deep.equal(['op1', 'op2', 'op3', 'op4', 'op5']); + var blockOp = { + type: 'put', + key: 'blk-1441906365', + value: '00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863' + } + db.store.batch.args[0][0].should.deep.equal([blockOp, 'op1', 'op2', 'op3', 'op4', 'op5']); done(); }); }); @@ -663,7 +731,7 @@ describe('DB Service', function() { Service3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error')); db.node.services.service3 = new Service3(); - db.runAllBlockHandlers('block', true, function(err) { + db.runAllBlockHandlers(block, true, function(err) { should.exist(err); done(); }); @@ -675,7 +743,7 @@ describe('DB Service', function() { service3: new Service3() }; - db.runAllBlockHandlers('block', true, function(err) { + db.runAllBlockHandlers(block, true, function(err) { should.not.exist(err); done(); }); @@ -688,7 +756,7 @@ describe('DB Service', function() { }; (function() { - db.runAllBlockHandlers('block', true, function(err) { + db.runAllBlockHandlers(block, true, function(err) { should.not.exist(err); }); }).should.throw('bitcore.ErrorInvalidArgument'); @@ -701,7 +769,7 @@ describe('DB Service', function() { db.node = {}; db.node.services = {}; var methods = db.getAPIMethods(); - methods.length.should.equal(5); + methods.length.should.equal(6); }); });