From 8d39d02ee66526093220022c1c486dc393bcbf68 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Tue, 21 Jul 2015 14:59:08 -0600 Subject: [PATCH 1/7] modularize db --- lib/db.js | 278 +++++++++-------------------------------- lib/module.js | 28 +++++ lib/modules/address.js | 204 ++++++++++++++++++++++++++++++ 3 files changed, 288 insertions(+), 222 deletions(-) create mode 100644 lib/module.js create mode 100644 lib/modules/address.js diff --git a/lib/db.js b/lib/db.js index f950ca78..4f6ee98a 100644 --- a/lib/db.js +++ b/lib/db.js @@ -6,12 +6,14 @@ var BaseDB = chainlib.DB; var Transaction = require('./transaction'); var async = require('async'); var bitcore = require('bitcore'); +var $ = bitcore.util.preconditions; var BufferWriter = bitcore.encoding.BufferWriter; var errors = require('./errors'); var levelup = chainlib.deps.levelup; var log = chainlib.log; -var PublicKey = bitcore.PublicKey; var Address = bitcore.Address; +var BaseModule = require('./module'); +var AddressModule = require('./modules/address'); function DB(options) { if(!options) { @@ -25,15 +27,21 @@ function DB(options) { this.Transaction = Transaction; this.network = bitcore.Networks.get(options.network) || bitcore.Networks.testnet; + this.modules = []; + + // Add address module + this.addModule(AddressModule); + + // Add other modules + if(options.modules && options.modules.length) { + for(var i = 0; i < options.modules.length; i++) { + this.addModule(options.modules[i]); + } + } } util.inherits(DB, BaseDB); -DB.PREFIXES = { - OUTPUTS: 'outs' -}; -DB.CONCURRENCY = 10; - DB.prototype.getBlock = function(hash, callback) { var self = this; @@ -159,241 +167,67 @@ DB.prototype.getInputTotal = function(transactions) { return grandTotal; }; -DB.prototype._updateOutputs = function(block, addOutput, callback) { - var txs = this.getTransactionsFromBlock(block); - - log.debug('Processing transactions', txs); - log.debug('Updating outputs'); - - var action = 'put'; - if (!addOutput) { - action = 'del'; - } - - var operations = []; - - for (var i = 0; i < txs.length; i++) { - - var tx = txs[i]; - var txid = tx.id; - var inputs = tx.inputs; - var outputs = tx.outputs; - - for (var j = 0; j < outputs.length; j++) { - var output = outputs[j]; - - var script = output.script; - if(!script) { - log.debug('Invalid script'); - continue; - } - - if (!script.isPublicKeyHashOut() && !script.isScriptHashOut() && !script.isPublicKeyOut()) { - // ignore for now - log.debug('script was not pubkeyhashout, scripthashout, or pubkeyout'); - continue; - } - - var address; - - if(script.isPublicKeyOut()) { - var pubkey = script.chunks[0].buf; - address = Address.fromPublicKey(new PublicKey(pubkey), this.network); - } else { - address = output.script.toAddress(this.network); - } - - var outputIndex = j; - - var timestamp = block.timestamp.getTime(); - var height = block.height; - - operations.push({ - type: action, - key: [DB.PREFIXES.OUTPUTS, address, timestamp, txid, outputIndex].join('-'), - value: [output.satoshis, script, height].join(':') - }); - } - - if(tx.isCoinbase()) { - continue; - } - - } - - setImmediate(function() { - callback(null, operations); - }); -}; - DB.prototype._onChainAddBlock = function(block, callback) { - - var self = this; - log.debug('DB handling new chain block'); // Remove block from mempool - self.mempool.removeBlock(block.hash); - - async.series([ - this._updateOutputs.bind(this, block, true), // add outputs - ], function(err, results) { - - if (err) { - return callback(err); - } - - var operations = []; - for (var i = 0; i < results.length; i++) { - operations = operations.concat(results[i]); - } - - log.debug('Updating the database with operations', operations); - - self.store.batch(operations, callback); - - }); - + this.mempool.removeBlock(block.hash); + this.blockHandler(block, true, callback); }; - DB.prototype._onChainRemoveBlock = function(block, callback) { + log.debug('DB removing chain block'); + this.blockHandler(block, false, callback); +}; +DB.prototype.blockHandler = function(block, add, callback) { var self = this; + var operations = []; - async.series([ - this._updateOutputs.bind(this, block, false), // remove outputs - ], function(err, results) { + async.eachSeries( + this.modules, + function(module, next) { + module['blockHandler'].call(module, block, add, function(err, ops) { + if(err) { + return next(err); + } - if (err) { - return callback(err); + operations = operations.concat(ops); + next(); + }); + }, + function(err) { + if (err) { + return callback(err); + } + + log.debug('Updating the database with operations', operations); + + self.store.batch(operations, callback); } - - var operations = []; - for (var i = 0; i < results.length; i++) { - operations = operations.concat(results[i]); - } - self.store.batch(operations, callback); - - }); - + ); }; DB.prototype.getAPIMethods = function() { - return [ - ['getTransaction', this, this.getTransaction, 2], - ['getBalance', this, this.getBalance, 2], - ['getOutputs', this, this.getOutputs, 2], - ['getUnspentOutputs', this, this.getUnspentOutputs, 2], - ['isSpent', this, this.isSpent, 2] + var methods = [ + ['getTransaction', this, this.getTransaction, 2] ]; + + for(var i = 0; i < this.modules.length; i++) { + methods = methods.concat(this.modules[i]['methods'].call(this.modules[i])); + } + + return methods; }; -DB.prototype.getBalance = function(address, queryMempool, callback) { - this.getUnspentOutputs(address, queryMempool, function(err, outputs) { - if(err) { - return callback(err); - } - - var satoshis = outputs.map(function(output) { - return output.satoshis; - }); - - var sum = satoshis.reduce(function(a, b) { - return a + b; - }, 0); - - return callback(null, sum); - }); -}; - -DB.prototype.getOutputs = function(address, queryMempool, callback) { - var self = this; - - var outputs = []; - var key = [DB.PREFIXES.OUTPUTS, address].join('-'); - - var stream = this.store.createReadStream({ - start: key, - end: key + '~' - }); - - stream.on('data', function(data) { - - var key = data.key.split('-'); - var value = data.value.split(':'); - - var output = { - address: key[1], - txid: key[3], - outputIndex: Number(key[4]), - satoshis: Number(value[0]), - script: value[1], - blockHeight: Number(value[2]) - }; - - outputs.push(output); - - }); - - var error; - - stream.on('error', function(streamError) { - if (streamError) { - error = streamError; - } - }); - - stream.on('close', function() { - if (error) { - return callback(error); - } - - if(queryMempool) { - outputs = outputs.concat(self.bitcoind.getMempoolOutputs(address)); - } - - callback(null, outputs); - }); - - return stream; - -}; - -DB.prototype.getUnspentOutputs = function(address, queryMempool, callback) { - - var self = this; - - this.getOutputs(address, queryMempool, function(err, outputs) { - if (err) { - return callback(err); - } else if(!outputs.length) { - return callback(new errors.NoOutputs('Address ' + address + ' has no outputs'), []); - } - - var isUnspent = function(output, callback) { - self.isUnspent(output, queryMempool, callback); - }; - - async.filter(outputs, isUnspent, function(results) { - callback(null, results); - }); - }); -}; - -DB.prototype.isUnspent = function(output, queryMempool, callback) { - this.isSpent(output, queryMempool, function(spent) { - callback(!spent); - }); -}; - -DB.prototype.isSpent = function(output, queryMempool, callback) { - var self = this; - var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; - - setImmediate(function() { - callback(self.bitcoind.isSpent(txid, output.outputIndex)); +DB.prototype.addModule = function(Module) { + var module = new Module({ + db: this, + bitcoind: this.bitcoind, + network: this.network }); + $.checkArgumentType(module, BaseModule); + this.modules.push(module); }; module.exports = DB; diff --git a/lib/module.js b/lib/module.js new file mode 100644 index 00000000..896f8a95 --- /dev/null +++ b/lib/module.js @@ -0,0 +1,28 @@ +'use strict'; + +var Module = function(options) { + this.db = options.db; + this.bitcoind = options.bitcoind; + this.network = options.network; +}; + +Module.prototype.blockHandler = function(block, add, callback) { + // implement in the child class + setImmediate(callback); +}; + +Module.prototype.methods = function() { + // Example: + // return [ + // ['getData', this, this.getData, 1] + // ]; + + return []; +}; + +// Example: +// Module.prototype.getData = function(arg1, callback) { +// +// }; + +module.exports = Module; \ No newline at end of file diff --git a/lib/modules/address.js b/lib/modules/address.js new file mode 100644 index 00000000..8d4f72e7 --- /dev/null +++ b/lib/modules/address.js @@ -0,0 +1,204 @@ +'use strict'; + +var BaseModule = require('../module'); +var inherits = require('util').inherits; +var async = require('async'); +var chainlib = require('chainlib'); +var log = chainlib.log; +var bitcore = require('bitcore'); +var PublicKey = bitcore.PublicKey; +var Address = bitcore.Address; + +var AddressModule = function(options) { + BaseModule.call(this, options); +}; + +inherits(AddressModule, BaseModule); + +AddressModule.PREFIXES = { + OUTPUTS: 'outs' +}; + +AddressModule.prototype.methods = function() { + return [ + ['getBalance', this, this.getBalance, 2], + ['getOutputs', this, this.getOutputs, 2], + ['getUnspentOutputs', this, this.getUnspentOutputs, 2], + ['isSpent', this, this.isSpent, 2] + ]; +}; + +AddressModule.prototype.blockHandler = function(block, addOutput, callback) { + var txs = this.db.getTransactionsFromBlock(block); + + log.debug('Updating output index'); + + var action = 'put'; + if (!addOutput) { + action = 'del'; + } + + var operations = []; + + for (var i = 0; i < txs.length; i++) { + + var tx = txs[i]; + var txid = tx.id; + var inputs = tx.inputs; + var outputs = tx.outputs; + + for (var j = 0; j < outputs.length; j++) { + var output = outputs[j]; + + var script = output.script; + if(!script) { + log.debug('Invalid script'); + continue; + } + + if (!script.isPublicKeyHashOut() && !script.isScriptHashOut() && !script.isPublicKeyOut()) { + // ignore for now + log.debug('script was not pubkeyhashout, scripthashout, or pubkeyout'); + continue; + } + + var address; + + if(script.isPublicKeyOut()) { + var pubkey = script.chunks[0].buf; + address = Address.fromPublicKey(new PublicKey(pubkey), this.network); + } else { + address = output.script.toAddress(this.network); + } + + var outputIndex = j; + + var timestamp = block.timestamp.getTime(); + var height = block.height; + + operations.push({ + type: action, + key: [AddressModule.PREFIXES.OUTPUTS, address, timestamp, txid, outputIndex].join('-'), + value: [output.satoshis, script, height].join(':') + }); + } + + if(tx.isCoinbase()) { + continue; + } + + } + + setImmediate(function() { + callback(null, operations); + }); +}; + +AddressModule.prototype.getBalance = function(address, queryMempool, callback) { + this.getUnspentOutputs(address, queryMempool, function(err, outputs) { + if(err) { + return callback(err); + } + + var satoshis = outputs.map(function(output) { + return output.satoshis; + }); + + var sum = satoshis.reduce(function(a, b) { + return a + b; + }, 0); + + return callback(null, sum); + }); +}; + +AddressModule.prototype.getOutputs = function(address, queryMempool, callback) { + var self = this; + + var outputs = []; + var key = [AddressModule.PREFIXES.OUTPUTS, address].join('-'); + + var stream = this.db.store.createReadStream({ + start: key, + end: key + '~' + }); + + stream.on('data', function(data) { + + var key = data.key.split('-'); + var value = data.value.split(':'); + + var output = { + address: key[1], + txid: key[3], + outputIndex: Number(key[4]), + satoshis: Number(value[0]), + script: value[1], + blockHeight: Number(value[2]) + }; + + outputs.push(output); + + }); + + var error; + + stream.on('error', function(streamError) { + if (streamError) { + error = streamError; + } + }); + + stream.on('close', function() { + if (error) { + return callback(error); + } + + if(queryMempool) { + outputs = outputs.concat(self.bitcoind.getMempoolOutputs(address)); + } + + callback(null, outputs); + }); + + return stream; + +}; + +AddressModule.prototype.getUnspentOutputs = function(address, queryMempool, callback) { + + var self = this; + + this.getOutputs(address, queryMempool, function(err, outputs) { + if (err) { + return callback(err); + } else if(!outputs.length) { + return callback(new errors.NoOutputs('Address ' + address + ' has no outputs'), []); + } + + var isUnspent = function(output, callback) { + self.isUnspent(output, queryMempool, callback); + }; + + async.filter(outputs, isUnspent, function(results) { + callback(null, results); + }); + }); +}; + +AddressModule.prototype.isUnspent = function(output, queryMempool, callback) { + this.isSpent(output, queryMempool, function(spent) { + callback(!spent); + }); +}; + +AddressModule.prototype.isSpent = function(output, queryMempool, callback) { + var self = this; + var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; + + setImmediate(function() { + callback(self.bitcoind.isSpent(txid, output.outputIndex)); + }); +}; + +module.exports = AddressModule; \ No newline at end of file From 14e21fadb09ebb2829483cecbd2619c11b1b4414 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Thu, 23 Jul 2015 09:33:42 -0600 Subject: [PATCH 2/7] update tests --- index.js | 3 + lib/modules/address.js | 1 + test/db.unit.js | 435 +++++++---------------------------- test/modules/address.unit.js | 329 ++++++++++++++++++++++++++ 4 files changed, 418 insertions(+), 350 deletions(-) create mode 100644 test/modules/address.unit.js diff --git a/index.js b/index.js index 6236363e..857a26d7 100644 --- a/index.js +++ b/index.js @@ -9,5 +9,8 @@ module.exports.DB = require('./lib/db'); module.exports.Transaction = require('./lib/transaction'); module.exports.errors = require('./lib/errors'); +module.exports.modules = {}; +module.exports.modules.AddressModule = require('./lib/modules/address'); + module.exports.deps = {}; module.exports.deps.chainlib = require('chainlib'); \ No newline at end of file diff --git a/lib/modules/address.js b/lib/modules/address.js index 8d4f72e7..1c77b761 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -5,6 +5,7 @@ var inherits = require('util').inherits; var async = require('async'); var chainlib = require('chainlib'); var log = chainlib.log; +var errors = chainlib.errors; var bitcore = require('bitcore'); var PublicKey = bitcore.PublicKey; var Address = bitcore.Address; diff --git a/test/db.unit.js b/test/db.unit.js index 580f5292..c74ca0b2 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -11,6 +11,8 @@ var bitcore = require('bitcore'); var EventEmitter = require('events').EventEmitter; var errors = bitcoindjs.errors; var memdown = require('memdown'); +var inherits = require('util').inherits; +var BaseModule = require('../lib/module'); describe('Bitcoin DB', function() { var coinbaseAmount = 50 * 1e8; @@ -196,395 +198,128 @@ describe('Bitcoin DB', function() { }); }); - describe('#_updateOutputs', function() { - var block = bitcore.Block.fromString(blockData); - var db = new DB({path: 'path', store: memdown, network: 'livenet'}); - db.getTransactionsFromBlock = function() { - return block.transactions.slice(0, 8); - }; - - var data = [ - { - key: { - address: '1F1MAvhTKg2VG29w8cXsiSN2PJ8gSsrJw', - timestamp: 1424836934000, - txid: 'fdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e16923', - outputIndex: 0 - }, - value: { - satoshis: 2502227470, - script: 'OP_DUP OP_HASH160 20 0x02a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b OP_EQUALVERIFY OP_CHECKSIG', - blockHeight: 345003 - } - }, - { - key: { - prevTxId: '3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a9', - prevOutputIndex: 32 - }, - value: { - txid: '5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca', - inputIndex: 0, - timestamp: 1424836934000 - } - }, - { - key: { - address: '1Ep5LA4T6Y7zaBPiwruUJurjGFvCJHzJhm', - timestamp: 1424836934000, - txid: 'e66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d', - outputIndex: 1 - }, - value: { - satoshis: 3100000, - script: 'OP_DUP OP_HASH160 20 0x9780ccd5356e2acc0ee439ee04e0fe69426c7528 OP_EQUALVERIFY OP_CHECKSIG', - blockHeight: 345003 - } - } - ]; - var key0 = data[0].key; - var value0 = data[0].value; - var key3 = data[1].key; - var value3 = data[1].value; - var key64 = data[2].key; - var value64 = data[2].value; - - it('should create the correct operations when updating/adding outputs', function(done) { - db._updateOutputs({height: 345003, timestamp: new Date(1424836934000)}, true, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(11); - operations[0].type.should.equal('put'); - var expected0 = ['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-'); - operations[0].key.should.equal(expected0); - operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':')); - done(); - }); - }); - it('should create the correct operations when removing outputs', function(done) { - db._updateOutputs({height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(11); - operations[0].type.should.equal('del'); - operations[0].key.should.equal(['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-')); - operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':')); - done(); - }); - }); - it('should continue if output script is null', function(done) { - var db = new DB({path: 'path', store: memdown, network: 'livenet'}); - var transactions = [ - { - inputs: [], - outputs: [ - { - script: null, - satoshis: 1000, - } - ], - isCoinbase: sinon.stub().returns(false) - } - ]; - db.getTransactionsFromBlock = function() { - return transactions; - }; - - db._updateOutputs({height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { - should.not.exist(err); - operations.length.should.equal(0); - done(); - }); - }); - }); describe('#_onChainAddBlock', function() { - var db = new DB({path: 'path', store: memdown}); - db._updateOutputs = sinon.stub().callsArgWith(2, null, ['1a', '1b']); - db.store = { - batch: sinon.stub().callsArg(1) - }; - - it('should give error when there is a failure to write', function() { - var errordb = new DB({path: 'path', store: memdown}); - errordb._updateOutputs = sinon.stub().callsArgWith(2, null, ['1a', '1b']); - errordb.store = { - batch: sinon.stub().callsArgWith(1, new Error('error')) + it('should remove block from mempool and call blockHandler with true', function(done) { + var db = new DB({store: memdown}); + db.mempool = { + removeBlock: sinon.stub() }; - errordb._onChainAddBlock('block', function(err) { - should.exist(err); - }); - }); - - it('should call block processing functions and write to database', function(done) { - db._onChainAddBlock('block', function(err) { + db.blockHandler = sinon.stub().callsArg(2); + db._onChainAddBlock({hash: 'hash'}, function(err) { should.not.exist(err); - db._updateOutputs.calledOnce.should.equal(true); - db._updateOutputs.calledWith('block', true).should.equal(true); - db.store.batch.args[0][0].should.deep.equal(['1a', '1b']); - done(); - }); - }); - it('should halt on an error and not write to database', function(done) { - db._updateOutputs.reset(); - db.store.batch.reset(); - db._updateOutputs = sinon.stub().callsArgWith(2, new Error('error')); - db._onChainAddBlock('block', function(err) { - should.exist(err); - err.message.should.equal('error'); - db._updateOutputs.calledOnce.should.equal(true); - db._updateOutputs.calledWith('block', true).should.equal(true); - db.store.batch.called.should.equal(false); + db.mempool.removeBlock.args[0][0].should.equal('hash'); + db.blockHandler.args[0][1].should.equal(true); done(); }); }); }); describe('#_onChainRemoveBlock', function() { - var db = new DB({path: 'path', store: memdown}); - db._updateOutputs = sinon.stub().callsArgWith(2, null, ['1a', '1b']); + it('should call blockHandler with false', function(done) { + var db = new DB({store: memdown}); + db.blockHandler = sinon.stub().callsArg(2); + db._onChainRemoveBlock({hash: 'hash'}, function(err) { + should.not.exist(err); + db.blockHandler.args[0][1].should.equal(false); + done(); + }); + }); + }); + + describe('#blockHandler', function() { + var db = new DB({store: memdown}); + var Module1 = function() {}; + Module1.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op1', 'op2', 'op3']); + var Module2 = function() {}; + Module2.prototype.blockHandler = sinon.stub().callsArgWith(2, null, ['op4', 'op5']); + db.modules = [ + new Module1(), + new Module2() + ]; db.store = { batch: sinon.stub().callsArg(1) }; - it('should give error when there is a failure to write', function() { - var errordb = new DB({path: 'path', store: memdown}); - errordb._updateOutputs = sinon.stub().callsArgWith(2, null, ['1a', '1b']); - errordb.store = { - batch: sinon.stub().callsArgWith(1, new Error('error')) - }; - errordb._onChainRemoveBlock('block', function(err) { - should.exist(err); - }); - }); - - it('should call block processing functions and write to database', function(done) { - db._onChainRemoveBlock('block', function(err) { + it('should call blockHandler in all modules and perform operations', function(done) { + db.blockHandler('block', true, function(err) { should.not.exist(err); - db._updateOutputs.calledOnce.should.equal(true); - db._updateOutputs.calledWith('block', false).should.equal(true); - db.store.batch.args[0][0].should.deep.equal(['1a', '1b']); + db.store.batch.args[0][0].should.deep.equal(['op1', 'op2', 'op3', 'op4', 'op5']); done(); }); }); - it('should halt on an error and not write to database', function(done) { - db._updateOutputs.reset(); - db.store.batch.reset(); - db._updateOutputs = sinon.stub().callsArgWith(2, new Error('error')); - db._onChainRemoveBlock('block', function(err) { + + it('should give an error if one of the modules gives an error', function(done) { + var Module3 = function() {}; + Module3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error')); + db.modules.push(new Module3()); + + db.blockHandler('block', true, function(err) { should.exist(err); - err.message.should.equal('error'); - db._updateOutputs.calledOnce.should.equal(true); - db._updateOutputs.calledWith('block', false).should.equal(true); - db.store.batch.called.should.equal(false); done(); }); }); }); describe('#getAPIMethods', function() { - it('should return the correct methods', function() { - var db = new DB({path: 'path', store: memdown}); + it('should return the correct db methods', function() { + var db = new DB({store: memdown}); + db.modules = []; var methods = db.getAPIMethods(); - methods.length.should.equal(5); - }); - }); - - describe('#getBalance', function() { - it('should sum up the unspent outputs', function(done) { - var db = new DB({path: 'path', store: memdown}); - var outputs = [ - {satoshis: 1000}, {satoshis: 2000}, {satoshis: 3000} - ]; - db.getUnspentOutputs = sinon.stub().callsArgWith(2, null, outputs); - db.getBalance('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N', false, function(err, balance) { - should.not.exist(err); - balance.should.equal(6000); - done(); - }); + methods.length.should.equal(1); }); - it('will handle error from unspent outputs', function(done) { - var db = new DB({path: 'path', store: memdown}); - db.getUnspentOutputs = sinon.stub().callsArgWith(2, new Error('error')); - db.getBalance('someaddress', false, function(err) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - - }); - - describe('#getOutputs', function() { - var db = new DB({path: 'path', store: memdown}); - var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; - - it('should get outputs for an address', function(done) { - var readStream1 = new EventEmitter(); - db.store = { - createReadStream: sinon.stub().returns(readStream1) - }; - var mempoolOutputs = [ - { - address: '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', - txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371', - satoshis: 307627737, - script: 'OP_DUP OP_HASH160 f6db95c81dea3d10f0ff8d890927751bf7b203c1 OP_EQUALVERIFY OP_CHECKSIG', - blockHeight: 352532 + it('should also return modules methods', function() { + var module1 = { + methods: function() { + return [ + ['module1-one', module1, module1, 2], + ['module1-two', module1, module1, 2] + ]; } - ]; - db.bitcoind = { - getMempoolOutputs: sinon.stub().returns(mempoolOutputs) }; - - db.getOutputs(address, true, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(3); - outputs[0].address.should.equal(address); - outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); - outputs[0].outputIndex.should.equal(1); - outputs[0].satoshis.should.equal(4527773864); - outputs[0].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG'); - outputs[0].blockHeight.should.equal(345000); - outputs[1].address.should.equal(address); - outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); - outputs[1].outputIndex.should.equal(2); - outputs[1].satoshis.should.equal(10000); - outputs[1].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG'); - outputs[1].blockHeight.should.equal(345004); - outputs[2].address.should.equal(address); - outputs[2].txid.should.equal('aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371'); - outputs[2].script.should.equal('OP_DUP OP_HASH160 f6db95c81dea3d10f0ff8d890927751bf7b203c1 OP_EQUALVERIFY OP_CHECKSIG'); - outputs[2].blockHeight.should.equal(352532); - done(); - }); - - var data1 = { - key: ['outs', address, '1424835319000', '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'), - value: ['4527773864', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345000'].join(':') - }; - - var data2 = { - key: ['outs', address, '1424837300000', '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '2'].join('-'), - value: ['10000', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345004'].join(':') - }; - - readStream1.emit('data', data1); - readStream1.emit('data', data2); - readStream1.emit('close'); - }); - - it('should give an error if the readstream has an error', function(done) { - var readStream2 = new EventEmitter(); - db.store = { - createReadStream: sinon.stub().returns(readStream2) - }; - - db.getOutputs(address, true, function(err, outputs) { - should.exist(err); - err.message.should.equal('readstreamerror'); - done(); - }); - - readStream2.emit('error', new Error('readstreamerror')); - process.nextTick(function() { - readStream2.emit('close'); - }); - }); - }); - - describe('#getUnspentOutputs', function() { - it('should filter out spent outputs', function(done) { - var outputs = [ - { - satoshis: 1000, - spent: false, - }, - { - satoshis: 2000, - spent: true - }, - { - satoshis: 3000, - spent: false + var module2 = { + methods: function() { + return [ + ['moudle2-one', module2, module2, 1] + ]; } - ]; - var i = 0; - - var db = new DB({path: 'path', store: memdown}); - db.getOutputs = sinon.stub().callsArgWith(2, null, outputs); - db.isUnspent = function(output, queryMempool, callback) { - callback(!outputs[i].spent); - i++; }; - db.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.not.exist(err); - outputs.length.should.equal(2); - outputs[0].satoshis.should.equal(1000); - outputs[1].satoshis.should.equal(3000); - done(); - }); - }); - it('should handle an error from getOutputs', function(done) { - var db = new DB({path: 'path', store: memdown}); - db.getOutputs = sinon.stub().callsArgWith(2, new Error('error')); - db.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.exist(err); - err.message.should.equal('error'); - done(); - }); - }); - it('should handle when there are no outputs', function(done) { - var db = new DB({path: 'path', store: memdown}); - db.getOutputs = sinon.stub().callsArgWith(2, null, []); - db.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { - should.exist(err); - err.should.be.instanceof(errors.NoOutputs); - outputs.length.should.equal(0); - done(); - }); + var db = new DB({store: memdown}); + db.modules = [module1, module2]; + + var methods = db.getAPIMethods(); + methods.length.should.equal(4); }); }); - describe('#isUnspent', function() { - var db = new DB({path: 'path', store: memdown}); + describe('#addModule', function() { + it('instantiate module and add to db.modules', function() { + var Module1 = function(options) { + BaseModule.call(this, options); + }; + inherits(Module1, BaseModule); - it('should give true when isSpent() gives false', function(done) { - db.isSpent = sinon.stub().callsArgWith(2, false); - db.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { - unspent.should.equal(true); - done(); - }); + var db = new DB({store: memdown}); + db.bitcoind = {}; + db.network = {}; + db.modules = []; + db.addModule(Module1); + + db.modules.length.should.equal(1); + should.exist(db.modules[0].db); + should.exist(db.modules[0].bitcoind); + should.exist(db.modules[0].network); }); - it('should give false when isSpent() gives true', function(done) { - db.isSpent = sinon.stub().callsArgWith(2, true); - db.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { - unspent.should.equal(false); - done(); - }); - }); + it('should throw an error if module is not an instance of BaseModule', function() { + var Module2 = function(options) {}; + var db = new DB({store: memdown}); + db.modules = []; - it('should give false when isSpent() returns an error', function(done) { - db.isSpent = sinon.stub().callsArgWith(2, new Error('error')); - db.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { - unspent.should.equal(false); - done(); - }); + (function() { + db.addModule(Module2); + }).should.throw('bitcore.ErrorInvalidArgumentType'); }); }); - - describe('#isSpent', function() { - var db = new DB({path: 'path', store: memdown}); - db.bitcoind = { - isSpent: sinon.stub().returns(true) - }; - - it('should give true if bitcoind.isSpent gives true', function(done) { - db.isSpent('output', true, function(spent) { - spent.should.equal(true); - done(); - }); - }); - }); - }); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js new file mode 100644 index 00000000..d0fb446d --- /dev/null +++ b/test/modules/address.unit.js @@ -0,0 +1,329 @@ +'use strict'; + +var should = require('chai').should(); +var sinon = require('sinon'); +var chainlib = require('chainlib'); +var levelup = chainlib.deps.levelup; +var bitcoindjs = require('../../'); +var AddressModule = bitcoindjs.modules.AddressModule; +var blockData = require('../data/livenet-345003.json'); +var bitcore = require('bitcore'); +var EventEmitter = require('events').EventEmitter; +var errors = bitcoindjs.errors; + +describe('AddressModule', function() { + + describe('#methods', function() { + it('should return the correct methods', function() { + var am = new AddressModule({}); + var methods = am.methods(); + methods.length.should.equal(4); + }); + }); + + describe('#blockHandler', function() { + var block = bitcore.Block.fromString(blockData); + var db = { + getTransactionsFromBlock: function() { + return block.transactions.slice(0, 8); + } + }; + var am = new AddressModule({db: db, network: 'livenet'}); + + var data = [ + { + key: { + address: '1F1MAvhTKg2VG29w8cXsiSN2PJ8gSsrJw', + timestamp: 1424836934000, + txid: 'fdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e16923', + outputIndex: 0 + }, + value: { + satoshis: 2502227470, + script: 'OP_DUP OP_HASH160 20 0x02a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b OP_EQUALVERIFY OP_CHECKSIG', + blockHeight: 345003 + } + }, + { + key: { + prevTxId: '3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a9', + prevOutputIndex: 32 + }, + value: { + txid: '5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca', + inputIndex: 0, + timestamp: 1424836934000 + } + }, + { + key: { + address: '1Ep5LA4T6Y7zaBPiwruUJurjGFvCJHzJhm', + timestamp: 1424836934000, + txid: 'e66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d', + outputIndex: 1 + }, + value: { + satoshis: 3100000, + script: 'OP_DUP OP_HASH160 20 0x9780ccd5356e2acc0ee439ee04e0fe69426c7528 OP_EQUALVERIFY OP_CHECKSIG', + blockHeight: 345003 + } + } + ]; + var key0 = data[0].key; + var value0 = data[0].value; + var key3 = data[1].key; + var value3 = data[1].value; + var key64 = data[2].key; + var value64 = data[2].value; + + it('should create the correct operations when updating/adding outputs', function(done) { + am.blockHandler({height: 345003, timestamp: new Date(1424836934000)}, true, function(err, operations) { + should.not.exist(err); + operations.length.should.equal(11); + operations[0].type.should.equal('put'); + var expected0 = ['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-'); + operations[0].key.should.equal(expected0); + operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':')); + done(); + }); + }); + it('should create the correct operations when removing outputs', function(done) { + am.blockHandler({height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { + should.not.exist(err); + operations.length.should.equal(11); + operations[0].type.should.equal('del'); + operations[0].key.should.equal(['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-')); + operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':')); + done(); + }); + }); + it('should continue if output script is null', function(done) { + var transactions = [ + { + inputs: [], + outputs: [ + { + script: null, + satoshis: 1000, + } + ], + isCoinbase: sinon.stub().returns(false) + } + ]; + var db = { + getTransactionsFromBlock: function() { + return transactions; + } + }; + + var am = new AddressModule({db: db, network: 'livenet'}); + + am.blockHandler({height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { + should.not.exist(err); + operations.length.should.equal(0); + done(); + }); + }); + }); + + describe('#getBalance', function() { + it('should sum up the unspent outputs', function(done) { + var am = new AddressModule({}); + var outputs = [ + {satoshis: 1000}, {satoshis: 2000}, {satoshis: 3000} + ]; + am.getUnspentOutputs = sinon.stub().callsArgWith(2, null, outputs); + am.getBalance('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N', false, function(err, balance) { + should.not.exist(err); + balance.should.equal(6000); + done(); + }); + }); + + it('will handle error from unspent outputs', function(done) { + var am = new AddressModule({}); + am.getUnspentOutputs = sinon.stub().callsArgWith(2, new Error('error')); + am.getBalance('someaddress', false, function(err) { + should.exist(err); + err.message.should.equal('error'); + done(); + }); + }); + + }); + + describe('#getOutputs', function() { + var am = new AddressModule({db: {}}); + var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; + + it('should get outputs for an address', function(done) { + var readStream1 = new EventEmitter(); + am.db.store = { + createReadStream: sinon.stub().returns(readStream1) + }; + var mempoolOutputs = [ + { + address: '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', + txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371', + satoshis: 307627737, + script: 'OP_DUP OP_HASH160 f6db95c81dea3d10f0ff8d890927751bf7b203c1 OP_EQUALVERIFY OP_CHECKSIG', + blockHeight: 352532 + } + ]; + am.bitcoind = { + getMempoolOutputs: sinon.stub().returns(mempoolOutputs) + }; + + am.getOutputs(address, true, function(err, outputs) { + should.not.exist(err); + outputs.length.should.equal(3); + outputs[0].address.should.equal(address); + outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); + outputs[0].outputIndex.should.equal(1); + outputs[0].satoshis.should.equal(4527773864); + outputs[0].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG'); + outputs[0].blockHeight.should.equal(345000); + outputs[1].address.should.equal(address); + outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); + outputs[1].outputIndex.should.equal(2); + outputs[1].satoshis.should.equal(10000); + outputs[1].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG'); + outputs[1].blockHeight.should.equal(345004); + outputs[2].address.should.equal(address); + outputs[2].txid.should.equal('aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371'); + outputs[2].script.should.equal('OP_DUP OP_HASH160 f6db95c81dea3d10f0ff8d890927751bf7b203c1 OP_EQUALVERIFY OP_CHECKSIG'); + outputs[2].blockHeight.should.equal(352532); + done(); + }); + + var data1 = { + key: ['outs', address, '1424835319000', '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'), + value: ['4527773864', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345000'].join(':') + }; + + var data2 = { + key: ['outs', address, '1424837300000', '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '2'].join('-'), + value: ['10000', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345004'].join(':') + }; + + readStream1.emit('data', data1); + readStream1.emit('data', data2); + readStream1.emit('close'); + }); + + it('should give an error if the readstream has an error', function(done) { + var readStream2 = new EventEmitter(); + am.db.store = { + createReadStream: sinon.stub().returns(readStream2) + }; + + am.getOutputs(address, true, function(err, outputs) { + should.exist(err); + err.message.should.equal('readstreamerror'); + done(); + }); + + readStream2.emit('error', new Error('readstreamerror')); + process.nextTick(function() { + readStream2.emit('close'); + }); + }); + }); + + describe('#getUnspentOutputs', function() { + it('should filter out spent outputs', function(done) { + var outputs = [ + { + satoshis: 1000, + spent: false, + }, + { + satoshis: 2000, + spent: true + }, + { + satoshis: 3000, + spent: false + } + ]; + var i = 0; + + var am = new AddressModule({}); + am.getOutputs = sinon.stub().callsArgWith(2, null, outputs); + am.isUnspent = function(output, queryMempool, callback) { + callback(!outputs[i].spent); + i++; + }; + + am.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { + should.not.exist(err); + outputs.length.should.equal(2); + outputs[0].satoshis.should.equal(1000); + outputs[1].satoshis.should.equal(3000); + done(); + }); + }); + it('should handle an error from getOutputs', function(done) { + var am = new AddressModule({}); + am.getOutputs = sinon.stub().callsArgWith(2, new Error('error')); + am.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { + should.exist(err); + err.message.should.equal('error'); + done(); + }); + }); + it('should handle when there are no outputs', function(done) { + var am = new AddressModule({}); + am.getOutputs = sinon.stub().callsArgWith(2, null, []); + am.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) { + should.exist(err); + err.should.be.instanceof(errors.NoOutputs); + outputs.length.should.equal(0); + done(); + }); + }); + }); + + describe('#isUnspent', function() { + var am = new AddressModule({}); + + it('should give true when isSpent() gives false', function(done) { + am.isSpent = sinon.stub().callsArgWith(2, false); + am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { + unspent.should.equal(true); + done(); + }); + }); + + it('should give false when isSpent() gives true', function(done) { + am.isSpent = sinon.stub().callsArgWith(2, true); + am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { + unspent.should.equal(false); + done(); + }); + }); + + it('should give false when isSpent() returns an error', function(done) { + am.isSpent = sinon.stub().callsArgWith(2, new Error('error')); + am.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) { + unspent.should.equal(false); + done(); + }); + }); + }); + + describe('#isSpent', function() { + var am = new AddressModule({}); + am.bitcoind = { + isSpent: sinon.stub().returns(true) + }; + + it('should give true if bitcoind.isSpent gives true', function(done) { + am.isSpent('output', true, function(spent) { + spent.should.equal(true); + done(); + }); + }); + }); + +}); From 1aee45e42338850f4296c2a8a8f3d13597407f43 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Thu, 23 Jul 2015 10:55:50 -0600 Subject: [PATCH 3/7] add Module to index.js --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 857a26d7..3e625d9f 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ module.exports.Block = require('./lib/block'); module.exports.Chain = require('./lib/chain'); module.exports.DB = require('./lib/db'); module.exports.Transaction = require('./lib/transaction'); +module.exports.Module = require('./lib/module'); module.exports.errors = require('./lib/errors'); module.exports.modules = {}; From 8290d61a09a09a87dfc3a73c66251c2347bb9caa Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Thu, 23 Jul 2015 13:13:06 -0600 Subject: [PATCH 4/7] only pass db as option to module because other params aren't available until init --- lib/db.js | 4 +--- lib/module.js | 2 -- lib/modules/address.js | 10 +++++----- test/db.unit.js | 4 ---- test/modules/address.unit.js | 12 ++++++------ 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/db.js b/lib/db.js index 4f6ee98a..14983950 100644 --- a/lib/db.js +++ b/lib/db.js @@ -222,9 +222,7 @@ DB.prototype.getAPIMethods = function() { DB.prototype.addModule = function(Module) { var module = new Module({ - db: this, - bitcoind: this.bitcoind, - network: this.network + db: this }); $.checkArgumentType(module, BaseModule); this.modules.push(module); diff --git a/lib/module.js b/lib/module.js index 896f8a95..9624e96c 100644 --- a/lib/module.js +++ b/lib/module.js @@ -2,8 +2,6 @@ var Module = function(options) { this.db = options.db; - this.bitcoind = options.bitcoind; - this.network = options.network; }; Module.prototype.blockHandler = function(block, add, callback) { diff --git a/lib/modules/address.js b/lib/modules/address.js index 1c77b761..4b21e05b 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -67,15 +67,15 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) { if(script.isPublicKeyOut()) { var pubkey = script.chunks[0].buf; - address = Address.fromPublicKey(new PublicKey(pubkey), this.network); + address = Address.fromPublicKey(new PublicKey(pubkey), this.db.network); } else { - address = output.script.toAddress(this.network); + address = output.script.toAddress(this.db.network); } var outputIndex = j; var timestamp = block.timestamp.getTime(); - var height = block.height; + var height = block.__height; operations.push({ type: action, @@ -156,7 +156,7 @@ AddressModule.prototype.getOutputs = function(address, queryMempool, callback) { } if(queryMempool) { - outputs = outputs.concat(self.bitcoind.getMempoolOutputs(address)); + outputs = outputs.concat(self.db.bitcoind.getMempoolOutputs(address)); } callback(null, outputs); @@ -198,7 +198,7 @@ AddressModule.prototype.isSpent = function(output, queryMempool, callback) { var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid; setImmediate(function() { - callback(self.bitcoind.isSpent(txid, output.outputIndex)); + callback(self.db.bitcoind.isSpent(txid, output.outputIndex)); }); }; diff --git a/test/db.unit.js b/test/db.unit.js index c74ca0b2..96a75339 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -301,15 +301,11 @@ describe('Bitcoin DB', function() { inherits(Module1, BaseModule); var db = new DB({store: memdown}); - db.bitcoind = {}; - db.network = {}; db.modules = []; db.addModule(Module1); db.modules.length.should.equal(1); should.exist(db.modules[0].db); - should.exist(db.modules[0].bitcoind); - should.exist(db.modules[0].network); }); it('should throw an error if module is not an instance of BaseModule', function() { diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index d0fb446d..d5c10499 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -77,7 +77,7 @@ describe('AddressModule', function() { var value64 = data[2].value; it('should create the correct operations when updating/adding outputs', function(done) { - am.blockHandler({height: 345003, timestamp: new Date(1424836934000)}, true, function(err, operations) { + am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, true, function(err, operations) { should.not.exist(err); operations.length.should.equal(11); operations[0].type.should.equal('put'); @@ -88,7 +88,7 @@ describe('AddressModule', function() { }); }); it('should create the correct operations when removing outputs', function(done) { - am.blockHandler({height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { + am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { should.not.exist(err); operations.length.should.equal(11); operations[0].type.should.equal('del'); @@ -118,7 +118,7 @@ describe('AddressModule', function() { var am = new AddressModule({db: db, network: 'livenet'}); - am.blockHandler({height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { + am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) { should.not.exist(err); operations.length.should.equal(0); done(); @@ -170,7 +170,7 @@ describe('AddressModule', function() { blockHeight: 352532 } ]; - am.bitcoind = { + am.db.bitcoind = { getMempoolOutputs: sinon.stub().returns(mempoolOutputs) }; @@ -313,8 +313,8 @@ describe('AddressModule', function() { }); describe('#isSpent', function() { - var am = new AddressModule({}); - am.bitcoind = { + var am = new AddressModule({db: {}}); + am.db.bitcoind = { isSpent: sinon.stub().returns(true) }; From 990f508bbbcab0e0aa12eea9cf73a4a56091d2fc Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Thu, 23 Jul 2015 14:52:22 -0600 Subject: [PATCH 5/7] update README with module docs --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/module.js | 10 ++++++ 2 files changed, 98 insertions(+) diff --git a/README.md b/README.md index a76252e8..4d32ff89 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,94 @@ $ tail -f ~/.bitcoin/debug.log ^C (SIGINT) will call `StartShutdown()` in bitcoind on the node thread pool. +## Modules + +Bitcoind.js has a module system where additional information can be indexed and queried from +the blockchain. One built-in module is the address module which exposes the API methods for getting balances and outputs. + +### Writing a module + +A new module can be created by inheriting from `BitcoindJS.Module`, implementing the methods() and blockHandler() methods, and any additional methods for querying the data. Here is an example: + +```js +var inherits = require('util').inherits; +var BitcoindJS = require('bitcoind.js'); + +var MyModule = function(options) { + BitcoindJS.Module.call(this, options); +}; + +inherits(MyModule, BitcoindJS.Module); + +/** + * blockHandler + * @param {Block} block - the block being added or removed from the chain + * @param {Boolean} add - whether the block is being added or removed + * @param {Function} callback - call with the leveldb database operations to perform + */ +MyModule.prototype.blockHandler = function(block, add, callback) { + var transactions = this.db.getTransactionsFromBlock(block); + // loop through transactions and outputs + // call the callback with leveldb database operations + var operations = []; + if(add) { + operations.push({ + type: 'put', + key: 'key', + value: 'value' + }); + } else { + operations.push({ + type: 'del', + key: 'key' + }); + } + + // If your function is not asynchronous, it is important to use setImmediate. + setImmediate(function() { + callback(null, operations); + }); +}; + +/** + * the API methods to expose + * @return {Array} return array of methods + */ +MyModule.prototype.methods = function() { + return [ + ['getData', this, this.getData, 1] + ]; +}; + +MyModule.prototype.getData = function(arg1, callback) { + // You can query the data by reading from the leveldb store on db + this.db.store.get(arg1, callback); +}; + +module.exports = MyModule; +``` + +The module can then be used when running a node: + +```js +var configuration = { + datadir: process.env.BITCOINDJS_DIR || '~/.bitcoin', + db: { + modules: [MyModule] + } +}; + +var node = new BitcoindJS.Node(configuration); + +node.on('ready', function() { + node.getData('key', function(err, value) { + console.log(err || value); + }); +}); +``` + +Note that if you already have a bitcoind.js database, and you want to query data from previous blocks in the blockchain, you will need to reindex. Reindexing right now means deleting your bitcoind.js database and resyncing. + ## Daemon Documentation - `daemon.start([options], [callback])` - Start the JavaScript Bitcoin node. diff --git a/lib/module.js b/lib/module.js index 9624e96c..8d87d42d 100644 --- a/lib/module.js +++ b/lib/module.js @@ -4,11 +4,21 @@ var Module = function(options) { this.db = options.db; }; +/** + * blockHandler + * @param {Block} block - the block being added or removed from the chain + * @param {Boolean} add - whether the block is being added or removed + * @param {Function} callback - call with the leveldb database operations to perform + */ Module.prototype.blockHandler = function(block, add, callback) { // implement in the child class setImmediate(callback); }; +/** + * the API methods to expose + * @return {Array} return array of methods + */ Module.prototype.methods = function() { // Example: // return [ From fd8ee3ba9dd0d7bd8e7792a389850ceb221cd16a Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Thu, 23 Jul 2015 15:08:09 -0600 Subject: [PATCH 6/7] expose getBlock method --- lib/db.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/db.js b/lib/db.js index 14983950..12201676 100644 --- a/lib/db.js +++ b/lib/db.js @@ -210,6 +210,7 @@ DB.prototype.blockHandler = function(block, add, callback) { DB.prototype.getAPIMethods = function() { var methods = [ + ['getBlock', this, this.getBlock, 1], ['getTransaction', this, this.getTransaction, 2] ]; From 9108b0f6954cf1050b755d34f4a5e913406350bb Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Thu, 23 Jul 2015 15:29:10 -0600 Subject: [PATCH 7/7] rename methods to getAPIMethods --- README.md | 6 +++--- lib/db.js | 2 +- lib/module.js | 2 +- lib/modules/address.js | 2 +- test/db.unit.js | 10 +++++----- test/modules/address.unit.js | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4d32ff89..4ce25789 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,9 @@ $ tail -f ~/.bitcoin/debug.log Bitcoind.js has a module system where additional information can be indexed and queried from the blockchain. One built-in module is the address module which exposes the API methods for getting balances and outputs. -### Writing a module +### Writing a Module -A new module can be created by inheriting from `BitcoindJS.Module`, implementing the methods() and blockHandler() methods, and any additional methods for querying the data. Here is an example: +A new module can be created by inheriting from `BitcoindJS.Module`, implementing the methods `blockHandler()` and `getAPIMethods()`, and any additional methods for querying the data. Here is an example: ```js var inherits = require('util').inherits; @@ -152,7 +152,7 @@ MyModule.prototype.blockHandler = function(block, add, callback) { * the API methods to expose * @return {Array} return array of methods */ -MyModule.prototype.methods = function() { +MyModule.prototype.getAPIMethods = function() { return [ ['getData', this, this.getData, 1] ]; diff --git a/lib/db.js b/lib/db.js index 12201676..7d7b6298 100644 --- a/lib/db.js +++ b/lib/db.js @@ -215,7 +215,7 @@ DB.prototype.getAPIMethods = function() { ]; for(var i = 0; i < this.modules.length; i++) { - methods = methods.concat(this.modules[i]['methods'].call(this.modules[i])); + methods = methods.concat(this.modules[i]['getAPIMethods'].call(this.modules[i])); } return methods; diff --git a/lib/module.js b/lib/module.js index 8d87d42d..1acce6c4 100644 --- a/lib/module.js +++ b/lib/module.js @@ -19,7 +19,7 @@ Module.prototype.blockHandler = function(block, add, callback) { * the API methods to expose * @return {Array} return array of methods */ -Module.prototype.methods = function() { +Module.prototype.getAPIMethods = function() { // Example: // return [ // ['getData', this, this.getData, 1] diff --git a/lib/modules/address.js b/lib/modules/address.js index 4b21e05b..fd340069 100644 --- a/lib/modules/address.js +++ b/lib/modules/address.js @@ -20,7 +20,7 @@ AddressModule.PREFIXES = { OUTPUTS: 'outs' }; -AddressModule.prototype.methods = function() { +AddressModule.prototype.getAPIMethods = function() { return [ ['getBalance', this, this.getBalance, 2], ['getOutputs', this, this.getOutputs, 2], diff --git a/test/db.unit.js b/test/db.unit.js index 96a75339..035d23e8 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -265,12 +265,12 @@ describe('Bitcoin DB', function() { var db = new DB({store: memdown}); db.modules = []; var methods = db.getAPIMethods(); - methods.length.should.equal(1); + methods.length.should.equal(2); }); - it('should also return modules methods', function() { + it('should also return modules API methods', function() { var module1 = { - methods: function() { + getAPIMethods: function() { return [ ['module1-one', module1, module1, 2], ['module1-two', module1, module1, 2] @@ -278,7 +278,7 @@ describe('Bitcoin DB', function() { } }; var module2 = { - methods: function() { + getAPIMethods: function() { return [ ['moudle2-one', module2, module2, 1] ]; @@ -289,7 +289,7 @@ describe('Bitcoin DB', function() { db.modules = [module1, module2]; var methods = db.getAPIMethods(); - methods.length.should.equal(4); + methods.length.should.equal(5); }); }); diff --git a/test/modules/address.unit.js b/test/modules/address.unit.js index d5c10499..2741dfbc 100644 --- a/test/modules/address.unit.js +++ b/test/modules/address.unit.js @@ -13,10 +13,10 @@ var errors = bitcoindjs.errors; describe('AddressModule', function() { - describe('#methods', function() { + describe('#getAPIMethods', function() { it('should return the correct methods', function() { var am = new AddressModule({}); - var methods = am.methods(); + var methods = am.getAPIMethods(); methods.length.should.equal(4); }); });