'use strict'; var should = require('chai').should(); var sinon = require('sinon'); var chainlib = require('chainlib'); var levelup = chainlib.deps.levelup; var bitcoindjs = require('../'); var DB = bitcoindjs.DB; var blockData = require('./data/livenet-345003.json'); 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; describe('#getBlock', function() { var db = new DB({store: memdown}); db.bitcoind = { getBlock: sinon.stub().callsArgWith(1, null, new Buffer(blockData, 'hex')) }; db.Block = { fromBuffer: sinon.stub().returns('block') }; it('should get the block from bitcoind.js', function(done) { db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) { should.not.exist(err); block.should.equal('block'); done(); }); }); it('should give an error when bitcoind.js gives an error', function(done) { db.bitcoind.getBlock = sinon.stub().callsArgWith(1, new Error('error')); db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) { should.exist(err); err.message.should.equal('error'); done(); }); }); }); describe('#putBlock', function() { it('should call callback', function(done) { var db = new DB({store: memdown}); db.putBlock('block', function(err) { should.not.exist(err); done(); }); }); }); describe('#buildGenesisData', function() { it('build genisis data', function() { var db = new DB({path: 'path', store: memdown}); db.buildCoinbaseTransaction = sinon.stub().returns({ toBuffer: sinon.stub().returns(new Buffer('abcdef', 'hex')) }); db.getMerkleRoot = sinon.stub().returns('merkleRoot'); var data = db.buildGenesisData(); data.buffer.should.deep.equal(new Buffer('01abcdef', 'hex')); data.merkleRoot.should.equal('merkleRoot'); }); }); describe('#buildCoinbaseTransaction', function() { it('should correctly build a coinbase transaction with no fees', function() { var db = new DB({path: 'path', store: memdown}); db.coinbaseAddress = 'mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L'; db.coinbaseAmount = coinbaseAmount; var coinbaseTx = db.buildCoinbaseTransaction(); coinbaseTx.inputs.length.should.equal(1); var input = coinbaseTx.inputs[0]; var expectedTxId = '0000000000000000000000000000000000000000000000000000000000000000'; input.prevTxId.toString('hex').should.equal(expectedTxId); should.exist(input.outputIndex); should.exist(input.sequenceNumber); should.exist(input._script); // coinbase input script returns null coinbaseTx.outputs.length.should.equal(1); var output = coinbaseTx.outputs[0]; output.satoshis.should.equal(coinbaseAmount); }); it('should correctly build a coinbase transaction with fees', function() { var db = new DB({path: 'path', store: memdown}); db.coinbaseAddress = 'mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L'; db.coinbaseAmount = coinbaseAmount; var transactions = [ { _getInputAmount: sinon.stub().returns(5000), _getOutputAmount: sinon.stub().returns(4000), isCoinbase: sinon.stub().returns(false) }, { _getInputAmount: sinon.stub().returns(8000), _getOutputAmount: sinon.stub().returns(7000), isCoinbase: sinon.stub().returns(false) } ]; var coinbaseTx = db.buildCoinbaseTransaction(transactions); coinbaseTx.inputs.length.should.equal(1); var input = coinbaseTx.inputs[0]; var expectedTxId = '0000000000000000000000000000000000000000000000000000000000000000'; input.prevTxId.toString('hex').should.equal(expectedTxId); should.exist(input.outputIndex); should.exist(input.sequenceNumber); should.exist(input._script); // coinbase input returns null coinbaseTx.outputs.length.should.equal(1); var output = coinbaseTx.outputs[0]; output.satoshis.should.equal(coinbaseAmount + 2000); }); it('should throw an error if coinbaseAddress not included', function() { var db = new DB({path: 'path', store: memdown}); (function() { db.buildCoinbaseTransaction(); }).should.throw('coinbaseAddress required to build coinbase'); }); it('will build a coinbase database with different data', function() { var db = new DB({path: 'path', store: memdown}); db.coinbaseAddress = 'mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L'; var tx1 = db.buildCoinbaseTransaction().uncheckedSerialize(); var tx2 = db.buildCoinbaseTransaction().uncheckedSerialize(); tx1.should.not.equal(tx2); }); it('can pass in custom data', function() { var db = new DB({path: 'path', store: memdown}); db.coinbaseAddress = 'mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L'; var tx1 = db.buildCoinbaseTransaction(null, new Buffer('abcdef', 'hex')); var data = tx1.inputs[0]._script.getData(); data.should.deep.equal(new Buffer('abcdef', 'hex')); }); }); describe('#getOutputTotal', function() { it('should return the correct value including the coinbase', function() { var totals = [10, 20, 30]; var db = new DB({path: 'path', store: memdown}); var transactions = totals.map(function(total) { return { _getOutputAmount: function() { return total; }, isCoinbase: function() { return total === 10 ? true : false; } }; }); var grandTotal = db.getOutputTotal(transactions); grandTotal.should.equal(60); }); it('should return the correct value excluding the coinbase', function() { var totals = [10, 20, 30]; var db = new DB({path: 'path', store: memdown}); var transactions = totals.map(function(total) { return { _getOutputAmount: function() { return total; }, isCoinbase: function() { return total === 10 ? true : false; } }; }); var grandTotal = db.getOutputTotal(transactions, true); grandTotal.should.equal(50) }); }); describe('#getInputTotal', function() { it('should return the correct value', function() { var totals = [10, 20, 30]; var db = new DB({path: 'path', store: memdown}); var transactions = totals.map(function(total) { return { _getInputAmount: function() { return total; }, isCoinbase: sinon.stub().returns(false) }; }); var grandTotal = db.getInputTotal(transactions); grandTotal.should.equal(60); }); it('should return 0 if the tx is a coinbase', function() { var db = new DB({store: memdown}); var tx = { isCoinbase: sinon.stub().returns(true) }; var total = db.getInputTotal([tx]); total.should.equal(0); }); }); describe('#_onChainAddBlock', function() { it('should remove block from mempool and call blockHandler with true', function(done) { var db = new DB({store: memdown}); db.mempool = { removeBlock: sinon.stub() }; db.blockHandler = sinon.stub().callsArg(2); db._onChainAddBlock({hash: 'hash'}, function(err) { should.not.exist(err); db.mempool.removeBlock.args[0][0].should.equal('hash'); db.blockHandler.args[0][1].should.equal(true); done(); }); }); }); describe('#_onChainRemoveBlock', function() { 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 call blockHandler in all modules and perform operations', function(done) { db.blockHandler('block', true, function(err) { should.not.exist(err); db.store.batch.args[0][0].should.deep.equal(['op1', 'op2', 'op3', 'op4', 'op5']); done(); }); }); 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); done(); }); }); }); describe('#getAPIMethods', function() { it('should return the correct db methods', function() { var db = new DB({store: memdown}); db.modules = []; var methods = db.getAPIMethods(); methods.length.should.equal(1); }); it('should also return modules methods', function() { var module1 = { methods: function() { return [ ['module1-one', module1, module1, 2], ['module1-two', module1, module1, 2] ]; } }; var module2 = { methods: function() { return [ ['moudle2-one', module2, module2, 1] ]; } }; var db = new DB({store: memdown}); db.modules = [module1, module2]; var methods = db.getAPIMethods(); methods.length.should.equal(4); }); }); describe('#addModule', function() { it('instantiate module and add to db.modules', function() { var Module1 = function(options) { BaseModule.call(this, options); }; inherits(Module1, BaseModule); var db = new DB({store: memdown}); db.modules = []; db.addModule(Module1); db.modules.length.should.equal(1); should.exist(db.modules[0].db); }); 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 = []; (function() { db.addModule(Module2); }).should.throw('bitcore.ErrorInvalidArgumentType'); }); }); });