Pagination for Address History
- Reindexing the bitcore-node database is required with this change. - Address indexes are updated to include address and height in spent index so that both inputs and outputs can be queried by address and height using "start" and "stop" to limit the range of the query. - Address history also now supports paginated results using "from" and "to" values that indicate an index in the array.
This commit is contained in:
parent
c8b0dd0999
commit
c205f781a5
@ -24,6 +24,7 @@ var should = chai.should();
|
||||
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var index = require('..');
|
||||
var Transaction = index.Transaction;
|
||||
var BitcoreNode = index.Node;
|
||||
var AddressService = index.services.Address;
|
||||
var BitcoinService = index.services.Bitcoin;
|
||||
@ -34,6 +35,8 @@ var client;
|
||||
|
||||
describe('Node Functionality', function() {
|
||||
|
||||
var regtest;
|
||||
|
||||
before(function(done) {
|
||||
this.timeout(30000);
|
||||
|
||||
@ -51,6 +54,7 @@ describe('Node Functionality', function() {
|
||||
port: 18444,
|
||||
dnsSeeds: [ ]
|
||||
});
|
||||
regtest = bitcore.Networks.get('regtest');
|
||||
|
||||
var datadir = __dirname + '/data';
|
||||
|
||||
@ -238,4 +242,373 @@ describe('Node Functionality', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Address Functionality', function() {
|
||||
var address;
|
||||
var unspentOutput;
|
||||
before(function() {
|
||||
address = testKey.toAddress().toString();
|
||||
});
|
||||
it('should be able to get the balance of the test address', function(done) {
|
||||
node.services.address.getBalance(address, false, function(err, balance) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
balance.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get unspent outputs for address', function(done) {
|
||||
node.services.address.getUnspentOutputs(address, false, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(1);
|
||||
unspentOutput = results[0];
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('correctly give the history for the address', function(done) {
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 10,
|
||||
queryMempool: false
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(1);
|
||||
var info = results[0];
|
||||
info.address.should.equal(address);
|
||||
info.satoshis.should.equal(10 * 1e8);
|
||||
info.confirmations.should.equal(3);
|
||||
info.timestamp.should.be.a('number');
|
||||
info.fees.should.be.within(190, 193);
|
||||
info.outputIndexes.length.should.equal(1);
|
||||
info.outputIndexes[0].should.be.within(0, 1);
|
||||
info.inputIndexes.should.deep.equal([]);
|
||||
info.tx.should.be.an.instanceof(Transaction);
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe('History', function() {
|
||||
|
||||
this.timeout(20000);
|
||||
|
||||
var testKey2;
|
||||
var address2;
|
||||
var testKey3;
|
||||
var address3;
|
||||
var testKey4;
|
||||
var address4;
|
||||
var testKey5;
|
||||
var address5;
|
||||
var testKey6;
|
||||
var address6;
|
||||
|
||||
before(function(done) {
|
||||
/* jshint maxstatements: 50 */
|
||||
|
||||
testKey2 = bitcore.PrivateKey.fromWIF('cNfF4jXiLHQnFRsxaJyr2YSGcmtNYvxQYSakNhuDGxpkSzAwn95x');
|
||||
address2 = testKey2.toAddress().toString();
|
||||
|
||||
testKey3 = bitcore.PrivateKey.fromWIF('cVTYQbaFNetiZcvxzXcVMin89uMLC43pEBMy2etgZHbPPxH5obYt');
|
||||
address3 = testKey3.toAddress().toString();
|
||||
|
||||
testKey4 = bitcore.PrivateKey.fromWIF('cPNQmfE31H2oCUFqaHpfSqjDibkt7XoT2vydLJLDHNTvcddCesGw');
|
||||
address4 = testKey4.toAddress().toString();
|
||||
|
||||
testKey5 = bitcore.PrivateKey.fromWIF('cVrzm9gCmnzwEVMGeCxY6xLVPdG3XWW97kwkFH3H3v722nb99QBF');
|
||||
address5 = testKey5.toAddress().toString();
|
||||
|
||||
testKey6 = bitcore.PrivateKey.fromWIF('cPfMesNR2gsQEK69a6xe7qE44CZEZavgMUak5hQ74XDgsRmmGBYF');
|
||||
address6 = testKey6.toAddress().toString();
|
||||
|
||||
var tx = new Transaction();
|
||||
tx.from(unspentOutput);
|
||||
tx.to(address, 1 * 1e8);
|
||||
tx.to(address, 2 * 1e8);
|
||||
tx.to(address, 0.5 * 1e8);
|
||||
tx.to(address, 3 * 1e8);
|
||||
tx.fee(10000);
|
||||
tx.change(address);
|
||||
tx.sign(testKey);
|
||||
|
||||
node.services.bitcoind.sendTransaction(tx.serialize());
|
||||
|
||||
function mineBlock(next) {
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(response);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(response);
|
||||
node.once('synced', function() {
|
||||
node.services.address.getUnspentOutputs(address, false, function(err, results) {
|
||||
/* jshint maxstatements: 50 */
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(5);
|
||||
|
||||
async.series([
|
||||
function(next) {
|
||||
var tx2 = new Transaction();
|
||||
tx2.from(results[0]);
|
||||
tx2.to(address2, results[0].satoshis - 10000);
|
||||
tx2.change(address);
|
||||
tx2.sign(testKey);
|
||||
node.services.bitcoind.sendTransaction(tx2.serialize());
|
||||
mineBlock(next);
|
||||
}, function(next) {
|
||||
var tx3 = new Transaction();
|
||||
tx3.from(results[1]);
|
||||
tx3.to(address3, results[1].satoshis - 10000);
|
||||
tx3.change(address);
|
||||
tx3.sign(testKey);
|
||||
node.services.bitcoind.sendTransaction(tx3.serialize());
|
||||
mineBlock(next);
|
||||
}, function(next) {
|
||||
var tx4 = new Transaction();
|
||||
tx4.from(results[2]);
|
||||
tx4.to(address4, results[2].satoshis - 10000);
|
||||
tx4.change(address);
|
||||
tx4.sign(testKey);
|
||||
node.services.bitcoind.sendTransaction(tx4.serialize());
|
||||
mineBlock(next);
|
||||
}, function(next) {
|
||||
var tx5 = new Transaction();
|
||||
tx5.from(results[3]);
|
||||
tx5.from(results[4]);
|
||||
tx5.to(address5, results[3].satoshis - 10000);
|
||||
tx5.to(address6, results[4].satoshis - 10000);
|
||||
tx5.change(address);
|
||||
tx5.sign(testKey);
|
||||
node.services.bitcoind.sendTransaction(tx5.serialize());
|
||||
mineBlock(next);
|
||||
}
|
||||
], function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
node.once('synced', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('five addresses', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(5);
|
||||
history[0].height.should.equal(157);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].height.should.equal(157);
|
||||
history[2].height.should.equal(156);
|
||||
history[2].address.should.equal(address4);
|
||||
history[3].height.should.equal(155);
|
||||
history[3].address.should.equal(address3);
|
||||
history[4].height.should.equal(154);
|
||||
history[4].address.should.equal(address2);
|
||||
history[4].satoshis.should.equal(99990000);
|
||||
history[4].confirmations.should.equal(4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('five addresses (limited by height)', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {
|
||||
start: 157,
|
||||
end: 156
|
||||
};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(3);
|
||||
history[0].height.should.equal(157);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].height.should.equal(157);
|
||||
history[2].height.should.equal(156);
|
||||
history[2].address.should.equal(address4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('five addresses (paginated by index)', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 3
|
||||
};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(3);
|
||||
history[0].height.should.equal(157);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].height.should.equal(157);
|
||||
history[2].height.should.equal(156);
|
||||
history[2].address.should.equal(address4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('one address with sending and receiving', function(done) {
|
||||
var addresses = [
|
||||
address
|
||||
];
|
||||
var options = {};
|
||||
node.services.address.getAddressHistory(addresses, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(6);
|
||||
history[0].height.should.equal(157);
|
||||
history[0].inputIndexes.should.deep.equal([0, 1]);
|
||||
history[0].outputIndexes.should.deep.equal([2]);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].height.should.equal(156);
|
||||
history[2].height.should.equal(155);
|
||||
history[3].height.should.equal(154);
|
||||
history[4].height.should.equal(153);
|
||||
history[4].satoshis.should.equal(-10000);
|
||||
history[4].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
|
||||
history[4].inputIndexes.should.deep.equal([0]);
|
||||
history[5].height.should.equal(150);
|
||||
history[5].satoshis.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination', function() {
|
||||
it('from 0 to 1', function(done) {
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 1
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(157);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 1 to 2', function(done) {
|
||||
var options = {
|
||||
from: 1,
|
||||
to: 2
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(156);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 2 to 3', function(done) {
|
||||
var options = {
|
||||
from: 2,
|
||||
to: 3
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(155);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 3 to 4', function(done) {
|
||||
var options = {
|
||||
from: 3,
|
||||
to: 4
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(154);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 4 to 5', function(done) {
|
||||
var options = {
|
||||
from: 4,
|
||||
to: 5
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(153);
|
||||
history[0].satoshis.should.equal(-10000);
|
||||
history[0].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
|
||||
history[0].inputIndexes.should.deep.equal([0]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 5 to 6', function(done) {
|
||||
var options = {
|
||||
from: 5,
|
||||
to: 6
|
||||
};
|
||||
node.services.address.getAddressHistory(address, options, function(err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.length.should.equal(1);
|
||||
history[0].height.should.equal(150);
|
||||
history[0].satoshis.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
188
lib/services/address/history.js
Normal file
188
lib/services/address/history.js
Normal file
@ -0,0 +1,188 @@
|
||||
'use strict';
|
||||
|
||||
var bitcore = require('bitcore');
|
||||
var async = require('async');
|
||||
var _ = bitcore.deps._;
|
||||
|
||||
/**
|
||||
* This represents an instance that keeps track of data over a series of
|
||||
* asynchronous I/O calls to get the transaction history for a group of
|
||||
* addresses. History can be queried by start and end block heights to limit large sets
|
||||
* of results (uses leveldb key streaming). See AddressService.prototype.getAddressHistory
|
||||
* for complete documentation about options.
|
||||
*/
|
||||
function AddressHistory(args) {
|
||||
this.node = args.node;
|
||||
this.options = args.options;
|
||||
|
||||
if(Array.isArray(args.addresses)) {
|
||||
this.addresses = args.addresses;
|
||||
} else {
|
||||
this.addresses = [args.addresses];
|
||||
}
|
||||
this.transactionInfo = [];
|
||||
this.transactions = {};
|
||||
this.sortedArray = [];
|
||||
}
|
||||
|
||||
AddressHistory.MAX_ADDRESS_QUERIES = 20;
|
||||
AddressHistory.MAX_TX_QUERIES = 10;
|
||||
|
||||
AddressHistory.prototype.get = function(callback) {
|
||||
var self = this;
|
||||
|
||||
// TODO check for mempool inputs and outputs by a group of addresses, currently
|
||||
// each address individually loops through the mempool and does not check input scripts.
|
||||
|
||||
async.eachLimit(
|
||||
self.addresses,
|
||||
AddressHistory.MAX_ADDRESS_QUERIES,
|
||||
function(address, next) {
|
||||
self.getTransactionInfo(address, next);
|
||||
},
|
||||
function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
async.eachSeries(
|
||||
self.transactionInfo,
|
||||
function(txInfo, next) {
|
||||
self.getDetailedInfo(txInfo, next);
|
||||
},
|
||||
function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
self.sortTransactionsIntoArray();
|
||||
self.paginateSortedArray();
|
||||
callback(null, self.sortedArray);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
AddressHistory.prototype.getTransactionInfo = function(address, next) {
|
||||
var self = this;
|
||||
|
||||
var args = {
|
||||
start: self.options.start,
|
||||
end: self.options.end,
|
||||
queryMempool: _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool
|
||||
};
|
||||
|
||||
var outputs;
|
||||
var inputs;
|
||||
|
||||
async.parallel([
|
||||
function(done) {
|
||||
self.node.services.address.getOutputs(address, args, function(err, result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
outputs = result;
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
self.node.services.address.getInputs(address, args, function(err, result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
inputs = result;
|
||||
done();
|
||||
});
|
||||
}
|
||||
], function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
self.transactionInfo = self.transactionInfo.concat(outputs, inputs);
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
AddressHistory.sortByHeight = function(a, b) {
|
||||
// TODO consider timestamp for mempool transactions
|
||||
return a.height < b.height;
|
||||
};
|
||||
|
||||
AddressHistory.prototype.paginateSortedArray = function() {
|
||||
if (!_.isUndefined(this.options.from) && !_.isUndefined(this.options.to)) {
|
||||
this.sortedArray = this.sortedArray.slice(this.options.from, this.options.to);
|
||||
}
|
||||
};
|
||||
|
||||
AddressHistory.prototype.getDetailedInfo = function(txInfo, next) {
|
||||
var self = this;
|
||||
var queryMempool = _.isUndefined(self.options.queryMempool) ? true : self.options.queryMempool;
|
||||
|
||||
if (self.transactions[txInfo.address] && self.transactions[txInfo.address][txInfo.txid]) {
|
||||
self.amendDetailedInfoWithSatoshis(txInfo);
|
||||
setImmediate(next);
|
||||
} else {
|
||||
self.node.services.db.getTransactionWithBlockInfo(
|
||||
txInfo.txid,
|
||||
queryMempool,
|
||||
function(err, transaction) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
transaction.populateInputs(self.node.services.db, [], function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
var confirmations = 0;
|
||||
if (transaction.__height >= 0) {
|
||||
confirmations = self.node.services.db.tip.__height - transaction.__height + 1;
|
||||
}
|
||||
|
||||
if (!self.transactions[txInfo.address]) {
|
||||
self.transactions[txInfo.address] = {};
|
||||
}
|
||||
|
||||
self.transactions[txInfo.address][txInfo.txid] = {
|
||||
address: txInfo.address,
|
||||
satoshis: 0,
|
||||
height: transaction.__height,
|
||||
confirmations: confirmations,
|
||||
timestamp: transaction.__timestamp,
|
||||
// TODO bitcore should return null instead of throwing error on coinbase
|
||||
fees: !transaction.isCoinbase() ? transaction.getFee() : null,
|
||||
outputIndexes: [],
|
||||
inputIndexes: [],
|
||||
tx: transaction
|
||||
};
|
||||
|
||||
self.amendDetailedInfoWithSatoshis(txInfo);
|
||||
next();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AddressHistory.prototype.amendDetailedInfoWithSatoshis = function(txInfo) {
|
||||
var historyItem = this.transactions[txInfo.address][txInfo.txid];
|
||||
if (txInfo.outputIndex >= 0) {
|
||||
historyItem.outputIndexes.push(txInfo.outputIndex);
|
||||
historyItem.satoshis += txInfo.satoshis;
|
||||
} else if (txInfo.inputIndex >= 0){
|
||||
historyItem.inputIndexes.push(txInfo.inputIndex);
|
||||
historyItem.satoshis -= historyItem.tx.inputs[txInfo.inputIndex].output.satoshis;
|
||||
}
|
||||
};
|
||||
|
||||
AddressHistory.prototype.sortTransactionsIntoArray = function() {
|
||||
this.sortedArray = [];
|
||||
for(var address in this.transactions) {
|
||||
for(var txid in this.transactions[address]) {
|
||||
this.sortedArray.push(this.transactions[address][txid]);
|
||||
}
|
||||
}
|
||||
this.sortedArray.sort(AddressHistory.sortByHeight);
|
||||
};
|
||||
|
||||
module.exports = AddressHistory;
|
||||
@ -1,11 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
var BaseService = require('../service');
|
||||
var BaseService = require('../../service');
|
||||
var inherits = require('util').inherits;
|
||||
var async = require('async');
|
||||
var index = require('../');
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
var levelup = require('levelup');
|
||||
var errors = index.errors;
|
||||
var bitcore = require('bitcore');
|
||||
var $ = bitcore.util.preconditions;
|
||||
@ -13,6 +12,7 @@ var _ = bitcore.deps._;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var PublicKey = bitcore.PublicKey;
|
||||
var Address = bitcore.Address;
|
||||
var AddressHistory = require('./history');
|
||||
|
||||
var AddressService = function(options) {
|
||||
BaseService.call(this, options);
|
||||
@ -152,8 +152,8 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) {
|
||||
var txmessages = {};
|
||||
|
||||
var outputLength = outputs.length;
|
||||
for (var j = 0; j < outputLength; j++) {
|
||||
var output = outputs[j];
|
||||
for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) {
|
||||
var output = outputs[outputIndex];
|
||||
|
||||
var script = output.script;
|
||||
|
||||
@ -170,16 +170,26 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputIndex = j;
|
||||
// TODO: expose block timestamp as a date object in bitcore?
|
||||
var timestamp = block.header.timestamp;
|
||||
// We need to use the height for indexes (and not the timestamp) because the
|
||||
// the timestamp has unreliable sequential ordering. The next block
|
||||
// can have a time that is previous to the previous block (however not
|
||||
// less than the mean of the 11 previous blocks) and not greater than 2
|
||||
// hours in the future.
|
||||
var height = block.__height;
|
||||
|
||||
var addressStr = address.toString();
|
||||
var scriptHex = output._scriptBuffer.toString('hex');
|
||||
|
||||
var key = [AddressService.PREFIXES.OUTPUTS, addressStr, timestamp, txid, outputIndex].join('-');
|
||||
var value = [output.satoshis, scriptHex, height].join(':');
|
||||
// To lookup outputs by address and height
|
||||
var key = [
|
||||
AddressService.PREFIXES.OUTPUTS,
|
||||
addressStr,
|
||||
height,
|
||||
txid,
|
||||
outputIndex
|
||||
].join('-');
|
||||
|
||||
var value = [output.satoshis, scriptHex].join(':');
|
||||
|
||||
operations.push({
|
||||
type: action,
|
||||
@ -213,13 +223,38 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(var k = 0; k < inputs.length; k++) {
|
||||
var input = inputs[k].toObject();
|
||||
operations.push({
|
||||
type: action,
|
||||
key: [AddressService.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'),
|
||||
value: [txid, k].join(':')
|
||||
});
|
||||
for(var inputIndex = 0; inputIndex < inputs.length; inputIndex++) {
|
||||
|
||||
var input = inputs[inputIndex];
|
||||
var inputAddress = input.script.toAddress(this.node.network);
|
||||
|
||||
if (inputAddress) {
|
||||
|
||||
var inputObject = input.toObject();
|
||||
var inputAddressStr = inputAddress.toString();
|
||||
|
||||
var height = block.__height;
|
||||
|
||||
// To be able to query inputs by address and spent height
|
||||
var inputKey = [
|
||||
AddressService.PREFIXES.SPENTS,
|
||||
inputAddressStr,
|
||||
height,
|
||||
inputObject.prevTxId,
|
||||
inputObject.outputIndex
|
||||
].join('-');
|
||||
|
||||
var inputValue = [
|
||||
txid,
|
||||
inputIndex
|
||||
].join(':');
|
||||
|
||||
operations.push({
|
||||
type: action,
|
||||
key: inputKey,
|
||||
value: inputValue
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,17 +358,120 @@ AddressService.prototype.getBalance = function(address, queryMempool, callback)
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getOutputs = function(addressStr, queryMempool, callback) {
|
||||
/**
|
||||
* @param {String} addressStr - The relevant address
|
||||
* @param {Object} options - Additional options for query the outputs
|
||||
* @param {Number} [options.start] - The relevant start block height
|
||||
* @param {Number} [options.end] - The relevant end block height
|
||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the results
|
||||
* @param {Function} callback
|
||||
*/
|
||||
AddressService.prototype.getInputs = function(addressStr, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var outputs = [];
|
||||
var key = [AddressService.PREFIXES.OUTPUTS, addressStr].join('-');
|
||||
var inputs = [];
|
||||
var stream;
|
||||
|
||||
if (options.start && options.end) {
|
||||
|
||||
// The positions will be flipped because the end position should be greater
|
||||
// than the starting position for the stream, and we'll add one to the end key
|
||||
// so that it's included in the results.
|
||||
|
||||
var endKey = [AddressService.PREFIXES.SPENTS, addressStr, options.start + 1].join('-');
|
||||
var startKey = [AddressService.PREFIXES.SPENTS, addressStr, options.end].join('-');
|
||||
|
||||
stream = this.node.services.db.store.createReadStream({
|
||||
start: startKey,
|
||||
end: endKey
|
||||
});
|
||||
} else {
|
||||
var allKey = [AddressService.PREFIXES.SPENTS, addressStr].join('-');
|
||||
stream = this.node.services.db.store.createReadStream({
|
||||
start: allKey,
|
||||
end: allKey + '~'
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('data', function(data) {
|
||||
|
||||
var key = data.key.split('-');
|
||||
var value = data.value.split(':');
|
||||
|
||||
var blockHeight = Number(key[2]);
|
||||
|
||||
var output = {
|
||||
address: addressStr,
|
||||
txid: value[0],
|
||||
inputIndex: Number(value[1]),
|
||||
height: blockHeight,
|
||||
confirmations: self.node.services.db.tip.__height - blockHeight + 1
|
||||
};
|
||||
|
||||
inputs.push(output);
|
||||
|
||||
var stream = this.node.services.db.store.createReadStream({
|
||||
start: key,
|
||||
end: key + '~'
|
||||
});
|
||||
|
||||
var error;
|
||||
|
||||
stream.on('error', function(streamError) {
|
||||
if (streamError) {
|
||||
error = streamError;
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('close', function() {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// TODO include results from mempool
|
||||
|
||||
callback(null, inputs);
|
||||
|
||||
});
|
||||
|
||||
return stream;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} addressStr - The relevant address
|
||||
* @param {Object} options - Additional options for query the outputs
|
||||
* @param {Number} [options.start] - The relevant start block height
|
||||
* @param {Number} [options.end] - The relevant end block height
|
||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the results
|
||||
* @param {Function} callback
|
||||
*/
|
||||
AddressService.prototype.getOutputs = function(addressStr, options, callback) {
|
||||
var self = this;
|
||||
$.checkArgument(_.isObject(options), 'Second argument is expected to be an options object.');
|
||||
$.checkArgument(_.isFunction(callback), 'Third argument is expected to be a callback function.');
|
||||
|
||||
var outputs = [];
|
||||
var stream;
|
||||
|
||||
if (options.start && options.end) {
|
||||
|
||||
// The positions will be flipped because the end position should be greater
|
||||
// than the starting position for the stream, and we'll add one to the end key
|
||||
// so that it's included in the results.
|
||||
var endKey = [AddressService.PREFIXES.OUTPUTS, addressStr, options.start + 1].join('-');
|
||||
var startKey = [AddressService.PREFIXES.OUTPUTS, addressStr, options.end].join('-');
|
||||
|
||||
stream = this.node.services.db.store.createReadStream({
|
||||
start: startKey,
|
||||
end: endKey
|
||||
});
|
||||
} else {
|
||||
var allKey = [AddressService.PREFIXES.OUTPUTS, addressStr].join('-');
|
||||
stream = this.node.services.db.store.createReadStream({
|
||||
start: allKey,
|
||||
end: allKey + '~'
|
||||
});
|
||||
}
|
||||
|
||||
stream.on('data', function(data) {
|
||||
|
||||
var key = data.key.split('-');
|
||||
@ -343,11 +481,10 @@ AddressService.prototype.getOutputs = function(addressStr, queryMempool, callbac
|
||||
address: addressStr,
|
||||
txid: key[3],
|
||||
outputIndex: Number(key[4]),
|
||||
timestamp: Number(key[2]),
|
||||
height: Number(key[2]),
|
||||
satoshis: Number(value[0]),
|
||||
script: value[1],
|
||||
blockHeight: Number(value[2]),
|
||||
confirmations: self.node.services.db.tip.__height - Number(value[2]) + 1
|
||||
confirmations: self.node.services.db.tip.__height - Number(key[2]) + 1
|
||||
};
|
||||
|
||||
outputs.push(output);
|
||||
@ -367,10 +504,9 @@ AddressService.prototype.getOutputs = function(addressStr, queryMempool, callbac
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(queryMempool) {
|
||||
if(options.queryMempool) {
|
||||
outputs = outputs.concat(self.node.services.bitcoind.getMempoolOutputs(addressStr));
|
||||
}
|
||||
|
||||
callback(null, outputs);
|
||||
});
|
||||
|
||||
@ -407,7 +543,7 @@ AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMe
|
||||
|
||||
var self = this;
|
||||
|
||||
this.getOutputs(address, queryMempool, function(err, outputs) {
|
||||
this.getOutputs(address, {queryMempool: queryMempool}, function(err, outputs) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
} else if(!outputs.length) {
|
||||
@ -439,148 +575,25 @@ AddressService.prototype.isSpent = function(output, queryMempool, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getSpendInfoForOutput = function(txid, outputIndex, callback) {
|
||||
var self = this;
|
||||
|
||||
var key = [AddressService.PREFIXES.SPENTS, txid, outputIndex].join('-');
|
||||
this.node.services.db.store.get(key, function(err, value) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
value = value.split(':');
|
||||
|
||||
var info = {
|
||||
txid: value[0],
|
||||
inputIndex: value[1]
|
||||
};
|
||||
|
||||
callback(null, info);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getAddressHistory = function(addresses, queryMempool, callback) {
|
||||
var self = this;
|
||||
|
||||
if(!Array.isArray(addresses)) {
|
||||
addresses = [addresses];
|
||||
}
|
||||
|
||||
var history = [];
|
||||
|
||||
async.eachSeries(addresses, function(address, next) {
|
||||
self.getAddressHistoryForAddress(address, queryMempool, function(err, h) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
history = history.concat(h);
|
||||
next();
|
||||
});
|
||||
}, function(err) {
|
||||
callback(err, history);
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.getAddressHistoryForAddress = function(address, queryMempool, callback) {
|
||||
var self = this;
|
||||
|
||||
var txinfos = {};
|
||||
|
||||
function getTransactionInfo(txid, callback) {
|
||||
if(txinfos[txid]) {
|
||||
return callback(null, txinfos[txid]);
|
||||
}
|
||||
|
||||
self.node.services.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
transaction.populateInputs(self.node.services.db, [], function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var confirmations = 0;
|
||||
if(transaction.__height >= 0) {
|
||||
confirmations = self.node.services.db.tip.__height - transaction.__height;
|
||||
confirmations = self.node.services.db.tip.__height - transaction.__height + 1;
|
||||
}
|
||||
|
||||
txinfos[transaction.hash] = {
|
||||
address: address,
|
||||
satoshis: 0,
|
||||
height: transaction.__height,
|
||||
confirmations: confirmations,
|
||||
timestamp: transaction.__timestamp,
|
||||
// TODO bitcore should return null instead of throwing error on coinbase
|
||||
fees: !transaction.isCoinbase() ? transaction.getFee() : null,
|
||||
outputIndexes: [],
|
||||
inputIndexes: [],
|
||||
tx: transaction
|
||||
};
|
||||
|
||||
callback(null, txinfos[transaction.hash]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.getOutputs(address, queryMempool, function(err, outputs) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
async.eachSeries(
|
||||
outputs,
|
||||
function(output, next) {
|
||||
getTransactionInfo(output.txid, function(err, txinfo) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
txinfo.outputIndexes.push(output.outputIndex);
|
||||
txinfo.satoshis += output.satoshis;
|
||||
|
||||
self.getSpendInfoForOutput(output.txid, output.outputIndex, function(err, spendInfo) {
|
||||
if(err instanceof levelup.errors.NotFoundError) {
|
||||
return next();
|
||||
} else if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
getTransactionInfo(spendInfo.txid, function(err, txinfo) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
txinfo.inputIndexes.push(spendInfo.inputIndex);
|
||||
txinfo.satoshis -= txinfo.tx.inputs[spendInfo.inputIndex].output.satoshis;
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// convert to array
|
||||
var history = [];
|
||||
for(var txid in txinfos) {
|
||||
history.push(txinfos[txid]);
|
||||
}
|
||||
|
||||
// sort by height
|
||||
history.sort(function(a, b) {
|
||||
return a.height > b.height;
|
||||
});
|
||||
|
||||
callback(null, history);
|
||||
}
|
||||
);
|
||||
/**
|
||||
* This will give the history for many addresses limited by a range of dates (to limit
|
||||
* the database lookup times) and/or paginated to limit the results length.
|
||||
* @param {Array} addresses - An array of addresses
|
||||
* @param {Object} options - The options to limit the query
|
||||
* @param {Number} [options.from] - The pagination "from" index
|
||||
* @param {Number} [options.to] - The pagination "to" index
|
||||
* @param {Number} [options.start] - The beginning block height
|
||||
* @param {Number} [options.end] - The ending block height
|
||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the query
|
||||
* @param {Function} callback
|
||||
*/
|
||||
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
|
||||
var history = new AddressHistory({
|
||||
node: this.node,
|
||||
options: options,
|
||||
addresses: addresses
|
||||
});
|
||||
history.get(callback);
|
||||
};
|
||||
|
||||
module.exports = AddressService;
|
||||
574
test/services/address/history.unit.js
Normal file
574
test/services/address/history.unit.js
Normal file
@ -0,0 +1,574 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var Transaction = require('../../../lib/transaction');
|
||||
var AddressHistory = require('../../../lib/services/address/history');
|
||||
|
||||
describe('Address Service History', function() {
|
||||
|
||||
var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX';
|
||||
|
||||
describe('@constructor', function() {
|
||||
it('will construct a new instance', function() {
|
||||
var node = {};
|
||||
var options = {};
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: node,
|
||||
options: options,
|
||||
addresses: addresses
|
||||
});
|
||||
history.should.be.instanceof(AddressHistory);
|
||||
history.node.should.equal(node);
|
||||
history.options.should.equal(options);
|
||||
history.addresses.should.equal(addresses);
|
||||
history.transactions.should.deep.equal({});
|
||||
history.transactionInfo.should.deep.equal([]);
|
||||
history.sortedArray.should.deep.equal([]);
|
||||
});
|
||||
it('will set addresses an array if only sent a string', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: address
|
||||
});
|
||||
history.addresses.should.deep.equal([address]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', function() {
|
||||
it('will complete the async each limit series', function(done) {
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: addresses
|
||||
});
|
||||
var expected = [{}];
|
||||
history.sortedArray = expected;
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo = sinon.stub().callsArg(1);
|
||||
history.paginateSortedArray = sinon.stub();
|
||||
history.getDetailedInfo = sinon.stub().callsArg(1);
|
||||
history.sortTransactionsIntoArray = sinon.stub();
|
||||
history.get(function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.getTransactionInfo.callCount.should.equal(1);
|
||||
history.getDetailedInfo.callCount.should.equal(1);
|
||||
history.sortTransactionsIntoArray.callCount.should.equal(1);
|
||||
history.paginateSortedArray.callCount.should.equal(1);
|
||||
results.should.equal(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('handle an error from getDetailedInfo', function(done) {
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: addresses
|
||||
});
|
||||
var expected = [{}];
|
||||
history.sortedArray = expected;
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo = sinon.stub().callsArg(1);
|
||||
history.paginateSortedArray = sinon.stub();
|
||||
history.getDetailedInfo = sinon.stub().callsArgWith(1, new Error('test'));
|
||||
history.get(function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('handle an error from getTransactionInfo', function(done) {
|
||||
var addresses = [address];
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: addresses
|
||||
});
|
||||
var expected = [{}];
|
||||
history.sortedArray = expected;
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo = sinon.stub().callsArgWith(1, new Error('test'));
|
||||
history.get(function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getTransactionInfo', function() {
|
||||
it('will handle an error from getInputs', function(done) {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, null, []),
|
||||
getInputs: sinon.stub().callsArgWith(2, new Error('test'))
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('will handle an error from getOutputs', function(done) {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, new Error('test')),
|
||||
getInputs: sinon.stub().callsArgWith(2, null, [])
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
err.message.should.equal('test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('will call getOutputs and getInputs with the correct options', function() {
|
||||
var startTimestamp = 1438289011844;
|
||||
var endTimestamp = 1438289012412;
|
||||
var expectedArgs = {
|
||||
start: new Date(startTimestamp * 1000),
|
||||
end: new Date(endTimestamp * 1000),
|
||||
queryMempool: true
|
||||
};
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, null, []),
|
||||
getInputs: sinon.stub().callsArgWith(2, null, [])
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
start: new Date(startTimestamp * 1000),
|
||||
end: new Date(endTimestamp * 1000),
|
||||
queryMempool: true
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.node.services.address.getOutputs.args[0][1].should.deep.equal(expectedArgs);
|
||||
history.node.services.address.getInputs.args[0][1].should.deep.equal(expectedArgs);
|
||||
});
|
||||
});
|
||||
it('will handle empty results from getOutputs and getInputs', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, null, []),
|
||||
getInputs: sinon.stub().callsArgWith(2, null, [])
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.transactionInfo.length.should.equal(1);
|
||||
history.node.services.address.getOutputs.args[0][0].should.equal(address);
|
||||
});
|
||||
});
|
||||
it('will concatenate outputs and inputs', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
address: {
|
||||
getOutputs: sinon.stub().callsArgWith(2, null, [{}]),
|
||||
getInputs: sinon.stub().callsArgWith(2, null, [{}])
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.transactionInfo = [{}];
|
||||
history.getTransactionInfo(address, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.transactionInfo.length.should.equal(3);
|
||||
history.node.services.address.getOutputs.args[0][0].should.equal(address);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('@sortByHeight', function() {
|
||||
it('will sort latest to oldest using height', function() {
|
||||
var transactionInfo = [
|
||||
{
|
||||
height: 12
|
||||
},
|
||||
{
|
||||
height: 14,
|
||||
},
|
||||
{
|
||||
height: 13
|
||||
}
|
||||
];
|
||||
transactionInfo.sort(AddressHistory.sortByHeight);
|
||||
transactionInfo[0].height.should.equal(14);
|
||||
transactionInfo[1].height.should.equal(13);
|
||||
transactionInfo[2].height.should.equal(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#paginateSortedArray', function() {
|
||||
it('from 0 to 2', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 0,
|
||||
to: 2
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.sortedArray = [
|
||||
{
|
||||
height: 14
|
||||
},
|
||||
{
|
||||
height: 13,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.paginateSortedArray();
|
||||
history.sortedArray.length.should.equal(2);
|
||||
history.sortedArray[0].height.should.equal(14);
|
||||
history.sortedArray[1].height.should.equal(13);
|
||||
});
|
||||
it('from 0 to 4 (exceeds length)', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 0,
|
||||
to: 4
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.sortedArray = [
|
||||
{
|
||||
height: 14
|
||||
},
|
||||
{
|
||||
height: 13,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.paginateSortedArray();
|
||||
history.sortedArray.length.should.equal(3);
|
||||
history.sortedArray[0].height.should.equal(14);
|
||||
history.sortedArray[1].height.should.equal(13);
|
||||
history.sortedArray[2].height.should.equal(12);
|
||||
});
|
||||
it('from 0 to 1', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 0,
|
||||
to: 1
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.sortedArray = [
|
||||
{
|
||||
height: 14
|
||||
},
|
||||
{
|
||||
height: 13,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.paginateSortedArray();
|
||||
history.sortedArray.length.should.equal(1);
|
||||
history.sortedArray[0].height.should.equal(14);
|
||||
});
|
||||
it('from 2 to 3', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 2,
|
||||
to: 3
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.sortedArray = [
|
||||
{
|
||||
height: 14
|
||||
},
|
||||
{
|
||||
height: 13,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.paginateSortedArray();
|
||||
history.sortedArray.length.should.equal(1);
|
||||
history.sortedArray[0].height.should.equal(12);
|
||||
});
|
||||
it('from 10 to 20 (out of range)', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 10,
|
||||
to: 20
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.sortedArray = [
|
||||
{
|
||||
height: 14
|
||||
},
|
||||
{
|
||||
height: 13,
|
||||
},
|
||||
{
|
||||
height: 12
|
||||
}
|
||||
];
|
||||
history.paginateSortedArray();
|
||||
history.sortedArray.length.should.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDetailedInfo', function() {
|
||||
it('will add additional information to existing this.transactions', function() {
|
||||
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
getTransactionWithBlockInfo: sinon.stub()
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.transactions[txid] = {};
|
||||
history.amendDetailedInfoWithSatoshis = sinon.stub();
|
||||
history.getDetailedInfo(txid, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
history.amendDetailedInfoWithSatoshis.callCount.should.equal(1);
|
||||
history.node.services.db.getTransactionsWithBlockInfo.callCount.should.equal(0);
|
||||
});
|
||||
});
|
||||
it('will handle error from getTransactionFromBlock', function() {
|
||||
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, new Error('test')),
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getDetailedInfo(txid, function(err) {
|
||||
err.message.should.equal('test');
|
||||
});
|
||||
});
|
||||
it('will handle error from populateInputs', function() {
|
||||
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, {
|
||||
populateInputs: sinon.stub().callsArgWith(2, new Error('test'))
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.getDetailedInfo(txid, function(err) {
|
||||
err.message.should.equal('test');
|
||||
});
|
||||
});
|
||||
it('will set this.transactions with correct information', function() {
|
||||
// block #314159
|
||||
// txid 30169e8bf78bc27c4014a7aba3862c60e2e3cce19e52f1909c8255e4b7b3174e
|
||||
// outputIndex 1
|
||||
var txAddress = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo';
|
||||
var txString = '0100000001a08ee59fcd5d86fa170abb6d925d62d5c5c476359681b70877c04f270c4ef246000000008a47304402203fb9b476bb0c37c9b9ed5784ebd67ae589492be11d4ae1612be29887e3e4ce750220741ef83781d1b3a5df8c66fa1957ad0398c733005310d7d9b1d8c2310ef4f74c0141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02b0a75fac000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac20badc02000000001976a914809dc14496f99b6deb722cf46d89d22f4beb8efd88ac00000000';
|
||||
var previousTxString = '010000000155532fad2869bb951b0bd646a546887f6ee668c4c0ee13bf3f1c4bce6d6e3ed9000000008c4930460221008540795f4ef79b1d2549c400c61155ca5abbf3089c84ad280e1ba6db2a31abce022100d7d162175483d51174d40bba722e721542c924202a0c2970b07e680b51f3a0670141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02f0af3caf000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac80969800000000001976a91421277e65777760d1f3c7c982ba14ed8f934f005888ac00000000';
|
||||
var transaction = new Transaction();
|
||||
var previousTransaction = new Transaction();
|
||||
previousTransaction.fromString(previousTxString);
|
||||
var previousTransactionTxid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
transaction.fromString(txString);
|
||||
var txid = transaction.hash;
|
||||
transaction.__blockHash = '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45';
|
||||
transaction.__height = 314159;
|
||||
transaction.__timestamp = 1407292005;
|
||||
var history = new AddressHistory({
|
||||
node: {
|
||||
services: {
|
||||
db: {
|
||||
tip: {
|
||||
__height: 314159
|
||||
},
|
||||
getTransactionWithBlockInfo: sinon.stub().callsArgWith(2, null, transaction),
|
||||
getTransaction: function(prevTxid, queryMempool, callback) {
|
||||
prevTxid.should.equal(previousTransactionTxid);
|
||||
setImmediate(function() {
|
||||
callback(null, previousTransaction);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
var transactionInfo = {
|
||||
txid: txid,
|
||||
timestamp: 1407292005,
|
||||
outputIndex: 1,
|
||||
satoshis: 48020000,
|
||||
address: txAddress
|
||||
};
|
||||
history.getDetailedInfo(transactionInfo, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var info = history.transactions[txAddress][txid];
|
||||
info.address.should.equal(txAddress);
|
||||
info.satoshis.should.equal(48020000);
|
||||
info.height.should.equal(314159);
|
||||
info.confirmations.should.equal(1);
|
||||
info.timestamp.should.equal(1407292005);
|
||||
info.fees.should.equal(20000);
|
||||
info.outputIndexes.should.deep.equal([1]);
|
||||
info.inputIndexes.should.deep.equal([]);
|
||||
info.tx.should.equal(transaction);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#amendDetailedInfoWithSatoshis', function() {
|
||||
it('will amend info with inputIndex and subtract satoshis', function() {
|
||||
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.transactions[address] = {};
|
||||
history.transactions[address][txid] = {
|
||||
inputIndexes: [],
|
||||
satoshis: 10,
|
||||
tx: {
|
||||
inputs: [
|
||||
{
|
||||
output: {
|
||||
satoshis: 3000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
history.amendDetailedInfoWithSatoshis({
|
||||
address: address,
|
||||
txid: txid,
|
||||
inputIndex: 0
|
||||
});
|
||||
history.transactions[address][txid].inputIndexes.should.deep.equal([0]);
|
||||
history.transactions[address][txid].satoshis.should.equal(-2990);
|
||||
});
|
||||
it('will amend info with outputIndex and add satoshis', function() {
|
||||
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {},
|
||||
addresses: []
|
||||
});
|
||||
history.transactions[address] = {};
|
||||
history.transactions[address][txid] = {
|
||||
outputIndexes: [],
|
||||
satoshis: 10
|
||||
};
|
||||
history.amendDetailedInfoWithSatoshis({
|
||||
address: address,
|
||||
txid: txid,
|
||||
outputIndex: 10,
|
||||
satoshis: 2000
|
||||
});
|
||||
history.transactions[address][txid].outputIndexes.should.deep.equal([10]);
|
||||
history.transactions[address][txid].satoshis.should.equal(2010);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sortTransactionIntoArray', function() {
|
||||
it('will convert this.transactions into an array and sort by height', function() {
|
||||
var history = new AddressHistory({
|
||||
node: {},
|
||||
options: {
|
||||
from: 10,
|
||||
to: 20
|
||||
},
|
||||
addresses: []
|
||||
});
|
||||
history.transactions = {
|
||||
address1: {
|
||||
txid1: {
|
||||
height: 12
|
||||
},
|
||||
txid2: {
|
||||
height: 14,
|
||||
},
|
||||
txid3: {
|
||||
height: 13
|
||||
}
|
||||
},
|
||||
address2: {
|
||||
txid4: {
|
||||
height: 15
|
||||
}
|
||||
}
|
||||
};
|
||||
history.sortTransactionsIntoArray();
|
||||
history.sortedArray.length.should.equal(4);
|
||||
history.sortedArray[0].height.should.equal(15);
|
||||
history.sortedArray[1].height.should.equal(14);
|
||||
history.sortedArray[2].height.should.equal(13);
|
||||
history.sortedArray[3].height.should.equal(12);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2,14 +2,14 @@
|
||||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var bitcorenode = require('../../');
|
||||
var proxyquire = require('proxyquire');
|
||||
var bitcorenode = require('../../../');
|
||||
var AddressService = bitcorenode.services.Address;
|
||||
var blockData = require('../data/livenet-345003.json');
|
||||
var blockData = require('../../data/livenet-345003.json');
|
||||
var bitcore = require('bitcore');
|
||||
var Networks = bitcore.Networks;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var errors = bitcorenode.errors;
|
||||
var levelup = require('levelup');
|
||||
|
||||
var mockdb = {
|
||||
};
|
||||
@ -110,7 +110,7 @@ describe('Address Service', function() {
|
||||
{
|
||||
key: {
|
||||
address: '1F1MAvhTKg2VG29w8cXsiSN2PJ8gSsrJw',
|
||||
timestamp: 1424836934,
|
||||
height: 345003,
|
||||
txid: 'fdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e16923',
|
||||
outputIndex: 0
|
||||
},
|
||||
@ -122,19 +122,20 @@ describe('Address Service', function() {
|
||||
},
|
||||
{
|
||||
key: {
|
||||
address: '1Q8ec8kG7c7HqgK7uSzQyWsX9tzepRcKEL',
|
||||
height: 345003,
|
||||
prevTxId: '3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a9',
|
||||
prevOutputIndex: 32
|
||||
},
|
||||
value: {
|
||||
txid: '5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca',
|
||||
inputIndex: 0,
|
||||
timestamp: 1424836934
|
||||
inputIndex: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
key: {
|
||||
address: '1Ep5LA4T6Y7zaBPiwruUJurjGFvCJHzJhm',
|
||||
timestamp: 1424836934,
|
||||
height: 345003,
|
||||
txid: 'e66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d',
|
||||
outputIndex: 1
|
||||
},
|
||||
@ -170,17 +171,17 @@ describe('Address Service', function() {
|
||||
should.not.exist(err);
|
||||
operations.length.should.equal(81);
|
||||
operations[0].type.should.equal('put');
|
||||
var expected0 = ['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-');
|
||||
var expected0 = ['outs', key0.address, key0.height, key0.txid, key0.outputIndex].join('-');
|
||||
operations[0].key.should.equal(expected0);
|
||||
operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':'));
|
||||
operations[0].value.should.equal([value0.satoshis, value0.script].join(':'));
|
||||
operations[3].type.should.equal('put');
|
||||
var expected3 = ['sp', key3.prevTxId, key3.prevOutputIndex].join('-');
|
||||
var expected3 = ['sp', key3.address, key3.height, key3.prevTxId, key3.prevOutputIndex].join('-');
|
||||
operations[3].key.should.equal(expected3);
|
||||
operations[3].value.should.equal([value3.txid, value3.inputIndex].join(':'));
|
||||
operations[64].type.should.equal('put');
|
||||
var expected64 = ['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-');
|
||||
var expected64 = ['outs', key64.address, key64.height, key64.txid, key64.outputIndex].join('-');
|
||||
operations[64].key.should.equal(expected64);
|
||||
operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':'));
|
||||
operations[64].value.should.equal([value64.satoshis, value64.script].join(':'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -196,14 +197,14 @@ describe('Address Service', function() {
|
||||
should.not.exist(err);
|
||||
operations.length.should.equal(81);
|
||||
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(':'));
|
||||
operations[0].key.should.equal(['outs', key0.address, key0.height, key0.txid, key0.outputIndex].join('-'));
|
||||
operations[0].value.should.equal([value0.satoshis, value0.script].join(':'));
|
||||
operations[3].type.should.equal('del');
|
||||
operations[3].key.should.equal(['sp', key3.prevTxId, key3.prevOutputIndex].join('-'));
|
||||
operations[3].key.should.equal(['sp', key3.address, key3.height, key3.prevTxId, key3.prevOutputIndex].join('-'));
|
||||
operations[3].value.should.equal([value3.txid, value3.inputIndex].join(':'));
|
||||
operations[64].type.should.equal('del');
|
||||
operations[64].key.should.equal(['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-'));
|
||||
operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':'));
|
||||
operations[64].key.should.equal(['outs', key64.address, key64.height, key64.txid, key64.outputIndex].join('-'));
|
||||
operations[64].value.should.equal([value64.satoshis, value64.script].join(':'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -421,6 +422,116 @@ describe('Address Service', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('#getInputs', function() {
|
||||
var am;
|
||||
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
||||
var db = {
|
||||
tip: {
|
||||
__height: 1
|
||||
}
|
||||
};
|
||||
var testnode = {
|
||||
services: {
|
||||
db: db,
|
||||
bitcoind: {
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
before(function() {
|
||||
am = new AddressService({node: testnode});
|
||||
});
|
||||
|
||||
it('will get inputs for an address and timestamp', function(done) {
|
||||
var testStream = new EventEmitter();
|
||||
var args = {
|
||||
start: 15,
|
||||
end: 12,
|
||||
queryMempool: true
|
||||
};
|
||||
var createReadStreamCallCount = 0;
|
||||
am.node.services.db.store = {
|
||||
createReadStream: function(ops) {
|
||||
ops.start.should.equal([AddressService.PREFIXES.SPENTS, address, 12].join('-'));
|
||||
ops.end.should.equal([AddressService.PREFIXES.SPENTS, address, 16].join('-'));
|
||||
createReadStreamCallCount++;
|
||||
return testStream;
|
||||
}
|
||||
};
|
||||
am.node.services.bitcoind = {
|
||||
getMempoolInputs: sinon.stub().returns([])
|
||||
};
|
||||
am.getInputs(address, args, function(err, inputs) {
|
||||
should.not.exist(err);
|
||||
inputs.length.should.equal(1);
|
||||
inputs[0].address.should.equal(address);
|
||||
inputs[0].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7');
|
||||
inputs[0].inputIndex.should.equal(0);
|
||||
inputs[0].height.should.equal(15);
|
||||
done();
|
||||
});
|
||||
createReadStreamCallCount.should.equal(1);
|
||||
var data = {
|
||||
key: ['sp', address, '15', '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'),
|
||||
value: ['3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '0'].join(':')
|
||||
};
|
||||
testStream.emit('data', data);
|
||||
testStream.emit('close');
|
||||
});
|
||||
it('should get inputs for address', function(done) {
|
||||
var testStream = new EventEmitter();
|
||||
var args = {
|
||||
queryMempool: true
|
||||
};
|
||||
var createReadStreamCallCount = 0;
|
||||
am.node.services.db.store = {
|
||||
createReadStream: function(ops) {
|
||||
ops.start.should.equal([AddressService.PREFIXES.SPENTS, address].join('-'));
|
||||
ops.end.should.equal([AddressService.PREFIXES.SPENTS, address].join('-') + '~');
|
||||
createReadStreamCallCount++;
|
||||
return testStream;
|
||||
}
|
||||
};
|
||||
am.node.services.bitcoind = {
|
||||
getMempoolInputs: sinon.stub().returns([])
|
||||
};
|
||||
am.getInputs(address, args, function(err, inputs) {
|
||||
should.not.exist(err);
|
||||
inputs.length.should.equal(1);
|
||||
inputs[0].address.should.equal(address);
|
||||
inputs[0].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7');
|
||||
inputs[0].inputIndex.should.equal(0);
|
||||
inputs[0].height.should.equal(15);
|
||||
done();
|
||||
});
|
||||
createReadStreamCallCount.should.equal(1);
|
||||
var data = {
|
||||
key: ['sp', address, '15', '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'),
|
||||
value: ['3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '0'].join(':')
|
||||
};
|
||||
testStream.emit('data', data);
|
||||
testStream.emit('close');
|
||||
});
|
||||
it('should give an error if the readstream has an error', function(done) {
|
||||
var testStream = new EventEmitter();
|
||||
am.node.services.db.store = {
|
||||
createReadStream: sinon.stub().returns(testStream)
|
||||
};
|
||||
|
||||
am.getOutputs(address, {}, function(err, outputs) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('readstreamerror');
|
||||
done();
|
||||
});
|
||||
|
||||
testStream.emit('error', new Error('readstreamerror'));
|
||||
setImmediate(function() {
|
||||
testStream.emit('close');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#getOutputs', function() {
|
||||
var am;
|
||||
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
||||
@ -437,11 +548,53 @@ describe('Address Service', function() {
|
||||
}
|
||||
}
|
||||
};
|
||||
var options = {
|
||||
queryMempool: true
|
||||
};
|
||||
|
||||
before(function() {
|
||||
am = new AddressService({node: testnode});
|
||||
});
|
||||
|
||||
it('will get outputs for an address and timestamp', function(done) {
|
||||
var testStream = new EventEmitter();
|
||||
var args = {
|
||||
start: 15,
|
||||
end: 12,
|
||||
queryMempool: true
|
||||
};
|
||||
var createReadStreamCallCount = 0;
|
||||
am.node.services.db.store = {
|
||||
createReadStream: function(ops) {
|
||||
ops.start.should.equal([AddressService.PREFIXES.OUTPUTS, address, 12].join('-'));
|
||||
ops.end.should.equal([AddressService.PREFIXES.OUTPUTS, address, 16].join('-'));
|
||||
createReadStreamCallCount++;
|
||||
return testStream;
|
||||
}
|
||||
};
|
||||
am.node.services.bitcoind = {
|
||||
getMempoolOutputs: sinon.stub().returns([])
|
||||
};
|
||||
am.getOutputs(address, args, function(err, outputs) {
|
||||
should.not.exist(err);
|
||||
outputs.length.should.equal(1);
|
||||
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('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac');
|
||||
outputs[0].height.should.equal(15);
|
||||
done();
|
||||
});
|
||||
createReadStreamCallCount.should.equal(1);
|
||||
var data = {
|
||||
key: ['outs', address, '15', '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'),
|
||||
value: ['4527773864', '76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'].join(':')
|
||||
};
|
||||
testStream.emit('data', data);
|
||||
testStream.emit('close');
|
||||
});
|
||||
|
||||
it('should get outputs for an address', function(done) {
|
||||
var readStream1 = new EventEmitter();
|
||||
am.node.services.db.store = {
|
||||
@ -452,7 +605,7 @@ describe('Address Service', function() {
|
||||
address: '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W',
|
||||
txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371',
|
||||
satoshis: 307627737,
|
||||
script: 'OP_DUP OP_HASH160 f6db95c81dea3d10f0ff8d890927751bf7b203c1 OP_EQUALVERIFY OP_CHECKSIG',
|
||||
script: '76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac',
|
||||
blockHeight: 352532
|
||||
}
|
||||
];
|
||||
@ -460,38 +613,36 @@ describe('Address Service', function() {
|
||||
getMempoolOutputs: sinon.stub().returns(mempoolOutputs)
|
||||
};
|
||||
|
||||
am.getOutputs(address, true, function(err, outputs) {
|
||||
am.getOutputs(address, options, 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[0].timestamp.should.equal(1424835319000);
|
||||
outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac');
|
||||
outputs[0].height.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[1].timestamp.should.equal(1424837300000);
|
||||
outputs[1].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac');
|
||||
outputs[1].height.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].script.should.equal('76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac');
|
||||
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(':')
|
||||
key: ['outs', address, 345000, '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'),
|
||||
value: ['4527773864', '76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'].join(':')
|
||||
};
|
||||
|
||||
var data2 = {
|
||||
key: ['outs', address, 1424837300000, '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '2'].join('-'),
|
||||
value: ['10000', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345004'].join(':')
|
||||
key: ['outs', address, 345004, '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '2'].join('-'),
|
||||
value: ['10000', '76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'].join(':')
|
||||
};
|
||||
|
||||
readStream1.emit('data', data1);
|
||||
@ -505,14 +656,14 @@ describe('Address Service', function() {
|
||||
createReadStream: sinon.stub().returns(readStream2)
|
||||
};
|
||||
|
||||
am.getOutputs(address, true, function(err, outputs) {
|
||||
am.getOutputs(address, options, function(err, outputs) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('readstreamerror');
|
||||
done();
|
||||
});
|
||||
|
||||
readStream2.emit('error', new Error('readstreamerror'));
|
||||
process.nextTick(function() {
|
||||
setImmediate(function() {
|
||||
readStream2.emit('close');
|
||||
});
|
||||
});
|
||||
@ -731,245 +882,20 @@ describe('Address Service', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getSpendInfoForOutput', function() {
|
||||
it('should call store.get the right values', function(done) {
|
||||
var db = {
|
||||
store: {
|
||||
get: sinon.stub().callsArgWith(1, null, 'spendtxid:1')
|
||||
}
|
||||
};
|
||||
var testnode = {
|
||||
services: {
|
||||
db: db,
|
||||
bitcoind: {
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var am = new AddressService({node: testnode});
|
||||
am.getSpendInfoForOutput('txid', 3, function(err, info) {
|
||||
should.not.exist(err);
|
||||
info.txid.should.equal('spendtxid');
|
||||
info.inputIndex.should.equal('1');
|
||||
db.store.get.args[0][0].should.equal('sp-txid-3');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAddressHistory', function() {
|
||||
var incoming = [
|
||||
{
|
||||
txid: 'tx1',
|
||||
outputIndex: 0,
|
||||
spentTx: 'tx2',
|
||||
inputIndex: 0,
|
||||
height: 1,
|
||||
timestamp: 1438289011844,
|
||||
satoshis: 5000,
|
||||
getFee: sinon.stub().throws(new Error('inputs not populated')),
|
||||
isCoinbase: sinon.stub().returns(true)
|
||||
},
|
||||
{
|
||||
txid: 'tx3',
|
||||
outputIndex: 1,
|
||||
height: 3,
|
||||
timestamp: 1438289031844,
|
||||
satoshis: 2000,
|
||||
getFee: sinon.stub().returns(1000),
|
||||
isCoinbase: sinon.stub().returns(false)
|
||||
},
|
||||
{
|
||||
txid: 'tx4',
|
||||
outputIndex: 2,
|
||||
spentTx: 'tx5',
|
||||
inputIndex: 1,
|
||||
height: 4,
|
||||
timestamp: 1438289041844,
|
||||
satoshis: 3000,
|
||||
getFee: sinon.stub().returns(1000),
|
||||
isCoinbase: sinon.stub().returns(false)
|
||||
},
|
||||
];
|
||||
|
||||
var outgoing = [
|
||||
{
|
||||
txid: 'tx2',
|
||||
height: 2,
|
||||
timestamp: 1438289021844,
|
||||
inputs: [
|
||||
{
|
||||
output: {
|
||||
satoshis: 5000
|
||||
}
|
||||
}
|
||||
],
|
||||
getFee: sinon.stub().returns(1000),
|
||||
isCoinbase: sinon.stub().returns(false)
|
||||
},
|
||||
{
|
||||
txid: 'tx5',
|
||||
height: 5,
|
||||
timestamp: 1438289051844,
|
||||
inputs: [
|
||||
{},
|
||||
{
|
||||
output: {
|
||||
satoshis: 3000
|
||||
}
|
||||
}
|
||||
],
|
||||
getFee: sinon.stub().returns(1000),
|
||||
isCoinbase: sinon.stub().returns(false)
|
||||
it('will call get on address history instance', function(done) {
|
||||
function TestAddressHistory(args) {
|
||||
args.node.should.equal(mocknode);
|
||||
args.addresses.should.deep.equal([]);
|
||||
args.options.should.deep.equal({});
|
||||
}
|
||||
];
|
||||
|
||||
var db = {
|
||||
tip: {
|
||||
__height: 1
|
||||
},
|
||||
getTransactionWithBlockInfo: function(txid, queryMempool, callback) {
|
||||
var transaction = {
|
||||
populateInputs: sinon.stub().callsArg(2)
|
||||
};
|
||||
for(var i = 0; i < incoming.length; i++) {
|
||||
if(incoming[i].txid === txid) {
|
||||
if(incoming[i].error) {
|
||||
return callback(new Error(incoming[i].error));
|
||||
}
|
||||
transaction.hash = txid;
|
||||
transaction.__height = incoming[i].height;
|
||||
transaction.__timestamp = incoming[i].timestamp;
|
||||
transaction.getFee = incoming[i].getFee;
|
||||
transaction.isCoinbase = incoming[i].isCoinbase;
|
||||
return callback(null, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
for(var i = 0; i < outgoing.length; i++) {
|
||||
if(outgoing[i].txid === txid) {
|
||||
if(outgoing[i].error) {
|
||||
return callback(new Error(outgoing[i].error));
|
||||
}
|
||||
transaction.hash = txid;
|
||||
transaction.__height = outgoing[i].height;
|
||||
transaction.__timestamp = outgoing[i].timestamp;
|
||||
transaction.inputs = outgoing[i].inputs;
|
||||
transaction.getFee = outgoing[i].getFee;
|
||||
transaction.isCoinbase = outgoing[i].isCoinbase;
|
||||
return callback(null, transaction);
|
||||
}
|
||||
}
|
||||
callback(new Error('tx ' + txid + ' not found'));
|
||||
}
|
||||
};
|
||||
var testnode = {
|
||||
services: {
|
||||
db: db,
|
||||
bitcoind: {
|
||||
on: sinon.stub()
|
||||
}
|
||||
}
|
||||
};
|
||||
var am = new AddressService({node: testnode});
|
||||
|
||||
am.getOutputs = sinon.stub().callsArgWith(2, null, incoming);
|
||||
am.getSpendInfoForOutput = function(txid, outputIndex, callback) {
|
||||
for(var i = 0; i < incoming.length; i++) {
|
||||
if(incoming[i].txid === txid && incoming[i].outputIndex === outputIndex && incoming[i].spentTx) {
|
||||
if(incoming[i].spendError) {
|
||||
return callback(new Error(incoming[i].spendError));
|
||||
}
|
||||
return callback(null, {
|
||||
txid: incoming[i].spentTx,
|
||||
inputIndex: incoming[i].inputIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
callback(new levelup.errors.NotFoundError());
|
||||
};
|
||||
|
||||
it('should give transaction history for an address', function(done) {
|
||||
am.getAddressHistory('address', true, function(err, history) {
|
||||
should.not.exist(err);
|
||||
history[0].tx.hash.should.equal('tx1');
|
||||
history[0].satoshis.should.equal(5000);
|
||||
history[0].height.should.equal(1);
|
||||
history[0].timestamp.should.equal(1438289011844);
|
||||
should.equal(history[0].fees, null);
|
||||
history[1].tx.hash.should.equal('tx2');
|
||||
history[1].satoshis.should.equal(-5000);
|
||||
history[1].height.should.equal(2);
|
||||
history[1].timestamp.should.equal(1438289021844);
|
||||
history[1].fees.should.equal(1000);
|
||||
history[2].tx.hash.should.equal('tx3');
|
||||
history[2].satoshis.should.equal(2000);
|
||||
history[2].height.should.equal(3);
|
||||
history[2].timestamp.should.equal(1438289031844);
|
||||
history[2].fees.should.equal(1000);
|
||||
history[3].tx.hash.should.equal('tx4');
|
||||
history[3].satoshis.should.equal(3000);
|
||||
history[3].height.should.equal(4);
|
||||
history[3].timestamp.should.equal(1438289041844);
|
||||
history[3].fees.should.equal(1000);
|
||||
history[4].tx.hash.should.equal('tx5');
|
||||
history[4].satoshis.should.equal(-3000);
|
||||
history[4].height.should.equal(5);
|
||||
history[4].timestamp.should.equal(1438289051844);
|
||||
history[4].fees.should.equal(1000);
|
||||
done();
|
||||
TestAddressHistory.prototype.get = sinon.stub().callsArg(0);
|
||||
var TestAddressService = proxyquire('../../../lib/services/address', {
|
||||
'./history': TestAddressHistory
|
||||
});
|
||||
});
|
||||
|
||||
it('should give an error if the second getTransactionInfo gives an error', function(done) {
|
||||
outgoing[0].error = 'txinfo2err';
|
||||
am.getAddressHistory('address', true, function(err, history) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('txinfo2err');
|
||||
outgoing[0].error = null;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should give an error if getSpendInfoForOutput gives an error', function(done) {
|
||||
incoming[0].spendError = 'spenderr';
|
||||
am.getAddressHistory('address', true, function(err, history) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('spenderr');
|
||||
incoming[0].spendError = null;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should give an error if the first getTransactionInfo gives an error', function(done) {
|
||||
incoming[1].error = 'txinfo1err';
|
||||
am.getAddressHistory('address', true, function(err, history) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('txinfo1err');
|
||||
incoming[1].error = null;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should give an error if populateInputs gives an error', function(done) {
|
||||
var populateStub = sinon.stub().callsArgWith(2, new Error('populateerr'));
|
||||
sinon.stub(db, 'getTransactionWithBlockInfo').callsArgWith(2, null, {
|
||||
populateInputs: populateStub
|
||||
});
|
||||
am.getAddressHistory('address', true, function(err, history) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('populateerr');
|
||||
db.getTransactionWithBlockInfo.restore();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should give an error if getOutputs gives an error', function(done) {
|
||||
am.getOutputs = sinon.stub().callsArgWith(2, new Error('getoutputserr'));
|
||||
am.getAddressHistory('address', true, function(err, history) {
|
||||
should.exist(err);
|
||||
err.message.should.equal('getoutputserr');
|
||||
var am = new TestAddressService({node: mocknode});
|
||||
am.getAddressHistory([], {}, function(err, history) {
|
||||
TestAddressHistory.prototype.get.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user