commit
ac09e767fb
88
README.md
88
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 `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.
|
||||
|
||||
4
index.js
4
index.js
@ -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
277
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,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
36
lib/module.js
Normal 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
205
lib/modules/address.js
Normal 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;
|
||||
445
test/db.unit.js
445
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,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
329
test/modules/address.unit.js
Normal file
329
test/modules/address.unit.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user