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 AddressService = index.services.Address;
var BitcoinService = index.services.Bitcoin;
var encoding = require('../lib/services/address/encoding');
var DBService = index.services.DB;
var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG';
var testKey;
@ -43,22 +44,6 @@ describe('Node Functionality', function() {
before(function(done) {
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';
testKey = bitcore.PrivateKey(testWIF);
@ -93,6 +78,9 @@ describe('Node Functionality', function() {
node = new BitcoreNode(configuration);
regtest = bitcore.Networks.get('regtest');
should.exist(regtest);
node.on('error', function(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
// 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) {
throw err;
}
@ -250,7 +238,7 @@ describe('Node Functionality', function() {
var address;
var unspentOutput;
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) {
node.services.address.getBalance(address, false, function(err, balance) {
@ -333,19 +321,19 @@ describe('Node Functionality', function() {
/* jshint maxstatements: 50 */
testKey2 = bitcore.PrivateKey.fromWIF('cNfF4jXiLHQnFRsxaJyr2YSGcmtNYvxQYSakNhuDGxpkSzAwn95x');
address2 = testKey2.toAddress().toString();
address2 = testKey2.toAddress(regtest).toString();
testKey3 = bitcore.PrivateKey.fromWIF('cVTYQbaFNetiZcvxzXcVMin89uMLC43pEBMy2etgZHbPPxH5obYt');
address3 = testKey3.toAddress().toString();
address3 = testKey3.toAddress(regtest).toString();
testKey4 = bitcore.PrivateKey.fromWIF('cPNQmfE31H2oCUFqaHpfSqjDibkt7XoT2vydLJLDHNTvcddCesGw');
address4 = testKey4.toAddress().toString();
address4 = testKey4.toAddress(regtest).toString();
testKey5 = bitcore.PrivateKey.fromWIF('cVrzm9gCmnzwEVMGeCxY6xLVPdG3XWW97kwkFH3H3v722nb99QBF');
address5 = testKey5.toAddress().toString();
address5 = testKey5.toAddress(regtest).toString();
testKey6 = bitcore.PrivateKey.fromWIF('cPfMesNR2gsQEK69a6xe7qE44CZEZavgMUak5hQ74XDgsRmmGBYF');
address6 = testKey6.toAddress().toString();
address6 = testKey6.toAddress(regtest).toString();
var tx = new Transaction();
tx.from(unspentOutput);
@ -726,7 +714,7 @@ describe('Node Functionality', function() {
node.services.bitcoind.sendTransaction(tx.serialize());
setImmediate(function() {
var addrObj = node.services.address._getAddressInfo(address);
var addrObj = encoding.getAddressInfo(address);
node.services.address._getOutputsMempool(address, addrObj.hashBuffer,
addrObj.hashTypeBuffer, function(err, outs) {
if (err) {

View File

@ -36,6 +36,8 @@ exports.HASH_TYPES_MAP = {
exports.SPACER_MIN = new Buffer('00', '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_MAX = new Buffer('ffffffffffffffff', 'hex');

View File

@ -61,6 +61,12 @@ exports.encodeOutputValue = function(satoshis, 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) {
var satoshis = buffer.readDoubleBE(0);
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) {
var heightBuffer = new Buffer(4);
heightBuffer.writeUInt32BE(height);
@ -175,7 +192,8 @@ exports.decodeSummaryCacheKey = function(buffer, network) {
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));
buffer.writeUInt32BE(tipHeight);
buffer.writeDoubleBE(cache.result.totalReceived, 4);
@ -189,21 +207,22 @@ exports.encodeSummaryCacheValue = function(cache, tipHeight) {
txidBuffers.push(buf);
}
var txidsBuffer = Buffer.concat(txidBuffers);
var value = Buffer.concat([buffer, txidsBuffer]);
var value = Buffer.concat([tipHashBuffer, buffer, txidsBuffer]);
return value;
};
exports.decodeSummaryCacheValue = function(buffer) {
var height = buffer.readUInt32BE();
var totalReceived = buffer.readDoubleBE(4);
var balance = buffer.readDoubleBE(12);
var hash = buffer.slice(0, 32).toString('hex');
var height = buffer.readUInt32BE(32);
var totalReceived = buffer.readDoubleBE(36);
var balance = buffer.readDoubleBE(44);
// read 32 byte chunks until exhausted
var appearanceIds = {};
var txids = [];
var pos = 20;
var pos = 52;
while(pos < buffer.length) {
var txid = buffer.slice(pos, pos + 32).toString('hex');
var txidHeight = buffer.readUInt32BE(pos + 32);
@ -214,6 +233,7 @@ exports.decodeSummaryCacheValue = function(buffer) {
var cache = {
height: height,
hash: hash,
result: {
appearanceIds: appearanceIds,
txids: txids,

View File

@ -64,8 +64,7 @@ AddressHistory.prototype._mergeAndSortTxids = function(summaries) {
// Unconfirmed are sorted by timestamp
return unconfirmedAppearanceIds[a] - unconfirmedAppearanceIds[b];
});
var txids = confirmedTxids.concat(unconfirmedTxids);
return txids;
return confirmedTxids.concat(unconfirmedTxids);
};
/**
@ -81,7 +80,7 @@ AddressHistory.prototype.get = function(callback) {
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];
self.node.services.address.getAddressSummary(address, this.options, function(err, summary) {
if (err) {
@ -111,9 +110,14 @@ AddressHistory.prototype.get = function(callback) {
totalCount = allTxids.length;
// Slice the page starting with the most recent
var fromOffset = totalCount - self.options.from;
var toOffset = totalCount - self.options.to;
var txids = allTxids.slice(toOffset, fromOffset);
var txids;
if (self.options.from >= 0 && self.options.to >= 0) {
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
if (txids.length > self.maxHistoryQueryLength) {
@ -212,16 +216,19 @@ AddressHistory.prototype.getAddressDetailsForTransaction = function(transaction)
continue;
}
var inputAddress = input.script.toAddress(this.node.network);
if (inputAddress && this.addressStrings.indexOf(inputAddress.toString()) > 0) {
if (!result.addresses[inputAddress]) {
result.addresses[inputAddress] = {
inputIndexes: [],
outputIndexes: []
};
} else {
result.addresses[inputAddress].inputIndexes.push(inputIndex);
if (inputAddress) {
var inputAddressString = inputAddress.toString();
if (this.addressStrings.indexOf(inputAddressString) >= 0) {
if (!result.addresses[inputAddressString]) {
result.addresses[inputAddressString] = {
inputIndexes: [inputIndex],
outputIndexes: []
};
} 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;
}
var outputAddress = output.script.toAddress(this.node.network);
if (outputAddress && this.addressStrings.indexOf(outputAddress.toString()) > 0) {
if (!result.addresses[outputAddress]) {
result.addresses[outputAddress] = {
inputIndexes: [],
outputIndexes: []
};
} else {
result.addresses[outputAddress].inputIndexes.push(outputIndex);
if (outputAddress) {
var outputAddressString = outputAddress.toString();
if (this.addressStrings.indexOf(outputAddressString) >= 0) {
if (!result.addresses[outputAddressString]) {
result.addresses[outputAddressString] = {
inputIndexes: [],
outputIndexes: [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,
addressInfo.hashBuffer,
addressInfo.hashTypeBuffer,
timestampBuffer,
txidBuffer,
outputIndexBuffer
]);
var outValue = encoding.encodeOutputValue(output.satoshis, output._scriptBuffer);
var outValue = encoding.encodeOutputMempoolValue(
output.satoshis,
timestampBuffer,
output._scriptBuffer
);
operations.push({
type: action,
@ -395,13 +398,13 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) {
constants.MEMPREFIXES.SPENTS,
inputHashBuffer,
inputHashType,
timestampBuffer,
input.prevTxId,
inputOutputIndexBuffer
]);
var inputValue = Buffer.concat([
txidBuffer,
inputIndexBuffer
inputIndexBuffer,
timestampBuffer
]);
operations.push({
type: action,
@ -768,13 +771,17 @@ AddressService.prototype.getInputForOutput = function(txid, outputIndex, options
* @param {Function} callback
*/
AddressService.prototype.createInputsStream = function(addressStr, options) {
var inputStream = new InputsTransformStream({
address: new Address(addressStr, this.node.network),
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;
@ -786,23 +793,27 @@ AddressService.prototype.createInputsDBStream = function(addressStr, options) {
var hashBuffer = addrObj.hashBuffer;
var hashTypeBuffer = addrObj.hashTypeBuffer;
if (options.start && options.end) {
if (options.start >= 0 && options.end >= 0) {
var endBuffer = new Buffer(4);
endBuffer.writeUInt32BE(options.end);
endBuffer.writeUInt32BE(options.end, 0);
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({
gte: Buffer.concat([
gt: Buffer.concat([
constants.PREFIXES.SPENTS,
hashBuffer,
hashTypeBuffer,
constants.SPACER_MIN,
endBuffer
]),
lte: Buffer.concat([
lt: Buffer.concat([
constants.PREFIXES.SPENTS,
hashBuffer,
hashTypeBuffer,
@ -815,8 +826,8 @@ AddressService.prototype.createInputsDBStream = function(addressStr, options) {
} else {
var allKey = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer]);
stream = this.node.services.db.store.createReadStream({
gte: Buffer.concat([allKey, constants.SPACER_MIN]),
lte: Buffer.concat([allKey, constants.SPACER_MAX]),
gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]),
lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]),
valueEncoding: 'binary',
keyEncoding: 'binary'
});
@ -903,27 +914,28 @@ AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, ha
constants.MEMPREFIXES.SPENTS,
hashBuffer,
hashTypeBuffer,
constants.TIMESTAMP_MIN
constants.SPACER_MIN
]),
lte: Buffer.concat([
constants.MEMPREFIXES.SPENTS,
hashBuffer,
hashTypeBuffer,
constants.TIMESTAMP_MAX
constants.SPACER_MAX
]),
valueEncoding: 'binary',
keyEncoding: 'binary'
});
stream.on('data', function(data) {
var timestamp = data.key.readDoubleBE(22);
var txid = data.value.slice(0, 32);
var inputIndex = data.value.readUInt32BE(32);
var timestamp = data.value.readDoubleBE(36);
var input = {
address: addressStr,
hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')],
txid: txid.toString('hex'), //TODO use a buffer
inputIndex: inputIndex,
timestamp: timestamp,
height: -1,
confirmations: 0
};
@ -979,7 +991,13 @@ AddressService.prototype.createOutputsStream = function(addressStr, options) {
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;
@ -992,22 +1010,27 @@ AddressService.prototype.createOutputsDBStream = function(addressStr, options) {
var hashTypeBuffer = addrObj.hashTypeBuffer;
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);
startBuffer.writeUInt32BE(options.start + 1);
var endBuffer = new Buffer(4);
endBuffer.writeUInt32BE(options.end);
// 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 startAdjusted = options.start + 1;
startBuffer.writeUInt32BE(startAdjusted, 0);
stream = this.node.services.db.store.createReadStream({
gte: Buffer.concat([
gt: Buffer.concat([
constants.PREFIXES.OUTPUTS,
hashBuffer,
hashTypeBuffer,
constants.SPACER_MIN,
endBuffer
]),
lte: Buffer.concat([
lt: Buffer.concat([
constants.PREFIXES.OUTPUTS,
hashBuffer,
hashTypeBuffer,
@ -1020,8 +1043,8 @@ AddressService.prototype.createOutputsDBStream = function(addressStr, options) {
} else {
var allKey = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer]);
stream = this.node.services.db.store.createReadStream({
gte: Buffer.concat([allKey, constants.SPACER_MIN]),
lte: Buffer.concat([allKey, constants.SPACER_MAX]),
gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]),
lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]),
valueEncoding: 'binary',
keyEncoding: 'binary'
});
@ -1113,13 +1136,13 @@ AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, h
constants.MEMPREFIXES.OUTPUTS,
hashBuffer,
hashTypeBuffer,
constants.TIMESTAMP_MIN
constants.SPACER_MIN
]),
lte: Buffer.concat([
constants.MEMPREFIXES.OUTPUTS,
hashBuffer,
hashTypeBuffer,
constants.TIMESTAMP_MAX
constants.SPACER_MAX
]),
valueEncoding: 'binary',
keyEncoding: 'binary'
@ -1127,18 +1150,17 @@ AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, h
stream.on('data', function(data) {
// Format of data:
// prefix: 1, hashBuffer: 20, hashTypeBuffer: 1, timestamp: 8, txid: 32, outputIndex: 4
var timestamp = data.key.readDoubleBE(22);
var txid = data.key.slice(30, 62);
var outputIndex = data.key.readUInt32BE(62);
var value = encoding.decodeOutputValue(data.value);
// prefix: 1, hashBuffer: 20, hashTypeBuffer: 1, txid: 32, outputIndex: 4
var txid = data.key.slice(22, 54);
var outputIndex = data.key.readUInt32BE(54);
var value = encoding.decodeOutputMempoolValue(data.value);
var output = {
address: addressStr,
hashType: constants.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')],
txid: txid.toString('hex'), //TODO use a buffer
outputIndex: outputIndex,
height: -1,
timestamp: timestamp,
timestamp: value.timestamp,
satoshis: value.satoshis,
script: value.scriptBuffer.toString('hex'), //TODO use a buffer
confirmations: 0
@ -1325,43 +1347,25 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb
var self = this;
var startTime = new Date();
var address = new Address(addressArg);
var tipHeight = this.node.services.db.tip.__height;
if (_.isUndefined(options.queryMempool)) {
options.queryMempool = true;
}
async.waterfall([
function(next) {
self._getAddressSummaryCache(address, next);
self._getAddressConfirmedSummary(address, options, next);
},
function(cache, next) {
self._getAddressInputsSummary(address, cache, tipHeight, 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);
self._getAddressMempoolSummary(address, options, cache, next);
}
], function(err, cache) {
if (err) {
return callback(err);
}
var result = cache.result;
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 summary = self._transformAddressSummaryFromCache(cache, options);
var timeDelta = new Date() - startTime;
if (timeDelta > 5000) {
@ -1370,52 +1374,30 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb
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);
});
};
AddressService.prototype._sortTxids = function(cache, tipHeight, callback) {
if (cache.height === tipHeight) {
return callback(null, cache);
}
cache.result.txids = Object.keys(cache.result.appearanceIds);
cache.result.txids.sort(function(a, b) {
return cache.result.appearanceIds[a] - cache.result.appearanceIds[b];
AddressService.prototype._getAddressConfirmedSummary = function(address, options, callback) {
var self = this;
var tipHeight = this.node.services.db.tip.__height;
self._getAddressConfirmedSummaryCache(address, options, function(err, cache) {
if (err) {
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) {
if (cache.height === tipHeight) {
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) {
AddressService.prototype._getAddressConfirmedSummaryCache = function(address, options, callback) {
var self = this;
var baseCache = {
result: {
appearanceIds: {},
@ -1425,6 +1407,11 @@ AddressService.prototype._getAddressSummaryCache = function(address, callback) {
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);
this.summaryCache.get(key, {
valueEncoding: 'binary',
@ -1436,25 +1423,64 @@ AddressService.prototype._getAddressSummaryCache = function(address, callback) {
return callback(err);
}
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);
});
};
AddressService.prototype._getAddressInputsSummary = function(address, cache, tipHeight, callback) {
if (cache.height === tipHeight) {
return callback(null, cache);
}
$.checkArgument(address instanceof Address);
AddressService.prototype._updateAddressConfirmedSummaryCache = function(address, options, cache, tipHeight, callback) {
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 opts = {
start: _.isUndefined(cache.height) ? 0 : cache.height + 1,
end: tipHeight
};
var inputsStream = self.createInputsStream(address, opts);
var inputsStream = self.createInputsStream(address, options);
inputsStream.on('data', function(input) {
var txid = input.txid;
cache.result.appearanceIds[txid] = input.height;
@ -1465,28 +1491,14 @@ AddressService.prototype._getAddressInputsSummary = function(address, cache, tip
});
inputsStream.on('end', function() {
var addressStr = address.toString();
var hashBuffer = address.hashBuffer;
var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type];
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);
});
if (error) {
return callback(error);
}
callback(null, cache);
});
};
AddressService.prototype._getAddressOutputsSummary = function(address, cache, tipHeight, callback) {
if (cache.height === tipHeight) {
return callback(null, cache);
}
AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, cache, options, callback) {
$.checkArgument(address instanceof Address);
$.checkArgument(!_.isUndefined(cache.result) &&
!_.isUndefined(cache.result.appearanceIds) &&
@ -1494,12 +1506,7 @@ AddressService.prototype._getAddressOutputsSummary = function(address, cache, ti
var self = this;
var opts = {
start: _.isUndefined(cache.height) ? 0 : cache.height + 1,
end: tipHeight
};
var outputStream = self.createOutputsStream(address, opts);
var outputStream = self.createOutputsStream(address, options);
outputStream.on('data', function(output) {
@ -1514,16 +1521,19 @@ AddressService.prototype._getAddressOutputsSummary = function(address, cache, ti
if (!spentDB) {
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
// we will subtract it from the unconfirmedBalance (a.k.a unconfirmedDelta)
var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(
new Buffer(txid, 'hex'), // TODO: get buffer directly
outputIndex
);
var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey];
if (spentMempool) {
cache.result.unconfirmedBalance -= output.satoshis;
if (options.queryMempool) {
// Check to see if this output is spent in the mempool and if so
// we will subtract it from the unconfirmedBalance (a.k.a unconfirmedDelta)
var spentIndexSyncKey = encoding.encodeSpentIndexSyncKey(
new Buffer(txid, 'hex'), // TODO: get buffer directly
outputIndex
);
var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey];
if (spentMempool) {
cache.result.unconfirmedBalance -= output.satoshis;
}
}
});
@ -1535,38 +1545,112 @@ AddressService.prototype._getAddressOutputsSummary = function(address, cache, ti
});
outputStream.on('end', function() {
var addressStr = address.toString();
var hashBuffer = address.hashBuffer;
var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type];
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);
});
if (error) {
return callback(error);
}
callback(null, 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;

View File

@ -54,8 +54,8 @@
"commander": "^2.8.1",
"errno": "^0.1.4",
"express": "^4.13.3",
"leveldown": "^1.4.2",
"levelup": "^1.2.1",
"leveldown": "^1.4.3",
"levelup": "^1.3.1",
"liftoff": "^2.2.0",
"memdown": "^1.0.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.options.should.equal(options);
history.addresses.should.equal(addresses);
history.transactionInfo.should.deep.equal([]);
history.combinedArray.should.deep.equal([]);
history.detailedArray.should.deep.equal([]);
});
it('will set addresses an array if only sent a string', function() {
@ -40,27 +38,29 @@ describe('Address Service History', function() {
describe('#get', function() {
it('will complete the async each limit series', function(done) {
var addresses = [address];
var summary = {
txids: []
};
var history = new AddressHistory({
node: {},
node: {
services: {
address: {
getAddressSummary: sinon.stub().callsArgWith(2, null, summary)
}
}
},
options: {},
addresses: addresses
});
var 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.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.combineTransactionInfo.callCount.should.equal(1);
history.sortAndPaginateCombinedArray.callCount.should.equal(1);
results.should.deep.equal({
totalCount: 1,
items: expected
@ -78,149 +78,15 @@ describe('Address Service History', function() {
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() {
describe('#_mergeAndSortTxids', function() {
it('will sort latest to oldest using height', function() {
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() {
it('will add additional information to existing this.transactions', function() {
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
@ -602,7 +343,7 @@ describe('Address Service History', function() {
}
},
options: {},
addresses: []
addresses: [txAddress]
});
var transactionInfo = {
addresses: {},
@ -614,7 +355,7 @@ describe('Address Service History', function() {
transactionInfo.addresses[txAddress] = {};
transactionInfo.addresses[txAddress].outputIndexes = [1];
transactionInfo.addresses[txAddress].inputIndexes = [];
history.getDetailedInfo(transactionInfo, function(err) {
history.getDetailedInfo(txid, function(err) {
if (err) {
throw err;
}
@ -653,28 +394,4 @@ describe('Address Service History', function() {
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