Address Service: Fixed many bugs from tests

- Refactored getAddressSummary and added several tests
- Fixed bugs revealed from the integration regtests
- Updated many unit tests
This commit is contained in:
Braydon Fuller 2016-01-11 18:45:51 -05:00
parent 188ff28ec7
commit 4fcec8755c
9 changed files with 1525 additions and 789 deletions

View File

@ -28,6 +28,7 @@ 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;
var encoding = require('../lib/services/address/encoding');
var DBService = index.services.DB; var DBService = index.services.DB;
var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG'; var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG';
var testKey; var testKey;
@ -43,22 +44,6 @@ describe('Node Functionality', function() {
before(function(done) { before(function(done) {
this.timeout(30000); this.timeout(30000);
// Add the regtest network
bitcore.Networks.remove(bitcore.Networks.testnet);
bitcore.Networks.add({
name: 'regtest',
alias: 'regtest',
pubkeyhash: 0x6f,
privatekey: 0xef,
scripthash: 0xc4,
xpubkey: 0x043587cf,
xprivkey: 0x04358394,
networkMagic: 0xfabfb5da,
port: 18444,
dnsSeeds: [ ]
});
regtest = bitcore.Networks.get('regtest');
var datadir = __dirname + '/data'; var datadir = __dirname + '/data';
testKey = bitcore.PrivateKey(testWIF); testKey = bitcore.PrivateKey(testWIF);
@ -93,6 +78,9 @@ describe('Node Functionality', function() {
node = new BitcoreNode(configuration); node = new BitcoreNode(configuration);
regtest = bitcore.Networks.get('regtest');
should.exist(regtest);
node.on('error', function(err) { node.on('error', function(err) {
log.error(err); log.error(err);
}); });
@ -208,7 +196,7 @@ describe('Node Functionality', function() {
// We need to add a transaction to the mempool so that the next block will // We need to add a transaction to the mempool so that the next block will
// have a different hash as the hash has been invalidated. // have a different hash as the hash has been invalidated.
client.sendToAddress(testKey.toAddress().toString(), 10, function(err) { client.sendToAddress(testKey.toAddress(regtest).toString(), 10, function(err) {
if (err) { if (err) {
throw err; throw err;
} }
@ -250,7 +238,7 @@ describe('Node Functionality', function() {
var address; var address;
var unspentOutput; var unspentOutput;
before(function() { before(function() {
address = testKey.toAddress().toString(); address = testKey.toAddress(regtest).toString();
}); });
it('should be able to get the balance of the test address', function(done) { it('should be able to get the balance of the test address', function(done) {
node.services.address.getBalance(address, false, function(err, balance) { node.services.address.getBalance(address, false, function(err, balance) {
@ -333,19 +321,19 @@ describe('Node Functionality', function() {
/* jshint maxstatements: 50 */ /* jshint maxstatements: 50 */
testKey2 = bitcore.PrivateKey.fromWIF('cNfF4jXiLHQnFRsxaJyr2YSGcmtNYvxQYSakNhuDGxpkSzAwn95x'); testKey2 = bitcore.PrivateKey.fromWIF('cNfF4jXiLHQnFRsxaJyr2YSGcmtNYvxQYSakNhuDGxpkSzAwn95x');
address2 = testKey2.toAddress().toString(); address2 = testKey2.toAddress(regtest).toString();
testKey3 = bitcore.PrivateKey.fromWIF('cVTYQbaFNetiZcvxzXcVMin89uMLC43pEBMy2etgZHbPPxH5obYt'); testKey3 = bitcore.PrivateKey.fromWIF('cVTYQbaFNetiZcvxzXcVMin89uMLC43pEBMy2etgZHbPPxH5obYt');
address3 = testKey3.toAddress().toString(); address3 = testKey3.toAddress(regtest).toString();
testKey4 = bitcore.PrivateKey.fromWIF('cPNQmfE31H2oCUFqaHpfSqjDibkt7XoT2vydLJLDHNTvcddCesGw'); testKey4 = bitcore.PrivateKey.fromWIF('cPNQmfE31H2oCUFqaHpfSqjDibkt7XoT2vydLJLDHNTvcddCesGw');
address4 = testKey4.toAddress().toString(); address4 = testKey4.toAddress(regtest).toString();
testKey5 = bitcore.PrivateKey.fromWIF('cVrzm9gCmnzwEVMGeCxY6xLVPdG3XWW97kwkFH3H3v722nb99QBF'); testKey5 = bitcore.PrivateKey.fromWIF('cVrzm9gCmnzwEVMGeCxY6xLVPdG3XWW97kwkFH3H3v722nb99QBF');
address5 = testKey5.toAddress().toString(); address5 = testKey5.toAddress(regtest).toString();
testKey6 = bitcore.PrivateKey.fromWIF('cPfMesNR2gsQEK69a6xe7qE44CZEZavgMUak5hQ74XDgsRmmGBYF'); testKey6 = bitcore.PrivateKey.fromWIF('cPfMesNR2gsQEK69a6xe7qE44CZEZavgMUak5hQ74XDgsRmmGBYF');
address6 = testKey6.toAddress().toString(); address6 = testKey6.toAddress(regtest).toString();
var tx = new Transaction(); var tx = new Transaction();
tx.from(unspentOutput); tx.from(unspentOutput);
@ -726,7 +714,7 @@ describe('Node Functionality', function() {
node.services.bitcoind.sendTransaction(tx.serialize()); node.services.bitcoind.sendTransaction(tx.serialize());
setImmediate(function() { setImmediate(function() {
var addrObj = node.services.address._getAddressInfo(address); var addrObj = encoding.getAddressInfo(address);
node.services.address._getOutputsMempool(address, addrObj.hashBuffer, node.services.address._getOutputsMempool(address, addrObj.hashBuffer,
addrObj.hashTypeBuffer, function(err, outs) { addrObj.hashTypeBuffer, function(err, outs) {
if (err) { if (err) {

View File

@ -36,6 +36,8 @@ exports.HASH_TYPES_MAP = {
exports.SPACER_MIN = new Buffer('00', 'hex'); exports.SPACER_MIN = new Buffer('00', 'hex');
exports.SPACER_MAX = new Buffer('ff', 'hex'); exports.SPACER_MAX = new Buffer('ff', 'hex');
exports.SPACER_HEIGHT_MIN = new Buffer('0000000000', 'hex');
exports.SPACER_HEIGHT_MAX = new Buffer('ffffffffff', 'hex');
exports.TIMESTAMP_MIN = new Buffer('0000000000000000', 'hex'); exports.TIMESTAMP_MIN = new Buffer('0000000000000000', 'hex');
exports.TIMESTAMP_MAX = new Buffer('ffffffffffffffff', 'hex'); exports.TIMESTAMP_MAX = new Buffer('ffffffffffffffff', 'hex');

View File

@ -61,6 +61,12 @@ exports.encodeOutputValue = function(satoshis, scriptBuffer) {
return Buffer.concat([satoshisBuffer, scriptBuffer]); return Buffer.concat([satoshisBuffer, scriptBuffer]);
}; };
exports.encodeOutputMempoolValue = function(satoshis, timestampBuffer, scriptBuffer) {
var satoshisBuffer = new Buffer(8);
satoshisBuffer.writeDoubleBE(satoshis);
return Buffer.concat([satoshisBuffer, timestampBuffer, scriptBuffer]);
};
exports.decodeOutputValue = function(buffer) { exports.decodeOutputValue = function(buffer) {
var satoshis = buffer.readDoubleBE(0); var satoshis = buffer.readDoubleBE(0);
var scriptBuffer = buffer.slice(8, buffer.length); var scriptBuffer = buffer.slice(8, buffer.length);
@ -70,6 +76,17 @@ exports.decodeOutputValue = function(buffer) {
}; };
}; };
exports.decodeOutputMempoolValue = function(buffer) {
var satoshis = buffer.readDoubleBE(0);
var timestamp = buffer.readDoubleBE(8);
var scriptBuffer = buffer.slice(16, buffer.length);
return {
satoshis: satoshis,
timestamp: timestamp,
scriptBuffer: scriptBuffer
};
};
exports.encodeInputKey = function(hashBuffer, hashTypeBuffer, height, prevTxIdBuffer, outputIndex) { exports.encodeInputKey = function(hashBuffer, hashTypeBuffer, height, prevTxIdBuffer, outputIndex) {
var heightBuffer = new Buffer(4); var heightBuffer = new Buffer(4);
heightBuffer.writeUInt32BE(height); heightBuffer.writeUInt32BE(height);
@ -175,7 +192,8 @@ exports.decodeSummaryCacheKey = function(buffer, network) {
return address; return address;
}; };
exports.encodeSummaryCacheValue = function(cache, tipHeight) { exports.encodeSummaryCacheValue = function(cache, tipHeight, tipHash) {
var tipHashBuffer = new Buffer(tipHash, 'hex');
var buffer = new Buffer(new Array(20)); var buffer = new Buffer(new Array(20));
buffer.writeUInt32BE(tipHeight); buffer.writeUInt32BE(tipHeight);
buffer.writeDoubleBE(cache.result.totalReceived, 4); buffer.writeDoubleBE(cache.result.totalReceived, 4);
@ -189,21 +207,22 @@ exports.encodeSummaryCacheValue = function(cache, tipHeight) {
txidBuffers.push(buf); txidBuffers.push(buf);
} }
var txidsBuffer = Buffer.concat(txidBuffers); var txidsBuffer = Buffer.concat(txidBuffers);
var value = Buffer.concat([buffer, txidsBuffer]); var value = Buffer.concat([tipHashBuffer, buffer, txidsBuffer]);
return value; return value;
}; };
exports.decodeSummaryCacheValue = function(buffer) { exports.decodeSummaryCacheValue = function(buffer) {
var height = buffer.readUInt32BE(); var hash = buffer.slice(0, 32).toString('hex');
var totalReceived = buffer.readDoubleBE(4); var height = buffer.readUInt32BE(32);
var balance = buffer.readDoubleBE(12); var totalReceived = buffer.readDoubleBE(36);
var balance = buffer.readDoubleBE(44);
// read 32 byte chunks until exhausted // read 32 byte chunks until exhausted
var appearanceIds = {}; var appearanceIds = {};
var txids = []; var txids = [];
var pos = 20; var pos = 52;
while(pos < buffer.length) { while(pos < buffer.length) {
var txid = buffer.slice(pos, pos + 32).toString('hex'); var txid = buffer.slice(pos, pos + 32).toString('hex');
var txidHeight = buffer.readUInt32BE(pos + 32); var txidHeight = buffer.readUInt32BE(pos + 32);
@ -214,6 +233,7 @@ exports.decodeSummaryCacheValue = function(buffer) {
var cache = { var cache = {
height: height, height: height,
hash: hash,
result: { result: {
appearanceIds: appearanceIds, appearanceIds: appearanceIds,
txids: txids, txids: txids,

View File

@ -64,8 +64,7 @@ AddressHistory.prototype._mergeAndSortTxids = function(summaries) {
// Unconfirmed are sorted by timestamp // Unconfirmed are sorted by timestamp
return unconfirmedAppearanceIds[a] - unconfirmedAppearanceIds[b]; return unconfirmedAppearanceIds[a] - unconfirmedAppearanceIds[b];
}); });
var txids = confirmedTxids.concat(unconfirmedTxids); return confirmedTxids.concat(unconfirmedTxids);
return txids;
}; };
/** /**
@ -81,7 +80,7 @@ AddressHistory.prototype.get = function(callback) {
return callback(new Error('Maximum number of addresses (' + this.maxAddressQuery + ') exceeded')); return callback(new Error('Maximum number of addresses (' + this.maxAddressQuery + ') exceeded'));
} }
if (this.addresses.length === 0) { if (this.addresses.length === 1) {
var address = this.addresses[0]; var address = this.addresses[0];
self.node.services.address.getAddressSummary(address, this.options, function(err, summary) { self.node.services.address.getAddressSummary(address, this.options, function(err, summary) {
if (err) { if (err) {
@ -111,9 +110,14 @@ AddressHistory.prototype.get = function(callback) {
totalCount = allTxids.length; totalCount = allTxids.length;
// Slice the page starting with the most recent // Slice the page starting with the most recent
var fromOffset = totalCount - self.options.from; var txids;
var toOffset = totalCount - self.options.to; if (self.options.from >= 0 && self.options.to >= 0) {
var txids = allTxids.slice(toOffset, fromOffset); var fromOffset = totalCount - self.options.from;
var toOffset = totalCount - self.options.to;
txids = allTxids.slice(toOffset, fromOffset);
} else {
txids = allTxids;
}
// Verify that this query isn't too long // Verify that this query isn't too long
if (txids.length > self.maxHistoryQueryLength) { if (txids.length > self.maxHistoryQueryLength) {
@ -212,16 +216,19 @@ AddressHistory.prototype.getAddressDetailsForTransaction = function(transaction)
continue; continue;
} }
var inputAddress = input.script.toAddress(this.node.network); var inputAddress = input.script.toAddress(this.node.network);
if (inputAddress && this.addressStrings.indexOf(inputAddress.toString()) > 0) { if (inputAddress) {
if (!result.addresses[inputAddress]) { var inputAddressString = inputAddress.toString();
result.addresses[inputAddress] = { if (this.addressStrings.indexOf(inputAddressString) >= 0) {
inputIndexes: [], if (!result.addresses[inputAddressString]) {
outputIndexes: [] result.addresses[inputAddressString] = {
}; inputIndexes: [inputIndex],
} else { outputIndexes: []
result.addresses[inputAddress].inputIndexes.push(inputIndex); };
} else {
result.addresses[inputAddressString].inputIndexes.push(inputIndex);
}
result.satoshis -= input.output.satoshis;
} }
result.satoshis -= input.output.satoshis;
} }
} }
@ -231,16 +238,19 @@ AddressHistory.prototype.getAddressDetailsForTransaction = function(transaction)
continue; continue;
} }
var outputAddress = output.script.toAddress(this.node.network); var outputAddress = output.script.toAddress(this.node.network);
if (outputAddress && this.addressStrings.indexOf(outputAddress.toString()) > 0) { if (outputAddress) {
if (!result.addresses[outputAddress]) { var outputAddressString = outputAddress.toString();
result.addresses[outputAddress] = { if (this.addressStrings.indexOf(outputAddressString) >= 0) {
inputIndexes: [], if (!result.addresses[outputAddressString]) {
outputIndexes: [] result.addresses[outputAddressString] = {
}; inputIndexes: [],
} else { outputIndexes: [outputIndex]
result.addresses[outputAddress].inputIndexes.push(outputIndex); };
} else {
result.addresses[outputAddressString].outputIndexes.push(outputIndex);
}
result.satoshis += output.satoshis;
} }
result.satoshis += output.satoshis;
} }
} }

View File

@ -328,12 +328,15 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) {
constants.MEMPREFIXES.OUTPUTS, constants.MEMPREFIXES.OUTPUTS,
addressInfo.hashBuffer, addressInfo.hashBuffer,
addressInfo.hashTypeBuffer, addressInfo.hashTypeBuffer,
timestampBuffer,
txidBuffer, txidBuffer,
outputIndexBuffer outputIndexBuffer
]); ]);
var outValue = encoding.encodeOutputValue(output.satoshis, output._scriptBuffer); var outValue = encoding.encodeOutputMempoolValue(
output.satoshis,
timestampBuffer,
output._scriptBuffer
);
operations.push({ operations.push({
type: action, type: action,
@ -395,13 +398,13 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) {
constants.MEMPREFIXES.SPENTS, constants.MEMPREFIXES.SPENTS,
inputHashBuffer, inputHashBuffer,
inputHashType, inputHashType,
timestampBuffer,
input.prevTxId, input.prevTxId,
inputOutputIndexBuffer inputOutputIndexBuffer
]); ]);
var inputValue = Buffer.concat([ var inputValue = Buffer.concat([
txidBuffer, txidBuffer,
inputIndexBuffer inputIndexBuffer,
timestampBuffer
]); ]);
operations.push({ operations.push({
type: action, type: action,
@ -768,13 +771,17 @@ AddressService.prototype.getInputForOutput = function(txid, outputIndex, options
* @param {Function} callback * @param {Function} callback
*/ */
AddressService.prototype.createInputsStream = function(addressStr, options) { AddressService.prototype.createInputsStream = function(addressStr, options) {
var inputStream = new InputsTransformStream({ var inputStream = new InputsTransformStream({
address: new Address(addressStr, this.node.network), address: new Address(addressStr, this.node.network),
tipHeight: this.node.services.db.tip.__height tipHeight: this.node.services.db.tip.__height
}); });
var stream = this.createInputsDBStream(addressStr, options).pipe(inputStream); var stream = this.createInputsDBStream(addressStr, options)
.on('error', function(err) {
// Forward the error
inputStream.emit('error', err);
inputStream.end();
}).pipe(inputStream);
return stream; return stream;
@ -786,23 +793,27 @@ AddressService.prototype.createInputsDBStream = function(addressStr, options) {
var hashBuffer = addrObj.hashBuffer; var hashBuffer = addrObj.hashBuffer;
var hashTypeBuffer = addrObj.hashTypeBuffer; var hashTypeBuffer = addrObj.hashTypeBuffer;
if (options.start && options.end) { if (options.start >= 0 && options.end >= 0) {
var endBuffer = new Buffer(4); var endBuffer = new Buffer(4);
endBuffer.writeUInt32BE(options.end); endBuffer.writeUInt32BE(options.end, 0);
var startBuffer = new Buffer(4); var startBuffer = new Buffer(4);
startBuffer.writeUInt32BE(options.start + 1); // Because the key has additional data following it, we don't have an ability
// to use "gte" or "lte" we can only use "gt" and "lt", we therefore need to adjust the number
// to be one value larger to include it.
var adjustedStart = options.start + 1;
startBuffer.writeUInt32BE(adjustedStart, 0);
stream = this.node.services.db.store.createReadStream({ stream = this.node.services.db.store.createReadStream({
gte: Buffer.concat([ gt: Buffer.concat([
constants.PREFIXES.SPENTS, constants.PREFIXES.SPENTS,
hashBuffer, hashBuffer,
hashTypeBuffer, hashTypeBuffer,
constants.SPACER_MIN, constants.SPACER_MIN,
endBuffer endBuffer
]), ]),
lte: Buffer.concat([ lt: Buffer.concat([
constants.PREFIXES.SPENTS, constants.PREFIXES.SPENTS,
hashBuffer, hashBuffer,
hashTypeBuffer, hashTypeBuffer,
@ -815,8 +826,8 @@ AddressService.prototype.createInputsDBStream = function(addressStr, options) {
} else { } else {
var allKey = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer]); var allKey = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer]);
stream = this.node.services.db.store.createReadStream({ stream = this.node.services.db.store.createReadStream({
gte: Buffer.concat([allKey, constants.SPACER_MIN]), gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]),
lte: Buffer.concat([allKey, constants.SPACER_MAX]), lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]),
valueEncoding: 'binary', valueEncoding: 'binary',
keyEncoding: 'binary' keyEncoding: 'binary'
}); });
@ -903,27 +914,28 @@ AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, ha
constants.MEMPREFIXES.SPENTS, constants.MEMPREFIXES.SPENTS,
hashBuffer, hashBuffer,
hashTypeBuffer, hashTypeBuffer,
constants.TIMESTAMP_MIN constants.SPACER_MIN
]), ]),
lte: Buffer.concat([ lte: Buffer.concat([
constants.MEMPREFIXES.SPENTS, constants.MEMPREFIXES.SPENTS,
hashBuffer, hashBuffer,
hashTypeBuffer, hashTypeBuffer,
constants.TIMESTAMP_MAX constants.SPACER_MAX
]), ]),
valueEncoding: 'binary', valueEncoding: 'binary',
keyEncoding: 'binary' keyEncoding: 'binary'
}); });
stream.on('data', function(data) { stream.on('data', function(data) {
var timestamp = data.key.readDoubleBE(22);
var txid = data.value.slice(0, 32); var txid = data.value.slice(0, 32);
var inputIndex = data.value.readUInt32BE(32); var inputIndex = data.value.readUInt32BE(32);
var timestamp = data.value.readDoubleBE(36);
var input = { var input = {
address: addressStr, address: addressStr,
hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')],
txid: txid.toString('hex'), //TODO use a buffer txid: txid.toString('hex'), //TODO use a buffer
inputIndex: inputIndex, inputIndex: inputIndex,
timestamp: timestamp,
height: -1, height: -1,
confirmations: 0 confirmations: 0
}; };
@ -979,7 +991,13 @@ AddressService.prototype.createOutputsStream = function(addressStr, options) {
tipHeight: this.node.services.db.tip.__height tipHeight: this.node.services.db.tip.__height
}); });
var stream = this.createOutputsDBStream(addressStr, options).pipe(outputStream); var stream = this.createOutputsDBStream(addressStr, options)
.on('error', function(err) {
// Forward the error
outputStream.emit('error', err);
outputStream.end();
})
.pipe(outputStream);
return stream; return stream;
@ -992,22 +1010,27 @@ AddressService.prototype.createOutputsDBStream = function(addressStr, options) {
var hashTypeBuffer = addrObj.hashTypeBuffer; var hashTypeBuffer = addrObj.hashTypeBuffer;
var stream; var stream;
if (options.start && options.end) { if (options.start >= 0 && options.end >= 0) {
var endBuffer = new Buffer(4);
endBuffer.writeUInt32BE(options.end, 0);
var startBuffer = new Buffer(4); var startBuffer = new Buffer(4);
startBuffer.writeUInt32BE(options.start + 1); // Because the key has additional data following it, we don't have an ability
var endBuffer = new Buffer(4); // to use "gte" or "lte" we can only use "gt" and "lt", we therefore need to adjust the number
endBuffer.writeUInt32BE(options.end); // to be one value larger to include it.
var startAdjusted = options.start + 1;
startBuffer.writeUInt32BE(startAdjusted, 0);
stream = this.node.services.db.store.createReadStream({ stream = this.node.services.db.store.createReadStream({
gte: Buffer.concat([ gt: Buffer.concat([
constants.PREFIXES.OUTPUTS, constants.PREFIXES.OUTPUTS,
hashBuffer, hashBuffer,
hashTypeBuffer, hashTypeBuffer,
constants.SPACER_MIN, constants.SPACER_MIN,
endBuffer endBuffer
]), ]),
lte: Buffer.concat([ lt: Buffer.concat([
constants.PREFIXES.OUTPUTS, constants.PREFIXES.OUTPUTS,
hashBuffer, hashBuffer,
hashTypeBuffer, hashTypeBuffer,
@ -1020,8 +1043,8 @@ AddressService.prototype.createOutputsDBStream = function(addressStr, options) {
} else { } else {
var allKey = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer]); var allKey = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer]);
stream = this.node.services.db.store.createReadStream({ stream = this.node.services.db.store.createReadStream({
gte: Buffer.concat([allKey, constants.SPACER_MIN]), gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]),
lte: Buffer.concat([allKey, constants.SPACER_MAX]), lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]),
valueEncoding: 'binary', valueEncoding: 'binary',
keyEncoding: 'binary' keyEncoding: 'binary'
}); });
@ -1113,13 +1136,13 @@ AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, h
constants.MEMPREFIXES.OUTPUTS, constants.MEMPREFIXES.OUTPUTS,
hashBuffer, hashBuffer,
hashTypeBuffer, hashTypeBuffer,
constants.TIMESTAMP_MIN constants.SPACER_MIN
]), ]),
lte: Buffer.concat([ lte: Buffer.concat([
constants.MEMPREFIXES.OUTPUTS, constants.MEMPREFIXES.OUTPUTS,
hashBuffer, hashBuffer,
hashTypeBuffer, hashTypeBuffer,
constants.TIMESTAMP_MAX constants.SPACER_MAX
]), ]),
valueEncoding: 'binary', valueEncoding: 'binary',
keyEncoding: 'binary' keyEncoding: 'binary'
@ -1127,18 +1150,17 @@ AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, h
stream.on('data', function(data) { stream.on('data', function(data) {
// Format of data: // Format of data:
// prefix: 1, hashBuffer: 20, hashTypeBuffer: 1, timestamp: 8, txid: 32, outputIndex: 4 // prefix: 1, hashBuffer: 20, hashTypeBuffer: 1, txid: 32, outputIndex: 4
var timestamp = data.key.readDoubleBE(22); var txid = data.key.slice(22, 54);
var txid = data.key.slice(30, 62); var outputIndex = data.key.readUInt32BE(54);
var outputIndex = data.key.readUInt32BE(62); var value = encoding.decodeOutputMempoolValue(data.value);
var value = encoding.decodeOutputValue(data.value);
var output = { var output = {
address: addressStr, address: addressStr,
hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')],
txid: txid.toString('hex'), //TODO use a buffer txid: txid.toString('hex'), //TODO use a buffer
outputIndex: outputIndex, outputIndex: outputIndex,
height: -1, height: -1,
timestamp: timestamp, timestamp: value.timestamp,
satoshis: value.satoshis, satoshis: value.satoshis,
script: value.scriptBuffer.toString('hex'), //TODO use a buffer script: value.scriptBuffer.toString('hex'), //TODO use a buffer
confirmations: 0 confirmations: 0
@ -1325,43 +1347,25 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb
var self = this; var self = this;
var startTime = new Date(); var startTime = new Date();
var address = new Address(addressArg); var address = new Address(addressArg);
var tipHeight = this.node.services.db.tip.__height;
if (_.isUndefined(options.queryMempool)) {
options.queryMempool = true;
}
async.waterfall([ async.waterfall([
function(next) { function(next) {
self._getAddressSummaryCache(address, next); self._getAddressConfirmedSummary(address, options, next);
}, },
function(cache, next) { function(cache, next) {
self._getAddressInputsSummary(address, cache, tipHeight, next); self._getAddressMempoolSummary(address, options, cache, next);
},
function(cache, next) {
self._getAddressOutputsSummary(address, cache, tipHeight, next);
},
function(cache, next) {
self._sortTxids(cache, tipHeight, next);
},
function(cache, next) {
self._saveAddressSummaryCache(address, cache, tipHeight, next);
} }
], function(err, cache) { ], function(err, cache) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
var result = cache.result; var summary = self._transformAddressSummaryFromCache(cache, options);
var confirmedTxids = result.txids;
var unconfirmedTxids = Object.keys(result.unconfirmedAppearanceIds);
var summary = {
totalReceived: result.totalReceived,
totalSpent: result.totalReceived - result.balance,
balance: result.balance,
appearances: confirmedTxids.length,
unconfirmedBalance: result.unconfirmedBalance,
unconfirmedAppearances: unconfirmedTxids.length
};
var timeDelta = new Date() - startTime; var timeDelta = new Date() - startTime;
if (timeDelta > 5000) { if (timeDelta > 5000) {
@ -1370,52 +1374,30 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb
log.warn('Address Summary:', summary); log.warn('Address Summary:', summary);
} }
if (options.fullTxList) {
summary.appearanceIds = result.appearanceIds;
summary.unconfirmedAppearanceIds = result.unconfirmedAppearanceIds;
} else if (!options.noTxList) {
summary.txids = confirmedTxids.concat(unconfirmedTxids);
}
callback(null, summary); callback(null, summary);
}); });
}; };
AddressService.prototype._sortTxids = function(cache, tipHeight, callback) { AddressService.prototype._getAddressConfirmedSummary = function(address, options, callback) {
if (cache.height === tipHeight) { var self = this;
return callback(null, cache); var tipHeight = this.node.services.db.tip.__height;
}
cache.result.txids = Object.keys(cache.result.appearanceIds); self._getAddressConfirmedSummaryCache(address, options, function(err, cache) {
cache.result.txids.sort(function(a, b) { if (err) {
return cache.result.appearanceIds[a] - cache.result.appearanceIds[b]; return callback(err);
}
// Immediately give cache is already current, otherwise update
if (cache && cache.height === tipHeight) {
return callback(null, cache);
}
self._updateAddressConfirmedSummaryCache(address, options, cache, tipHeight, callback);
}); });
callback(null, cache);
}; };
AddressService.prototype._saveAddressSummaryCache = function(address, cache, tipHeight, callback) { AddressService.prototype._getAddressConfirmedSummaryCache = function(address, options, callback) {
if (cache.height === tipHeight) { var self = this;
return callback(null, cache);
}
var transactionLength = cache.result.txids.length;
var exceedsCacheThreshold = (transactionLength > this.summaryCacheThreshold);
if (exceedsCacheThreshold) {
log.info('Saving address summary cache for: ' + address.toString() + 'at height: ' + tipHeight);
var key = encoding.encodeSummaryCacheKey(address);
var value = encoding.encodeSummaryCacheValue(cache, tipHeight);
this.summaryCache.put(key, value, function(err) {
if (err) {
return callback(err);
}
callback(null, cache);
});
} else {
callback(null, cache);
}
};
AddressService.prototype._getAddressSummaryCache = function(address, callback) {
var baseCache = { var baseCache = {
result: { result: {
appearanceIds: {}, appearanceIds: {},
@ -1425,6 +1407,11 @@ AddressService.prototype._getAddressSummaryCache = function(address, callback) {
unconfirmedBalance: 0 unconfirmedBalance: 0
} }
}; };
// Use the base cache if the "start" and "end" options have been used
// We only save and retrieve a cache for the summary of all history
if (options.start >= 0 || options.end >= 0) {
return callback(null, baseCache);
}
var key = encoding.encodeSummaryCacheKey(address); var key = encoding.encodeSummaryCacheKey(address);
this.summaryCache.get(key, { this.summaryCache.get(key, {
valueEncoding: 'binary', valueEncoding: 'binary',
@ -1436,25 +1423,64 @@ AddressService.prototype._getAddressSummaryCache = function(address, callback) {
return callback(err); return callback(err);
} }
var cache = encoding.decodeSummaryCacheValue(buffer); var cache = encoding.decodeSummaryCacheValue(buffer);
// Use base cache if the cached tip/height doesn't match (e.g. there has been a reorg)
var blockIndex = self.node.services.bitcoind.getBlockIndex(cache.height);
if (cache.hash !== blockIndex.hash) {
return callback(null, baseCache);
}
callback(null, cache); callback(null, cache);
}); });
}; };
AddressService.prototype._getAddressInputsSummary = function(address, cache, tipHeight, callback) { AddressService.prototype._updateAddressConfirmedSummaryCache = function(address, options, cache, tipHeight, callback) {
if (cache.height === tipHeight) {
return callback(null, cache);
}
$.checkArgument(address instanceof Address);
var self = this; var self = this;
var optionsPartial = _.clone(options);
var isHeightQuery = (options.start >= 0 || options.end >= 0);
if (!isHeightQuery) {
// We will pick up from the last point cached and query for all blocks
// proceeding the cache
var cacheHeight = _.isUndefined(cache.height) ? 0 : cache.height + 1;
optionsPartial.start = tipHeight;
optionsPartial.end = cacheHeight;
} else {
$.checkState(_.isUndefined(cache.height));
}
async.waterfall([
function(next) {
self._getAddressConfirmedInputsSummary(address, cache, optionsPartial, next);
},
function(cache, next) {
self._getAddressConfirmedOutputsSummary(address, cache, optionsPartial, next);
},
function(cache, next) {
self._setAndSortTxidsFromAppearanceIds(cache, next);
}
], function(err, cache) {
// Skip saving the cache if the "start" or "end" options have been used, or
// if the transaction length does not exceed the caching threshold.
// We only want to cache full history results for addresses that have a large
// number of transactions.
var exceedsCacheThreshold = (cache.result.txids.length > self.summaryCacheThreshold);
if (exceedsCacheThreshold && !isHeightQuery) {
self._saveAddressConfirmedSummaryCache(address, cache, tipHeight, callback);
} else {
callback(null, cache);
}
});
};
AddressService.prototype._getAddressConfirmedInputsSummary = function(address, cache, options, callback) {
$.checkArgument(address instanceof Address);
var self = this;
var error = null; var error = null;
var opts = { var inputsStream = self.createInputsStream(address, options);
start: _.isUndefined(cache.height) ? 0 : cache.height + 1,
end: tipHeight
};
var inputsStream = self.createInputsStream(address, opts);
inputsStream.on('data', function(input) { inputsStream.on('data', function(input) {
var txid = input.txid; var txid = input.txid;
cache.result.appearanceIds[txid] = input.height; cache.result.appearanceIds[txid] = input.height;
@ -1465,28 +1491,14 @@ AddressService.prototype._getAddressInputsSummary = function(address, cache, tip
}); });
inputsStream.on('end', function() { inputsStream.on('end', function() {
if (error) {
var addressStr = address.toString(); return callback(error);
var hashBuffer = address.hashBuffer; }
var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; callback(null, cache);
self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) {
if (err) {
return callback(err);
}
for(var i = 0; i < mempoolInputs.length; i++) {
var input = mempoolInputs[i];
cache.result.unconfirmedAppearanceIds[input.txid] = input.timestamp;
}
callback(error, cache);
});
}); });
}; };
AddressService.prototype._getAddressOutputsSummary = function(address, cache, tipHeight, callback) { AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, cache, options, callback) {
if (cache.height === tipHeight) {
return callback(null, cache);
}
$.checkArgument(address instanceof Address); $.checkArgument(address instanceof Address);
$.checkArgument(!_.isUndefined(cache.result) && $.checkArgument(!_.isUndefined(cache.result) &&
!_.isUndefined(cache.result.appearanceIds) && !_.isUndefined(cache.result.appearanceIds) &&
@ -1494,12 +1506,7 @@ AddressService.prototype._getAddressOutputsSummary = function(address, cache, ti
var self = this; var self = this;
var opts = { var outputStream = self.createOutputsStream(address, options);
start: _.isUndefined(cache.height) ? 0 : cache.height + 1,
end: tipHeight
};
var outputStream = self.createOutputsStream(address, opts);
outputStream.on('data', function(output) { outputStream.on('data', function(output) {
@ -1514,16 +1521,19 @@ AddressService.prototype._getAddressOutputsSummary = function(address, cache, ti
if (!spentDB) { if (!spentDB) {
cache.result.balance += output.satoshis; cache.result.balance += output.satoshis;
} }
// TODO: subtract if spent (because of cache)?
// Check to see if this output is spent in the mempool and if so if (options.queryMempool) {
// we will subtract it from the unconfirmedBalance (a.k.a unconfirmedDelta) // Check to see if this output is spent in the mempool and if so
var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey( // we will subtract it from the unconfirmedBalance (a.k.a unconfirmedDelta)
new Buffer(txid, 'hex'), // TODO: get buffer directly var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(
outputIndex new Buffer(txid, 'hex'), // TODO: get buffer directly
); outputIndex
var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey]; );
if (spentMempool) { var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey];
cache.result.unconfirmedBalance -= output.satoshis; if (spentMempool) {
cache.result.unconfirmedBalance -= output.satoshis;
}
} }
}); });
@ -1535,38 +1545,112 @@ AddressService.prototype._getAddressOutputsSummary = function(address, cache, ti
}); });
outputStream.on('end', function() { outputStream.on('end', function() {
if (error) {
var addressStr = address.toString(); return callback(error);
var hashBuffer = address.hashBuffer; }
var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type]; callback(null, cache);
self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) {
if (err) {
return callback(err);
}
for(var i = 0; i < mempoolOutputs.length; i++) {
var output = mempoolOutputs[i];
cache.result.unconfirmedAppearanceIds[output.txid] = output.timestamp;
var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(
new Buffer(output.txid, 'hex'), // TODO: get buffer directly
output.outputIndex
);
var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey];
// Only add this to the balance if it's not spent in the mempool already
if (!spentMempool) {
cache.result.unconfirmedBalance += output.satoshis;
}
}
callback(error, cache);
});
}); });
}; };
AddressService.prototype._setAndSortTxidsFromAppearanceIds = function(cache, callback) {
cache.result.txids = Object.keys(cache.result.appearanceIds);
cache.result.txids.sort(function(a, b) {
return cache.result.appearanceIds[a] - cache.result.appearanceIds[b];
});
callback(null, cache);
};
AddressService.prototype._saveAddressConfirmedSummaryCache = function(address, cache, tipHeight, callback) {
log.info('Saving address summary cache for: ' + address.toString() + 'at height: ' + tipHeight);
var key = encoding.encodeSummaryCacheKey(address);
var tipBlockIndex = this.node.services.bitcoind.getBlockIndex(tipHeight);
var value = encoding.encodeSummaryCacheValue(cache, tipHeight, tipBlockIndex.hash);
this.summaryCache.put(key, value, function(err) {
if (err) {
return callback(err);
}
callback(null, cache);
});
};
AddressService.prototype._getAddressMempoolSummary = function(address, options, cache, callback) {
var self = this;
// Skip if the options do not want to include the mempool
if (!options.queryMempool) {
return callback(null, cache);
}
var addressStr = address.toString();
var hashBuffer = address.hashBuffer;
var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type];
async.waterfall([
function(next) {
self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) {
if (err) {
return next(err);
}
for(var i = 0; i < mempoolInputs.length; i++) {
var input = mempoolInputs[i];
cache.result.unconfirmedAppearanceIds[input.txid] = input.timestamp;
}
next(null, cache);
});
}, function(cache, next) {
self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) {
if (err) {
return next(err);
}
for(var i = 0; i < mempoolOutputs.length; i++) {
var output = mempoolOutputs[i];
cache.result.unconfirmedAppearanceIds[output.txid] = output.timestamp;
var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(
new Buffer(output.txid, 'hex'), // TODO: get buffer directly
output.outputIndex
);
var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey];
// Only add this to the balance if it's not spent in the mempool already
if (!spentMempool) {
cache.result.unconfirmedBalance += output.satoshis;
}
}
next(null, cache);
});
}
], callback);
};
AddressService.prototype._transformAddressSummaryFromCache = function(cache, options) {
var result = cache.result;
var confirmedTxids = cache.result.txids;
var unconfirmedTxids = Object.keys(result.unconfirmedAppearanceIds);
var summary = {
totalReceived: result.totalReceived,
totalSpent: result.totalReceived - result.balance,
balance: result.balance,
appearances: confirmedTxids.length,
unconfirmedBalance: result.unconfirmedBalance,
unconfirmedAppearances: unconfirmedTxids.length
};
if (options.fullTxList) {
summary.appearanceIds = result.appearanceIds;
summary.unconfirmedAppearanceIds = result.unconfirmedAppearanceIds;
} else if (!options.noTxList) {
summary.txids = confirmedTxids.concat(unconfirmedTxids);
}
return summary;
};
module.exports = AddressService; module.exports = AddressService;

View File

@ -54,8 +54,8 @@
"commander": "^2.8.1", "commander": "^2.8.1",
"errno": "^0.1.4", "errno": "^0.1.4",
"express": "^4.13.3", "express": "^4.13.3",
"leveldown": "^1.4.2", "leveldown": "^1.4.3",
"levelup": "^1.2.1", "levelup": "^1.3.1",
"liftoff": "^2.2.0", "liftoff": "^2.2.0",
"memdown": "^1.0.0", "memdown": "^1.0.0",
"mkdirp": "0.5.0", "mkdirp": "0.5.0",

View File

@ -0,0 +1,103 @@
'use strict';
var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
var bitcorenode = require('../../../');
var bitcore = require('bitcore-lib');
var Address = bitcore.Address;
var Script = bitcore.Script;
var AddressService = bitcorenode.services.Address;
var Networks = bitcore.Networks;
var encoding = require('../../../lib/services/address/encoding');
var mockdb = {
};
var mocknode = {
network: Networks.testnet,
datadir: 'testdir',
db: mockdb,
services: {
bitcoind: {
on: sinon.stub()
}
}
};
describe('Address Service Encoding', function() {
describe('#encodeSpentIndexSyncKey', function() {
it('will encode to 36 bytes (string)', function() {
var txidBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex');
var key = encoding.encodeSpentIndexSyncKey(txidBuffer, 12);
key.length.should.equal(36);
});
it('will be able to decode encoded value', function() {
var txid = '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7';
var txidBuffer = new Buffer(txid, 'hex');
var key = encoding.encodeSpentIndexSyncKey(txidBuffer, 12);
var keyBuffer = new Buffer(key, 'binary');
keyBuffer.slice(0, 32).toString('hex').should.equal(txid);
var outputIndex = keyBuffer.readUInt32BE(32);
outputIndex.should.equal(12);
});
});
describe('#_encodeInputKeyMap/#_decodeInputKeyMap roundtrip', function() {
var encoded;
var outputTxIdBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex');
it('encode key', function() {
encoded = encoding.encodeInputKeyMap(outputTxIdBuffer, 13);
});
it('decode key', function() {
var key = encoding.decodeInputKeyMap(encoded);
key.outputTxId.toString('hex').should.equal(outputTxIdBuffer.toString('hex'));
key.outputIndex.should.equal(13);
});
});
describe('#_encodeInputValueMap/#_decodeInputValueMap roundtrip', function() {
var encoded;
var inputTxIdBuffer = new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', 'hex');
it('encode key', function() {
encoded = encoding.encodeInputValueMap(inputTxIdBuffer, 7);
});
it('decode key', function() {
var key = encoding.decodeInputValueMap(encoded);
key.inputTxId.toString('hex').should.equal(inputTxIdBuffer.toString('hex'));
key.inputIndex.should.equal(7);
});
});
describe('#extractAddressInfoFromScript', function() {
it('pay-to-publickey', function() {
var pubkey = new bitcore.PublicKey('022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da');
var script = Script.buildPublicKeyOut(pubkey);
var info = encoding.extractAddressInfoFromScript(script, Networks.livenet);
info.addressType.should.equal(Address.PayToPublicKeyHash);
info.hashBuffer.toString('hex').should.equal('9674af7395592ec5d91573aa8d6557de55f60147');
});
it('pay-to-publickeyhash', function() {
var script = Script('OP_DUP OP_HASH160 20 0x0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG');
var info = encoding.extractAddressInfoFromScript(script, Networks.livenet);
info.addressType.should.equal(Address.PayToPublicKeyHash);
info.hashBuffer.toString('hex').should.equal('0000000000000000000000000000000000000000');
});
it('pay-to-scripthash', function() {
var script = Script('OP_HASH160 20 0x0000000000000000000000000000000000000000 OP_EQUAL');
var info = encoding.extractAddressInfoFromScript(script, Networks.livenet);
info.addressType.should.equal(Address.PayToScriptHash);
info.hashBuffer.toString('hex').should.equal('0000000000000000000000000000000000000000');
});
it('non-address script type', function() {
var buf = new Buffer(40);
buf.fill(0);
var script = Script('OP_RETURN 40 0x' + buf.toString('hex'));
var info = encoding.extractAddressInfoFromScript(script, Networks.livenet);
info.should.equal(false);
});
});
});

View File

@ -23,8 +23,6 @@ describe('Address Service History', function() {
history.node.should.equal(node); history.node.should.equal(node);
history.options.should.equal(options); history.options.should.equal(options);
history.addresses.should.equal(addresses); history.addresses.should.equal(addresses);
history.transactionInfo.should.deep.equal([]);
history.combinedArray.should.deep.equal([]);
history.detailedArray.should.deep.equal([]); history.detailedArray.should.deep.equal([]);
}); });
it('will set addresses an array if only sent a string', function() { it('will set addresses an array if only sent a string', function() {
@ -40,27 +38,29 @@ describe('Address Service History', function() {
describe('#get', function() { describe('#get', function() {
it('will complete the async each limit series', function(done) { it('will complete the async each limit series', function(done) {
var addresses = [address]; var addresses = [address];
var summary = {
txids: []
};
var history = new AddressHistory({ var history = new AddressHistory({
node: {}, node: {
services: {
address: {
getAddressSummary: sinon.stub().callsArgWith(2, null, summary)
}
}
},
options: {}, options: {},
addresses: addresses addresses: addresses
}); });
var expected = [{}]; var expected = [{}];
history.detailedArray = expected; history.detailedArray = expected;
history.combinedArray = [{}];
history.getTransactionInfo = sinon.stub().callsArg(1);
history.combineTransactionInfo = sinon.stub();
history.sortAndPaginateCombinedArray = sinon.stub();
history.getDetailedInfo = sinon.stub().callsArg(1); history.getDetailedInfo = sinon.stub().callsArg(1);
history.sortTransactionsIntoArray = sinon.stub();
history.get(function(err, results) { history.get(function(err, results) {
if (err) { if (err) {
throw err; throw err;
} }
history.getTransactionInfo.callCount.should.equal(1);
history.getDetailedInfo.callCount.should.equal(1); history.getDetailedInfo.callCount.should.equal(1);
history.combineTransactionInfo.callCount.should.equal(1); history.combineTransactionInfo.callCount.should.equal(1);
history.sortAndPaginateCombinedArray.callCount.should.equal(1);
results.should.deep.equal({ results.should.deep.equal({
totalCount: 1, totalCount: 1,
items: expected items: expected
@ -78,149 +78,15 @@ describe('Address Service History', function() {
var expected = [{}]; var expected = [{}];
history.sortedArray = expected; history.sortedArray = expected;
history.transactionInfo = [{}]; history.transactionInfo = [{}];
history.getTransactionInfo = sinon.stub().callsArg(1);
history.paginateSortedArray = sinon.stub();
history.getDetailedInfo = sinon.stub().callsArgWith(1, new Error('test')); history.getDetailedInfo = sinon.stub().callsArgWith(1, new Error('test'));
history.get(function(err) { history.get(function(err) {
err.message.should.equal('test'); err.message.should.equal('test');
done(); 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() { describe('#_mergeAndSortTxids', 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() { it('will sort latest to oldest using height', function() {
var transactionInfo = [ var transactionInfo = [
{ {
@ -386,131 +252,6 @@ describe('Address Service History', function() {
}); });
}); });
describe('#sortAndPaginateCombinedArray', function() {
it('from 0 to 2', function() {
var history = new AddressHistory({
node: {},
options: {
from: 0,
to: 2
},
addresses: []
});
history.combinedArray = [
{
height: 13
},
{
height: 14,
},
{
height: 12
}
];
history.sortAndPaginateCombinedArray();
history.combinedArray.length.should.equal(2);
history.combinedArray[0].height.should.equal(14);
history.combinedArray[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.combinedArray = [
{
height: 13
},
{
height: 14,
},
{
height: 12
}
];
history.sortAndPaginateCombinedArray();
history.combinedArray.length.should.equal(3);
history.combinedArray[0].height.should.equal(14);
history.combinedArray[1].height.should.equal(13);
history.combinedArray[2].height.should.equal(12);
});
it('from 0 to 1', function() {
var history = new AddressHistory({
node: {},
options: {
from: 0,
to: 1
},
addresses: []
});
history.combinedArray = [
{
height: 13
},
{
height: 14,
},
{
height: 12
}
];
history.sortAndPaginateCombinedArray();
history.combinedArray.length.should.equal(1);
history.combinedArray[0].height.should.equal(14);
});
it('from 2 to 3', function() {
var history = new AddressHistory({
node: {},
options: {
from: 2,
to: 3
},
addresses: []
});
history.combinedArray = [
{
height: 13
},
{
height: 14,
},
{
height: 12
}
];
history.sortAndPaginateCombinedArray();
history.combinedArray.length.should.equal(1);
history.combinedArray[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.combinedArray = [
{
height: 13
},
{
height: 14,
},
{
height: 12
}
];
history.sortAndPaginateCombinedArray();
history.combinedArray.length.should.equal(0);
});
});
describe('#getDetailedInfo', function() { describe('#getDetailedInfo', function() {
it('will add additional information to existing this.transactions', function() { it('will add additional information to existing this.transactions', function() {
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0'; var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
@ -602,7 +343,7 @@ describe('Address Service History', function() {
} }
}, },
options: {}, options: {},
addresses: [] addresses: [txAddress]
}); });
var transactionInfo = { var transactionInfo = {
addresses: {}, addresses: {},
@ -614,7 +355,7 @@ describe('Address Service History', function() {
transactionInfo.addresses[txAddress] = {}; transactionInfo.addresses[txAddress] = {};
transactionInfo.addresses[txAddress].outputIndexes = [1]; transactionInfo.addresses[txAddress].outputIndexes = [1];
transactionInfo.addresses[txAddress].inputIndexes = []; transactionInfo.addresses[txAddress].inputIndexes = [];
history.getDetailedInfo(transactionInfo, function(err) { history.getDetailedInfo(txid, function(err) {
if (err) { if (err) {
throw err; throw err;
} }
@ -653,28 +394,4 @@ describe('Address Service History', function() {
history.getConfirmationsDetail(transaction).should.equal(1); history.getConfirmationsDetail(transaction).should.equal(1);
}); });
}); });
describe('#getSatoshisDetail', function() {
it('subtract inputIndexes satoshis without outputIndexes', function() {
var history = new AddressHistory({
node: {},
options: {},
addresses: []
});
var transaction = {
inputs: [
{
output: {
satoshis: 10000
}
}
]
};
var txInfo = {
addresses: {}
};
txInfo.addresses[address] = {};
txInfo.addresses[address].inputIndexes = [0];
history.getSatoshisDetail(transaction, txInfo).should.equal(-10000);
});
});
}); });

File diff suppressed because it is too large Load Diff