Merge pull request #45 from pnagurny/feature/modules

Module system
This commit is contained in:
Braydon Fuller 2015-07-23 17:59:36 -04:00
commit ac09e767fb
7 changed files with 805 additions and 579 deletions

View File

@ -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 `blockHandler()` and `getAPIMethods()`, 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.getAPIMethods = 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.

View File

@ -7,7 +7,11 @@ 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 = {};
module.exports.modules.AddressModule = require('./lib/modules/address');
module.exports.deps = {};
module.exports.deps.chainlib = require('chainlib');

277
lib/db.js
View File

@ -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,66 @@ 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 = [
['getBlock', this, this.getBlock, 1],
['getTransaction', this, this.getTransaction, 2]
];
for(var i = 0; i < this.modules.length; i++) {
methods = methods.concat(this.modules[i]['getAPIMethods'].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
});
$.checkArgumentType(module, BaseModule);
this.modules.push(module);
};
module.exports = DB;

36
lib/module.js Normal file
View File

@ -0,0 +1,36 @@
'use strict';
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.getAPIMethods = function() {
// Example:
// return [
// ['getData', this, this.getData, 1]
// ];
return [];
};
// Example:
// Module.prototype.getData = function(arg1, callback) {
//
// };
module.exports = Module;

205
lib/modules/address.js Normal file
View File

@ -0,0 +1,205 @@
'use strict';
var BaseModule = require('../module');
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;
var AddressModule = function(options) {
BaseModule.call(this, options);
};
inherits(AddressModule, BaseModule);
AddressModule.PREFIXES = {
OUTPUTS: 'outs'
};
AddressModule.prototype.getAPIMethods = 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.db.network);
} else {
address = output.script.toAddress(this.db.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.db.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.db.bitcoind.isSpent(txid, output.outputIndex));
});
};
module.exports = AddressModule;

View File

@ -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,124 @@ 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(2);
});
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(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();
});
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('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();
});
});
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 = [];
});
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
}
];
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');
});
(function() {
db.addModule(Module2);
}).should.throw('bitcore.ErrorInvalidArgumentType');
});
});
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 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();
});
});
});
describe('#isUnspent', function() {
var db = new DB({path: 'path', store: memdown});
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();
});
});
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 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();
});
});
});
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();
});
});
});
});

View File

@ -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('#getAPIMethods', function() {
it('should return the correct methods', function() {
var am = new AddressModule({});
var methods = am.getAPIMethods();
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.db.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({db: {}});
am.db.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();
});
});
});
});