496 lines
16 KiB
JavaScript
496 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
var should = require('chai').should();
|
|
var sinon = require('sinon');
|
|
var bitcoindjs = require('../');
|
|
var DB = bitcoindjs.DB;
|
|
var blockData = require('./data/livenet-345003.json');
|
|
var transactionData = require('./data/bitcoin-transactions.json');
|
|
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('#start', 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();
|
|
var readyFired = false;
|
|
db.on('ready', function() {
|
|
readyFired = true;
|
|
});
|
|
db.start(function() {
|
|
readyFired.should.equal(true);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#stop', function() {
|
|
it('should immediately call the callback', function(done) {
|
|
var db = new DB({store: memdown});
|
|
|
|
db.stop(function(err) {
|
|
should.not.exist(err);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#getTransaction', function() {
|
|
it('will return a NotFound error', function(done) {
|
|
var db = new DB({store: memdown});
|
|
db.bitcoind = {
|
|
getTransaction: sinon.stub().callsArgWith(2, null, null)
|
|
};
|
|
var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623';
|
|
db.getTransaction(txid, true, function(err) {
|
|
err.should.be.instanceof(errors.Transaction.NotFound);
|
|
done();
|
|
});
|
|
});
|
|
it('will return an error from bitcoind', function(done) {
|
|
var db = new DB({store: memdown});
|
|
db.bitcoind = {
|
|
getTransaction: sinon.stub().callsArgWith(2, new Error('test error'))
|
|
};
|
|
var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623';
|
|
db.getTransaction(txid, true, function(err) {
|
|
err.message.should.equal('test error');
|
|
done();
|
|
});
|
|
});
|
|
it('will return an error from bitcoind', function(done) {
|
|
var db = new DB({store: memdown});
|
|
db.bitcoind = {
|
|
getTransaction: sinon.stub().callsArgWith(2, null, new Buffer(transactionData[0].hex, 'hex'))
|
|
};
|
|
var txid = '7426c707d0e9705bdd8158e60983e37d0f5d63529086d6672b07d9238d5aa623';
|
|
db.getTransaction(txid, true, function(err, tx) {
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
should.exist(tx);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
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('#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});
|
|
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(4);
|
|
});
|
|
|
|
it('should also return modules API methods', function() {
|
|
var module1 = {
|
|
getAPIMethods: function() {
|
|
return [
|
|
['module1-one', module1, module1, 2],
|
|
['module1-two', module1, module1, 2]
|
|
];
|
|
}
|
|
};
|
|
var module2 = {
|
|
getAPIMethods: 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(7);
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
});
|