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 BitcoinRPC = require('bitcoind-rpc');
|
||||||
var index = require('..');
|
var index = require('..');
|
||||||
|
var Transaction = index.Transaction;
|
||||||
var BitcoreNode = index.Node;
|
var BitcoreNode = index.Node;
|
||||||
var AddressService = index.services.Address;
|
var AddressService = index.services.Address;
|
||||||
var BitcoinService = index.services.Bitcoin;
|
var BitcoinService = index.services.Bitcoin;
|
||||||
@ -34,6 +35,8 @@ var client;
|
|||||||
|
|
||||||
describe('Node Functionality', function() {
|
describe('Node Functionality', function() {
|
||||||
|
|
||||||
|
var regtest;
|
||||||
|
|
||||||
before(function(done) {
|
before(function(done) {
|
||||||
this.timeout(30000);
|
this.timeout(30000);
|
||||||
|
|
||||||
@ -51,6 +54,7 @@ describe('Node Functionality', function() {
|
|||||||
port: 18444,
|
port: 18444,
|
||||||
dnsSeeds: [ ]
|
dnsSeeds: [ ]
|
||||||
});
|
});
|
||||||
|
regtest = bitcore.Networks.get('regtest');
|
||||||
|
|
||||||
var datadir = __dirname + '/data';
|
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';
|
'use strict';
|
||||||
|
|
||||||
var BaseService = require('../service');
|
var BaseService = require('../../service');
|
||||||
var inherits = require('util').inherits;
|
var inherits = require('util').inherits;
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var index = require('../');
|
var index = require('../../');
|
||||||
var log = index.log;
|
var log = index.log;
|
||||||
var levelup = require('levelup');
|
|
||||||
var errors = index.errors;
|
var errors = index.errors;
|
||||||
var bitcore = require('bitcore');
|
var bitcore = require('bitcore');
|
||||||
var $ = bitcore.util.preconditions;
|
var $ = bitcore.util.preconditions;
|
||||||
@ -13,6 +12,7 @@ var _ = bitcore.deps._;
|
|||||||
var EventEmitter = require('events').EventEmitter;
|
var EventEmitter = require('events').EventEmitter;
|
||||||
var PublicKey = bitcore.PublicKey;
|
var PublicKey = bitcore.PublicKey;
|
||||||
var Address = bitcore.Address;
|
var Address = bitcore.Address;
|
||||||
|
var AddressHistory = require('./history');
|
||||||
|
|
||||||
var AddressService = function(options) {
|
var AddressService = function(options) {
|
||||||
BaseService.call(this, options);
|
BaseService.call(this, options);
|
||||||
@ -152,8 +152,8 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) {
|
|||||||
var txmessages = {};
|
var txmessages = {};
|
||||||
|
|
||||||
var outputLength = outputs.length;
|
var outputLength = outputs.length;
|
||||||
for (var j = 0; j < outputLength; j++) {
|
for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) {
|
||||||
var output = outputs[j];
|
var output = outputs[outputIndex];
|
||||||
|
|
||||||
var script = output.script;
|
var script = output.script;
|
||||||
|
|
||||||
@ -170,16 +170,26 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputIndex = j;
|
// We need to use the height for indexes (and not the timestamp) because the
|
||||||
// TODO: expose block timestamp as a date object in bitcore?
|
// the timestamp has unreliable sequential ordering. The next block
|
||||||
var timestamp = block.header.timestamp;
|
// 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 height = block.__height;
|
||||||
|
|
||||||
var addressStr = address.toString();
|
var addressStr = address.toString();
|
||||||
var scriptHex = output._scriptBuffer.toString('hex');
|
var scriptHex = output._scriptBuffer.toString('hex');
|
||||||
|
|
||||||
var key = [AddressService.PREFIXES.OUTPUTS, addressStr, timestamp, txid, outputIndex].join('-');
|
// To lookup outputs by address and height
|
||||||
var value = [output.satoshis, scriptHex, height].join(':');
|
var key = [
|
||||||
|
AddressService.PREFIXES.OUTPUTS,
|
||||||
|
addressStr,
|
||||||
|
height,
|
||||||
|
txid,
|
||||||
|
outputIndex
|
||||||
|
].join('-');
|
||||||
|
|
||||||
|
var value = [output.satoshis, scriptHex].join(':');
|
||||||
|
|
||||||
operations.push({
|
operations.push({
|
||||||
type: action,
|
type: action,
|
||||||
@ -213,13 +223,38 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(var k = 0; k < inputs.length; k++) {
|
for(var inputIndex = 0; inputIndex < inputs.length; inputIndex++) {
|
||||||
var input = inputs[k].toObject();
|
|
||||||
operations.push({
|
var input = inputs[inputIndex];
|
||||||
type: action,
|
var inputAddress = input.script.toAddress(this.node.network);
|
||||||
key: [AddressService.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'),
|
|
||||||
value: [txid, k].join(':')
|
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 self = this;
|
||||||
|
|
||||||
var outputs = [];
|
var inputs = [];
|
||||||
var key = [AddressService.PREFIXES.OUTPUTS, addressStr].join('-');
|
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) {
|
stream.on('data', function(data) {
|
||||||
|
|
||||||
var key = data.key.split('-');
|
var key = data.key.split('-');
|
||||||
@ -343,11 +481,10 @@ AddressService.prototype.getOutputs = function(addressStr, queryMempool, callbac
|
|||||||
address: addressStr,
|
address: addressStr,
|
||||||
txid: key[3],
|
txid: key[3],
|
||||||
outputIndex: Number(key[4]),
|
outputIndex: Number(key[4]),
|
||||||
timestamp: Number(key[2]),
|
height: Number(key[2]),
|
||||||
satoshis: Number(value[0]),
|
satoshis: Number(value[0]),
|
||||||
script: value[1],
|
script: value[1],
|
||||||
blockHeight: Number(value[2]),
|
confirmations: self.node.services.db.tip.__height - Number(key[2]) + 1
|
||||||
confirmations: self.node.services.db.tip.__height - Number(value[2]) + 1
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs.push(output);
|
outputs.push(output);
|
||||||
@ -367,10 +504,9 @@ AddressService.prototype.getOutputs = function(addressStr, queryMempool, callbac
|
|||||||
return callback(error);
|
return callback(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(queryMempool) {
|
if(options.queryMempool) {
|
||||||
outputs = outputs.concat(self.node.services.bitcoind.getMempoolOutputs(addressStr));
|
outputs = outputs.concat(self.node.services.bitcoind.getMempoolOutputs(addressStr));
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, outputs);
|
callback(null, outputs);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -407,7 +543,7 @@ AddressService.prototype.getUnspentOutputsForAddress = function(address, queryMe
|
|||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.getOutputs(address, queryMempool, function(err, outputs) {
|
this.getOutputs(address, {queryMempool: queryMempool}, function(err, outputs) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
} else if(!outputs.length) {
|
} 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;
|
* 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.
|
||||||
var key = [AddressService.PREFIXES.SPENTS, txid, outputIndex].join('-');
|
* @param {Array} addresses - An array of addresses
|
||||||
this.node.services.db.store.get(key, function(err, value) {
|
* @param {Object} options - The options to limit the query
|
||||||
if(err) {
|
* @param {Number} [options.from] - The pagination "from" index
|
||||||
return callback(err);
|
* @param {Number} [options.to] - The pagination "to" index
|
||||||
}
|
* @param {Number} [options.start] - The beginning block height
|
||||||
|
* @param {Number} [options.end] - The ending block height
|
||||||
value = value.split(':');
|
* @param {Boolean} [options.queryMempool] - Include the mempool in the query
|
||||||
|
* @param {Function} callback
|
||||||
var info = {
|
*/
|
||||||
txid: value[0],
|
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
|
||||||
inputIndex: value[1]
|
var history = new AddressHistory({
|
||||||
};
|
node: this.node,
|
||||||
|
options: options,
|
||||||
callback(null, info);
|
addresses: addresses
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
history.get(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = AddressService;
|
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 should = require('chai').should();
|
||||||
var sinon = require('sinon');
|
var sinon = require('sinon');
|
||||||
var bitcorenode = require('../../');
|
var proxyquire = require('proxyquire');
|
||||||
|
var bitcorenode = require('../../../');
|
||||||
var AddressService = bitcorenode.services.Address;
|
var AddressService = bitcorenode.services.Address;
|
||||||
var blockData = require('../data/livenet-345003.json');
|
var blockData = require('../../data/livenet-345003.json');
|
||||||
var bitcore = require('bitcore');
|
var bitcore = require('bitcore');
|
||||||
var Networks = bitcore.Networks;
|
var Networks = bitcore.Networks;
|
||||||
var EventEmitter = require('events').EventEmitter;
|
var EventEmitter = require('events').EventEmitter;
|
||||||
var errors = bitcorenode.errors;
|
var errors = bitcorenode.errors;
|
||||||
var levelup = require('levelup');
|
|
||||||
|
|
||||||
var mockdb = {
|
var mockdb = {
|
||||||
};
|
};
|
||||||
@ -110,7 +110,7 @@ describe('Address Service', function() {
|
|||||||
{
|
{
|
||||||
key: {
|
key: {
|
||||||
address: '1F1MAvhTKg2VG29w8cXsiSN2PJ8gSsrJw',
|
address: '1F1MAvhTKg2VG29w8cXsiSN2PJ8gSsrJw',
|
||||||
timestamp: 1424836934,
|
height: 345003,
|
||||||
txid: 'fdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e16923',
|
txid: 'fdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e16923',
|
||||||
outputIndex: 0
|
outputIndex: 0
|
||||||
},
|
},
|
||||||
@ -122,19 +122,20 @@ describe('Address Service', function() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: {
|
||||||
|
address: '1Q8ec8kG7c7HqgK7uSzQyWsX9tzepRcKEL',
|
||||||
|
height: 345003,
|
||||||
prevTxId: '3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a9',
|
prevTxId: '3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a9',
|
||||||
prevOutputIndex: 32
|
prevOutputIndex: 32
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
txid: '5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca',
|
txid: '5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca',
|
||||||
inputIndex: 0,
|
inputIndex: 0
|
||||||
timestamp: 1424836934
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: {
|
||||||
address: '1Ep5LA4T6Y7zaBPiwruUJurjGFvCJHzJhm',
|
address: '1Ep5LA4T6Y7zaBPiwruUJurjGFvCJHzJhm',
|
||||||
timestamp: 1424836934,
|
height: 345003,
|
||||||
txid: 'e66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d',
|
txid: 'e66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d',
|
||||||
outputIndex: 1
|
outputIndex: 1
|
||||||
},
|
},
|
||||||
@ -170,17 +171,17 @@ describe('Address Service', function() {
|
|||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
operations.length.should.equal(81);
|
operations.length.should.equal(81);
|
||||||
operations[0].type.should.equal('put');
|
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].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');
|
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].key.should.equal(expected3);
|
||||||
operations[3].value.should.equal([value3.txid, value3.inputIndex].join(':'));
|
operations[3].value.should.equal([value3.txid, value3.inputIndex].join(':'));
|
||||||
operations[64].type.should.equal('put');
|
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].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();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -196,14 +197,14 @@ describe('Address Service', function() {
|
|||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
operations.length.should.equal(81);
|
operations.length.should.equal(81);
|
||||||
operations[0].type.should.equal('del');
|
operations[0].type.should.equal('del');
|
||||||
operations[0].key.should.equal(['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].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, value0.blockHeight].join(':'));
|
operations[0].value.should.equal([value0.satoshis, value0.script].join(':'));
|
||||||
operations[3].type.should.equal('del');
|
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[3].value.should.equal([value3.txid, value3.inputIndex].join(':'));
|
||||||
operations[64].type.should.equal('del');
|
operations[64].type.should.equal('del');
|
||||||
operations[64].key.should.equal(['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].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, value64.blockHeight].join(':'));
|
operations[64].value.should.equal([value64.satoshis, value64.script].join(':'));
|
||||||
done();
|
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() {
|
describe('#getOutputs', function() {
|
||||||
var am;
|
var am;
|
||||||
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
|
||||||
@ -437,11 +548,53 @@ describe('Address Service', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var options = {
|
||||||
|
queryMempool: true
|
||||||
|
};
|
||||||
|
|
||||||
before(function() {
|
before(function() {
|
||||||
am = new AddressService({node: testnode});
|
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) {
|
it('should get outputs for an address', function(done) {
|
||||||
var readStream1 = new EventEmitter();
|
var readStream1 = new EventEmitter();
|
||||||
am.node.services.db.store = {
|
am.node.services.db.store = {
|
||||||
@ -452,7 +605,7 @@ describe('Address Service', function() {
|
|||||||
address: '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W',
|
address: '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W',
|
||||||
txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371',
|
txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371',
|
||||||
satoshis: 307627737,
|
satoshis: 307627737,
|
||||||
script: 'OP_DUP OP_HASH160 f6db95c81dea3d10f0ff8d890927751bf7b203c1 OP_EQUALVERIFY OP_CHECKSIG',
|
script: '76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac',
|
||||||
blockHeight: 352532
|
blockHeight: 352532
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -460,38 +613,36 @@ describe('Address Service', function() {
|
|||||||
getMempoolOutputs: sinon.stub().returns(mempoolOutputs)
|
getMempoolOutputs: sinon.stub().returns(mempoolOutputs)
|
||||||
};
|
};
|
||||||
|
|
||||||
am.getOutputs(address, true, function(err, outputs) {
|
am.getOutputs(address, options, function(err, outputs) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
outputs.length.should.equal(3);
|
outputs.length.should.equal(3);
|
||||||
outputs[0].address.should.equal(address);
|
outputs[0].address.should.equal(address);
|
||||||
outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87');
|
outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87');
|
||||||
outputs[0].outputIndex.should.equal(1);
|
outputs[0].outputIndex.should.equal(1);
|
||||||
outputs[0].satoshis.should.equal(4527773864);
|
outputs[0].satoshis.should.equal(4527773864);
|
||||||
outputs[0].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG');
|
outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac');
|
||||||
outputs[0].blockHeight.should.equal(345000);
|
outputs[0].height.should.equal(345000);
|
||||||
outputs[0].timestamp.should.equal(1424835319000);
|
|
||||||
outputs[1].address.should.equal(address);
|
outputs[1].address.should.equal(address);
|
||||||
outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7');
|
outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7');
|
||||||
outputs[1].outputIndex.should.equal(2);
|
outputs[1].outputIndex.should.equal(2);
|
||||||
outputs[1].satoshis.should.equal(10000);
|
outputs[1].satoshis.should.equal(10000);
|
||||||
outputs[1].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG');
|
outputs[1].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac');
|
||||||
outputs[1].blockHeight.should.equal(345004);
|
outputs[1].height.should.equal(345004);
|
||||||
outputs[1].timestamp.should.equal(1424837300000);
|
|
||||||
outputs[2].address.should.equal(address);
|
outputs[2].address.should.equal(address);
|
||||||
outputs[2].txid.should.equal('aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371');
|
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);
|
outputs[2].blockHeight.should.equal(352532);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
var data1 = {
|
var data1 = {
|
||||||
key: ['outs', address, 1424835319000, '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'),
|
key: ['outs', address, 345000, '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'),
|
||||||
value: ['4527773864', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345000'].join(':')
|
value: ['4527773864', '76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'].join(':')
|
||||||
};
|
};
|
||||||
|
|
||||||
var data2 = {
|
var data2 = {
|
||||||
key: ['outs', address, 1424837300000, '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '2'].join('-'),
|
key: ['outs', address, 345004, '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '2'].join('-'),
|
||||||
value: ['10000', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345004'].join(':')
|
value: ['10000', '76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'].join(':')
|
||||||
};
|
};
|
||||||
|
|
||||||
readStream1.emit('data', data1);
|
readStream1.emit('data', data1);
|
||||||
@ -505,14 +656,14 @@ describe('Address Service', function() {
|
|||||||
createReadStream: sinon.stub().returns(readStream2)
|
createReadStream: sinon.stub().returns(readStream2)
|
||||||
};
|
};
|
||||||
|
|
||||||
am.getOutputs(address, true, function(err, outputs) {
|
am.getOutputs(address, options, function(err, outputs) {
|
||||||
should.exist(err);
|
should.exist(err);
|
||||||
err.message.should.equal('readstreamerror');
|
err.message.should.equal('readstreamerror');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
readStream2.emit('error', new Error('readstreamerror'));
|
readStream2.emit('error', new Error('readstreamerror'));
|
||||||
process.nextTick(function() {
|
setImmediate(function() {
|
||||||
readStream2.emit('close');
|
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() {
|
describe('#getAddressHistory', function() {
|
||||||
var incoming = [
|
it('will call get on address history instance', function(done) {
|
||||||
{
|
function TestAddressHistory(args) {
|
||||||
txid: 'tx1',
|
args.node.should.equal(mocknode);
|
||||||
outputIndex: 0,
|
args.addresses.should.deep.equal([]);
|
||||||
spentTx: 'tx2',
|
args.options.should.deep.equal({});
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
];
|
TestAddressHistory.prototype.get = sinon.stub().callsArg(0);
|
||||||
|
var TestAddressService = proxyquire('../../../lib/services/address', {
|
||||||
var db = {
|
'./history': TestAddressHistory
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
var am = new TestAddressService({node: mocknode});
|
||||||
|
am.getAddressHistory([], {}, function(err, history) {
|
||||||
it('should give an error if the second getTransactionInfo gives an error', function(done) {
|
TestAddressHistory.prototype.get.callCount.should.equal(1);
|
||||||
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');
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue
Block a user