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:
parent
188ff28ec7
commit
4fcec8755c
@ -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) {
|
||||||
|
|||||||
@ -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');
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
103
test/services/address/encoding.unit.js
Normal file
103
test/services/address/encoding.unit.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@ -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
Loading…
Reference in New Issue
Block a user