Wip on block service.
This commit is contained in:
parent
fe6896ba98
commit
a8760b3451
83
child-pay-for-parent.js
Normal file
83
child-pay-for-parent.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
var unmentionables = require('./keys.json');
|
||||||
|
var assert = require('assert');
|
||||||
|
var p2p = require('bitcore-p2p');
|
||||||
|
var Peer = p2p.Peer;
|
||||||
|
var messages = new p2p.Messages();
|
||||||
|
|
||||||
|
var peer = new Peer({host: '68.101.164.118'});
|
||||||
|
|
||||||
|
// 1. prepare a low fee ts.
|
||||||
|
// 2. send low fee tx. The low fee tx should be enough to be relayed
|
||||||
|
// 3. 20 - 50 sats/byte is what we usually see
|
||||||
|
// 4. later, spend those outputs using an approproate fee
|
||||||
|
|
||||||
|
var bitcore = require('bitcore-lib');
|
||||||
|
var key1 = new bitcore.PrivateKey(unmentionables.key1);
|
||||||
|
var key2 = new bitcore.PrivateKey(unmentionables.key2);
|
||||||
|
var lowFeeRate = 40; //sat/byte
|
||||||
|
var highFeeRate = 260;
|
||||||
|
|
||||||
|
var parentUtxo = {
|
||||||
|
txid: '100304043f19ea9c4faf0810c9432b806cf383de38d9138b004c8a8df7f76249',
|
||||||
|
outputIndex: 0,
|
||||||
|
address: '1DxgVtn7xUwX9Jwqx7YW7JfsDDDVHDxTwL',
|
||||||
|
script: '76a9148e295bd3b705aac6ba0cb02bb582f98c451b83ee88ac',
|
||||||
|
satoshis: 17532710
|
||||||
|
};
|
||||||
|
|
||||||
|
var parentTx = new bitcore.Transaction();
|
||||||
|
|
||||||
|
var lowFee = lowFeeRate*193;
|
||||||
|
var highFee = highFeeRate*193;
|
||||||
|
var childToAddress = '12Awugz6fhM2BW4dH7Xx1ZKxy3CHWM6a8f';
|
||||||
|
|
||||||
|
parentTx.from(parentUtxo).to(childToAddress, (parentUtxo.satoshis - lowFee)).fee(lowFee).sign(key1);
|
||||||
|
console.log(parentTx.getFee());
|
||||||
|
console.log(parentTx.verify());
|
||||||
|
assert((parentTx.inputs[0].output.satoshis - parentTx.outputs[0].satoshis) === parentTx.getFee());
|
||||||
|
|
||||||
|
console.log(parentTx.toObject());
|
||||||
|
console.log(parentTx.serialize());
|
||||||
|
|
||||||
|
peer.on('ready', function() {
|
||||||
|
console.log(peer.version, peer.subversion, peer.bestHeight);
|
||||||
|
setTimeout(function() {
|
||||||
|
peer.sendMessage(messages.Transaction(parentTx));
|
||||||
|
setTimeout(function() { peer.disconnect(); }, 2000);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
peer.on('disconnect', function() {
|
||||||
|
console.log('connection closed');
|
||||||
|
});
|
||||||
|
peer.connect();
|
||||||
|
//var childUtxo = {
|
||||||
|
// txid: parentTx.id,
|
||||||
|
// outputIndex: 0,
|
||||||
|
// address: childToAddress,
|
||||||
|
// script: parentTx.outputs[0].script.toHex(),
|
||||||
|
// satoshis: (parentUtxo.satoshis - lowFee)
|
||||||
|
//};
|
||||||
|
//
|
||||||
|
//var childTx = new bitcore.Transaction();
|
||||||
|
//childTx.from(childUtxo).to(childToAddress, (childUtxo.satoshis - highFee)).fee(highFee).sign(key2);
|
||||||
|
//console.log(childTx.getFee());
|
||||||
|
//console.log(childTx.toObject());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//01000000
|
||||||
|
//01
|
||||||
|
//49
|
||||||
|
//62f7f78d8a4c008b13d938de83f36c802b43c91008af4f9cea193f04040310000000006a47304402200c98dee6e5a2db276e24ac45c153aa5586455894efee060e95e0e7d017569df30220258b48ebd0253ca6b4b8b35d8f18b4bcb41040fb060f1ed59f7989185dd296170121031c8ed8aead402b7f6e02617d50b7a7ba3b07a489da0702f65f985bf0ebb64f3a
|
||||||
|
//ffffffff
|
||||||
|
//01
|
||||||
|
//26
|
||||||
|
//87
|
||||||
|
//0b01000000001976a9140cd9b466eb74f45e3290b47fbbb622e458601267
|
||||||
|
//88
|
||||||
|
//ac
|
||||||
|
//00000000
|
||||||
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var exports = {};
|
|
||||||
|
|
||||||
exports.PREFIXES = {
|
|
||||||
ADDRESS: new Buffer('02', 'hex'),
|
|
||||||
UNSPENT: new Buffer('03', 'hex'),
|
|
||||||
// OUTPUTS: new Buffer('02', 'hex'), // Query outputs by address and/or height
|
|
||||||
// SPENTS: new Buffer('03', 'hex'), // Query inputs by address and/or height
|
|
||||||
// SPENTSMAP: new Buffer('05', 'hex') // Get the input that spends an output
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.MEMPREFIXES = {
|
|
||||||
OUTPUTS: new Buffer('01', 'hex'), // Query mempool outputs by address
|
|
||||||
SPENTS: new Buffer('02', 'hex'), // Query mempool inputs by address
|
|
||||||
SPENTSMAP: new Buffer('03', 'hex') // Query mempool for the input that spends an output
|
|
||||||
};
|
|
||||||
|
|
||||||
// To save space, we're only storing the PubKeyHash or ScriptHash in our index.
|
|
||||||
// To avoid intentional unspendable collisions, which have been seen on the blockchain,
|
|
||||||
// we must store the hash type (PK or Script) as well.
|
|
||||||
exports.HASH_TYPES = {
|
|
||||||
PUBKEY: new Buffer('01', 'hex'),
|
|
||||||
REDEEMSCRIPT: new Buffer('02', 'hex')
|
|
||||||
};
|
|
||||||
|
|
||||||
// Translates from our enum type back into the hash types returned by
|
|
||||||
// bitcore-lib/address.
|
|
||||||
exports.HASH_TYPES_READABLE = {
|
|
||||||
'01': 'pubkeyhash',
|
|
||||||
'02': 'scripthash'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.HASH_TYPES_MAP = {
|
|
||||||
'pubkeyhash': exports.HASH_TYPES.PUBKEY,
|
|
||||||
'scripthash': exports.HASH_TYPES.REDEEMSCRIPT
|
|
||||||
};
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
// The maximum number of inputs that can be queried at once
|
|
||||||
exports.MAX_INPUTS_QUERY_LENGTH = 50000;
|
|
||||||
// The maximum number of outputs that can be queried at once
|
|
||||||
exports.MAX_OUTPUTS_QUERY_LENGTH = 50000;
|
|
||||||
// The maximum number of transactions that can be queried at once
|
|
||||||
exports.MAX_HISTORY_QUERY_LENGTH = 100;
|
|
||||||
// The maximum number of addresses that can be queried at once
|
|
||||||
exports.MAX_ADDRESSES_QUERY = 10000;
|
|
||||||
// The maximum number of simultaneous requests
|
|
||||||
exports.MAX_ADDRESSES_LIMIT = 5;
|
|
||||||
|
|
||||||
module.exports = exports;
|
|
||||||
|
|
||||||
@ -8,32 +8,16 @@ var index = require('../../');
|
|||||||
var log = index.log;
|
var log = index.log;
|
||||||
var errors = index.errors;
|
var errors = index.errors;
|
||||||
var bitcore = require('bitcore-lib');
|
var bitcore = require('bitcore-lib');
|
||||||
var levelup = require('levelup');
|
|
||||||
var $ = bitcore.util.preconditions;
|
var $ = bitcore.util.preconditions;
|
||||||
var _ = bitcore.deps._;
|
var _ = bitcore.deps._;
|
||||||
var EventEmitter = require('events').EventEmitter;
|
var EventEmitter = require('events').EventEmitter;
|
||||||
var Address = bitcore.Address;
|
var Address = bitcore.Address;
|
||||||
var constants = require('../../constants');
|
|
||||||
var Encoding = require('./encoding');
|
var Encoding = require('./encoding');
|
||||||
var utils = require('../../utils');
|
var utils = require('../../utils');
|
||||||
|
|
||||||
/**
|
|
||||||
* The Address Service builds upon the Database Service and the Bitcoin Service to add additional
|
|
||||||
* functionality for getting information by base58check encoded addresses. This includes getting the
|
|
||||||
* balance for an address, the history for a collection of addresses, and unspent outputs for
|
|
||||||
* constructing transactions. This is typically the core functionality for building a wallet.
|
|
||||||
* @param {Object} options
|
|
||||||
* @param {Node} options.node - An instance of the node
|
|
||||||
* @param {String} options.name - An optional name of the service
|
|
||||||
*/
|
|
||||||
var AddressService = function(options) {
|
var AddressService = function(options) {
|
||||||
BaseService.call(this, options);
|
BaseService.call(this, options);
|
||||||
|
|
||||||
this.maxInputsQueryLength = options.maxInputsQueryLength || constants.MAX_INPUTS_QUERY_LENGTH;
|
|
||||||
this.maxOutputsQueryLength = options.maxOutputsQueryLength || constants.MAX_OUTPUTS_QUERY_LENGTH;
|
|
||||||
|
|
||||||
this.concurrency = options.concurrency || 20;
|
this.concurrency = options.concurrency || 20;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inherits(AddressService, BaseService);
|
inherits(AddressService, BaseService);
|
||||||
@ -61,17 +45,9 @@ AddressService.prototype.start = function(callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype.stop = function(callback) {
|
AddressService.prototype.stop = function(callback) {
|
||||||
// TODO Keep track of ongoing db requests before shutting down
|
|
||||||
// this.node.services.bitcoind.removeListener('tx', this._bitcoindTransactionListener);
|
|
||||||
// this.node.services.bitcoind.removeListener('txleave', this._bitcoindTransactionLeaveListener);
|
|
||||||
// this.mempoolIndex.close(callback);
|
|
||||||
setImmediate(callback);
|
setImmediate(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the Node to get the available API methods for this service,
|
|
||||||
* that can be exposed over the JSON-RPC interface.
|
|
||||||
*/
|
|
||||||
AddressService.prototype.getAPIMethods = function() {
|
AddressService.prototype.getAPIMethods = function() {
|
||||||
return [
|
return [
|
||||||
['getBalance', this, this.getBalance, 2],
|
['getBalance', this, this.getBalance, 2],
|
||||||
@ -84,34 +60,10 @@ AddressService.prototype.getAPIMethods = function() {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by the Bus to get the available events for this service.
|
|
||||||
*/
|
|
||||||
AddressService.prototype.getPublishEvents = function() {
|
AddressService.prototype.getPublishEvents = function() {
|
||||||
return [];
|
return [];
|
||||||
// return [
|
|
||||||
// {
|
|
||||||
// name: 'address/transaction',
|
|
||||||
// scope: this,
|
|
||||||
// subscribe: this.subscribe.bind(this, 'address/transaction'),
|
|
||||||
// unsubscribe: this.unsubscribe.bind(this, 'address/transaction')
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'address/balance',
|
|
||||||
// scope: this,
|
|
||||||
// subscribe: this.subscribe.bind(this, 'address/balance'),
|
|
||||||
// unsubscribe: this.unsubscribe.bind(this, 'address/balance')
|
|
||||||
// }
|
|
||||||
// ];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* The Database Service will run this function when blocks are connected and
|
|
||||||
* disconnected to the chain during syncing and reorganizations.
|
|
||||||
* @param {Block} block - An instance of a Bitcore Block
|
|
||||||
* @param {Boolean} addOutput - If the block is being removed or added to the chain
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.concurrentBlockHandler = function(block, connectBlock, callback) {
|
AddressService.prototype.concurrentBlockHandler = function(block, connectBlock, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@ -481,478 +433,7 @@ AddressService.prototype.getBalance = function(address, queryMempool, callback)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Will give the input that spends an output if it exists with:
|
|
||||||
* inputTxId - The input txid hex string
|
|
||||||
* inputIndex - A number with the spending input index
|
|
||||||
* @param {String|Buffer} txid - The transaction hash with the output
|
|
||||||
* @param {Number} outputIndex - The output index in the transaction
|
|
||||||
* @param {Object} options
|
|
||||||
* @param {Object} options.queryMempool - Include mempool in results
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.getInputForOutput = function(txid, outputIndex, options, callback) {
|
|
||||||
$.checkArgument(_.isNumber(outputIndex));
|
|
||||||
$.checkArgument(_.isObject(options));
|
|
||||||
$.checkArgument(_.isFunction(callback));
|
|
||||||
var self = this;
|
|
||||||
var txidBuffer;
|
|
||||||
if (Buffer.isBuffer(txid)) {
|
|
||||||
txidBuffer = txid;
|
|
||||||
} else {
|
|
||||||
txidBuffer = new Buffer(txid, 'hex');
|
|
||||||
}
|
|
||||||
if (options.queryMempool) {
|
|
||||||
var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey(txidBuffer, outputIndex);
|
|
||||||
if (this.mempoolSpentIndex[spentIndexSyncKey]) {
|
|
||||||
return this._getSpentMempool(txidBuffer, outputIndex, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var key = self._encoding.encodeInputKeyMap(txidBuffer, outputIndex);
|
|
||||||
var dbOptions = {
|
|
||||||
valueEncoding: 'binary',
|
|
||||||
keyEncoding: 'binary'
|
|
||||||
};
|
|
||||||
this.db.get(key, dbOptions, function(err, buffer) {
|
|
||||||
if (err instanceof levelup.errors.NotFoundError) {
|
|
||||||
return callback(null, false);
|
|
||||||
} else if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
var value = self._encoding.decodeInputValueMap(buffer);
|
|
||||||
callback(null, {
|
|
||||||
inputTxId: value.inputTxId.toString('hex'),
|
|
||||||
inputIndex: value.inputIndex
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A streaming equivalent to `getInputs`, and returns a transform stream with data
|
|
||||||
* emitted in the same format as `getInputs`.
|
|
||||||
*
|
|
||||||
* @param {String} addressStr - The relevant address
|
|
||||||
* @param {Object} options - Additional options for query the outputs
|
|
||||||
* @param {Number} [options.start] - The relevant start block height
|
|
||||||
* @param {Number} [options.end] - The relevant end block height
|
|
||||||
* @param {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)
|
|
||||||
.on('error', function(err) {
|
|
||||||
// Forward the error
|
|
||||||
inputStream.emit('error', err);
|
|
||||||
inputStream.end();
|
|
||||||
}).pipe(inputStream);
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype.createInputsDBStream = function(addressStr, options) {
|
|
||||||
var stream;
|
|
||||||
var addrObj = this.encoding.getAddressInfo(addressStr);
|
|
||||||
var hashBuffer = addrObj.hashBuffer;
|
|
||||||
var hashTypeBuffer = addrObj.hashTypeBuffer;
|
|
||||||
|
|
||||||
if (options.start >= 0 && options.end >= 0) {
|
|
||||||
|
|
||||||
var endBuffer = new Buffer(4);
|
|
||||||
endBuffer.writeUInt32BE(options.end, 0);
|
|
||||||
|
|
||||||
var startBuffer = new Buffer(4);
|
|
||||||
// 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.db.createReadStream({
|
|
||||||
gt: Buffer.concat([
|
|
||||||
constants.PREFIXES.SPENTS,
|
|
||||||
hashBuffer,
|
|
||||||
hashTypeBuffer,
|
|
||||||
constants.SPACER_MIN,
|
|
||||||
endBuffer
|
|
||||||
]),
|
|
||||||
lt: Buffer.concat([
|
|
||||||
constants.PREFIXES.SPENTS,
|
|
||||||
hashBuffer,
|
|
||||||
hashTypeBuffer,
|
|
||||||
constants.SPACER_MIN,
|
|
||||||
startBuffer
|
|
||||||
]),
|
|
||||||
valueEncoding: 'binary',
|
|
||||||
keyEncoding: 'binary'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var allKey = Buffer.concat([constants.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer]);
|
|
||||||
stream = this.db.createReadStream({
|
|
||||||
gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]),
|
|
||||||
lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]),
|
|
||||||
valueEncoding: 'binary',
|
|
||||||
keyEncoding: 'binary'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will give inputs that spend previous outputs for an address as an object with:
|
|
||||||
* address - The base58check encoded address
|
|
||||||
* hashtype - The type of the address, e.g. 'pubkeyhash' or 'scripthash'
|
|
||||||
* txid - A string of the transaction hash
|
|
||||||
* outputIndex - A number of corresponding transaction input
|
|
||||||
* height - The height of the block the transaction was included, will be -1 for mempool transactions
|
|
||||||
* confirmations - The number of confirmations, will equal 0 for mempool transactions
|
|
||||||
*
|
|
||||||
* @param {String} addressStr - The relevant address
|
|
||||||
* @param {Object} options - Additional options for query the outputs
|
|
||||||
* @param {Number} [options.start] - The relevant start block height
|
|
||||||
* @param {Number} [options.end] - The relevant end block height
|
|
||||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the results
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.getInputs = function(addressStr, options, callback) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var inputs = [];
|
|
||||||
|
|
||||||
var addrObj = self._encoding.getAddressInfo(addressStr);
|
|
||||||
var hashBuffer = addrObj.hashBuffer;
|
|
||||||
var hashTypeBuffer = addrObj.hashTypeBuffer;
|
|
||||||
|
|
||||||
var stream = this.createInputsStream(addressStr, options);
|
|
||||||
var error;
|
|
||||||
|
|
||||||
stream.on('data', function(input) {
|
|
||||||
inputs.push(input);
|
|
||||||
if (inputs.length > self.maxInputsQueryLength) {
|
|
||||||
log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for address '+ addressStr);
|
|
||||||
error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached');
|
|
||||||
stream.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('error', function(streamError) {
|
|
||||||
if (streamError) {
|
|
||||||
error = streamError;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('finish', function() {
|
|
||||||
if (error) {
|
|
||||||
return callback(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(options.queryMempool) {
|
|
||||||
self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
inputs = inputs.concat(mempoolInputs);
|
|
||||||
callback(null, inputs);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback(null, inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) {
|
|
||||||
var self = this;
|
|
||||||
var mempoolInputs = [];
|
|
||||||
|
|
||||||
var stream = self.mempoolIndex.createReadStream({
|
|
||||||
gte: Buffer.concat([
|
|
||||||
constants.MEMPREFIXES.SPENTS,
|
|
||||||
hashBuffer,
|
|
||||||
hashTypeBuffer,
|
|
||||||
constants.SPACER_MIN
|
|
||||||
]),
|
|
||||||
lte: Buffer.concat([
|
|
||||||
constants.MEMPREFIXES.SPENTS,
|
|
||||||
hashBuffer,
|
|
||||||
hashTypeBuffer,
|
|
||||||
constants.SPACER_MAX
|
|
||||||
]),
|
|
||||||
valueEncoding: 'binary',
|
|
||||||
keyEncoding: 'binary'
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('data', function(data) {
|
|
||||||
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
|
|
||||||
};
|
|
||||||
mempoolInputs.push(input);
|
|
||||||
});
|
|
||||||
|
|
||||||
var error;
|
|
||||||
|
|
||||||
stream.on('error', function(streamError) {
|
|
||||||
if (streamError) {
|
|
||||||
error = streamError;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('close', function() {
|
|
||||||
if (error) {
|
|
||||||
return callback(error);
|
|
||||||
}
|
|
||||||
callback(null, mempoolInputs);
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype._getSpentMempool = function(txidBuffer, outputIndex, callback) {
|
|
||||||
var outputIndexBuffer = new Buffer(4);
|
|
||||||
outputIndexBuffer.writeUInt32BE(outputIndex);
|
|
||||||
var spentIndexKey = Buffer.concat([
|
|
||||||
constants.MEMPREFIXES.SPENTSMAP,
|
|
||||||
txidBuffer,
|
|
||||||
outputIndexBuffer
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.mempoolIndex.get(
|
|
||||||
spentIndexKey,
|
|
||||||
function(err, mempoolValue) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
var inputTxId = mempoolValue.slice(0, 32);
|
|
||||||
var inputIndex = mempoolValue.readUInt32BE(32);
|
|
||||||
callback(null, {
|
|
||||||
inputTxId: inputTxId.toString('hex'),
|
|
||||||
inputIndex: inputIndex
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype.createOutputsStream = function(addressStr, options) {
|
|
||||||
var outputStream = new OutputsTransformStream({
|
|
||||||
address: new Address(addressStr, this.node.network),
|
|
||||||
tipHeight: this.node.services.db.tip.__height
|
|
||||||
});
|
|
||||||
|
|
||||||
var stream = this.createOutputsDBStream(addressStr, options)
|
|
||||||
.on('error', function(err) {
|
|
||||||
// Forward the error
|
|
||||||
outputStream.emit('error', err);
|
|
||||||
outputStream.end();
|
|
||||||
})
|
|
||||||
.pipe(outputStream);
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype.createOutputsDBStream = function(addressStr, options) {
|
|
||||||
|
|
||||||
var addrObj = this.encoding.getAddressInfo(addressStr);
|
|
||||||
var hashBuffer = addrObj.hashBuffer;
|
|
||||||
var hashTypeBuffer = addrObj.hashTypeBuffer;
|
|
||||||
var stream;
|
|
||||||
|
|
||||||
if (options.start >= 0 && options.end >= 0) {
|
|
||||||
|
|
||||||
var endBuffer = new Buffer(4);
|
|
||||||
endBuffer.writeUInt32BE(options.end, 0);
|
|
||||||
|
|
||||||
var startBuffer = new Buffer(4);
|
|
||||||
// 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.db.createReadStream({
|
|
||||||
gt: Buffer.concat([
|
|
||||||
constants.PREFIXES.OUTPUTS,
|
|
||||||
hashBuffer,
|
|
||||||
hashTypeBuffer,
|
|
||||||
constants.SPACER_MIN,
|
|
||||||
endBuffer
|
|
||||||
]),
|
|
||||||
lt: Buffer.concat([
|
|
||||||
constants.PREFIXES.OUTPUTS,
|
|
||||||
hashBuffer,
|
|
||||||
hashTypeBuffer,
|
|
||||||
constants.SPACER_MIN,
|
|
||||||
startBuffer
|
|
||||||
]),
|
|
||||||
valueEncoding: 'binary',
|
|
||||||
keyEncoding: 'binary'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var allKey = Buffer.concat([constants.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer]);
|
|
||||||
stream = this.db.createReadStream({
|
|
||||||
gt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MIN]),
|
|
||||||
lt: Buffer.concat([allKey, constants.SPACER_HEIGHT_MAX]),
|
|
||||||
valueEncoding: 'binary',
|
|
||||||
keyEncoding: 'binary'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will give outputs for an address as an object with:
|
|
||||||
* address - The base58check encoded address
|
|
||||||
* hashtype - The type of the address, e.g. 'pubkeyhash' or 'scripthash'
|
|
||||||
* txid - A string of the transaction hash
|
|
||||||
* outputIndex - A number of corresponding transaction output
|
|
||||||
* height - The height of the block the transaction was included, will be -1 for mempool transactions
|
|
||||||
* satoshis - The satoshis value of the output
|
|
||||||
* script - The script of the output as a hex string
|
|
||||||
* confirmations - The number of confirmations, will equal 0 for mempool transactions
|
|
||||||
*
|
|
||||||
* @param {String} addressStr - The relevant address
|
|
||||||
* @param {Object} options - Additional options for query the outputs
|
|
||||||
* @param {Number} [options.start] - The relevant start block height
|
|
||||||
* @param {Number} [options.end] - The relevant end block height
|
|
||||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the results
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.getOutputs = function(addressStr, options, callback) {
|
|
||||||
var self = this;
|
|
||||||
$.checkArgument(_.isObject(options), 'Second argument is expected to be an options object.');
|
|
||||||
$.checkArgument(_.isFunction(callback), 'Third argument is expected to be a callback function.');
|
|
||||||
|
|
||||||
var addrObj = self._encoding.getAddressInfo(addressStr);
|
|
||||||
var hashBuffer = addrObj.hashBuffer;
|
|
||||||
var hashTypeBuffer = addrObj.hashTypeBuffer;
|
|
||||||
if (!hashTypeBuffer) {
|
|
||||||
return callback(new Error('Unknown address type: ' + addrObj.hashTypeReadable + ' for address: ' + addressStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputs = [];
|
|
||||||
var stream = this.createOutputsStream(addressStr, options);
|
|
||||||
var error;
|
|
||||||
|
|
||||||
stream.on('data', function(data) {
|
|
||||||
outputs.push(data);
|
|
||||||
if (outputs.length > self.maxOutputsQueryLength) {
|
|
||||||
log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for address ' + addressStr);
|
|
||||||
error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached');
|
|
||||||
stream.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('error', function(streamError) {
|
|
||||||
if (streamError) {
|
|
||||||
error = streamError;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('finish', function() {
|
|
||||||
if (error) {
|
|
||||||
return callback(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(options.queryMempool) {
|
|
||||||
self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
outputs = outputs.concat(mempoolOutputs);
|
|
||||||
callback(null, outputs);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback(null, outputs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) {
|
|
||||||
var self = this;
|
|
||||||
var mempoolOutputs = [];
|
|
||||||
|
|
||||||
var stream = self.mempoolIndex.createReadStream({
|
|
||||||
gte: Buffer.concat([
|
|
||||||
constants.MEMPREFIXES.OUTPUTS,
|
|
||||||
hashBuffer,
|
|
||||||
hashTypeBuffer,
|
|
||||||
constants.SPACER_MIN
|
|
||||||
]),
|
|
||||||
lte: Buffer.concat([
|
|
||||||
constants.MEMPREFIXES.OUTPUTS,
|
|
||||||
hashBuffer,
|
|
||||||
hashTypeBuffer,
|
|
||||||
constants.SPACER_MAX
|
|
||||||
]),
|
|
||||||
valueEncoding: 'binary',
|
|
||||||
keyEncoding: 'binary'
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('data', function(data) {
|
|
||||||
// Format of data:
|
|
||||||
// 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 = self._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: value.timestamp,
|
|
||||||
satoshis: value.satoshis,
|
|
||||||
script: value.scriptBuffer.toString('hex'), //TODO use a buffer
|
|
||||||
confirmations: 0
|
|
||||||
};
|
|
||||||
mempoolOutputs.push(output);
|
|
||||||
});
|
|
||||||
|
|
||||||
var error;
|
|
||||||
|
|
||||||
stream.on('error', function(streamError) {
|
|
||||||
if (streamError) {
|
|
||||||
error = streamError;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('close', function() {
|
|
||||||
if (error) {
|
|
||||||
return callback(error);
|
|
||||||
}
|
|
||||||
callback(null, mempoolOutputs);
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will give unspent outputs for an address or an array of addresses.
|
|
||||||
* @param {Array|String} addresses - An array of addresses
|
|
||||||
* @param {Boolean} queryMempool - Include or exclude the mempool
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) {
|
AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@ -978,12 +459,6 @@ AddressService.prototype.getUtxos = function(addresses, queryMempool, callback)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Will give unspent outputs for an address.
|
|
||||||
* @param {String} address - An address in base58check encoding
|
|
||||||
* @param {Boolean} queryMempool - Include or exclude the mempool
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.getUtxosForAddress = function(address, queryMempool, callback) {
|
AddressService.prototype.getUtxosForAddress = function(address, queryMempool, callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
@ -1017,13 +492,6 @@ AddressService.prototype.getUtxosForAddress = function(address, queryMempool, ca
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Will give the inverse of isSpent
|
|
||||||
* @param {Object} output
|
|
||||||
* @param {Object} options
|
|
||||||
* @param {Boolean} options.queryMempool - Include mempool in results
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.isUnspent = function(output, options, callback) {
|
AddressService.prototype.isUnspent = function(output, options, callback) {
|
||||||
$.checkArgument(_.isFunction(callback));
|
$.checkArgument(_.isFunction(callback));
|
||||||
this.isSpent(output, options, function(spent) {
|
this.isSpent(output, options, function(spent) {
|
||||||
@ -1031,64 +499,6 @@ AddressService.prototype.isUnspent = function(output, options, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Will determine if an output is spent.
|
|
||||||
* @param {Object} output - An output as returned from getOutputs
|
|
||||||
* @param {Object} options
|
|
||||||
* @param {Boolean} options.queryMempool - Include mempool in results
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.isSpent = function(output, options, callback) {
|
|
||||||
$.checkArgument(_.isFunction(callback));
|
|
||||||
var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool;
|
|
||||||
var self = this;
|
|
||||||
var txid = output.prevTxId ? output.prevTxId.toString('hex') : output.txid;
|
|
||||||
var spent = self.node.services.bitcoind.isSpent(txid, output.outputIndex);
|
|
||||||
if (!spent && queryMempool) {
|
|
||||||
var txidBuffer = new Buffer(txid, 'hex');
|
|
||||||
var spentIndexSyncKey = self._encoding.encodeSpentIndexSyncKey(txidBuffer, output.outputIndex);
|
|
||||||
spent = self.mempoolSpentIndex[spentIndexSyncKey] ? true : false;
|
|
||||||
}
|
|
||||||
setImmediate(function() {
|
|
||||||
// TODO error should be the first argument?
|
|
||||||
callback(spent);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will give the history for many addresses limited by a range of block heights (to limit
|
|
||||||
* the database lookup times) and/or paginated to limit the results length.
|
|
||||||
*
|
|
||||||
* The response format will be:
|
|
||||||
* {
|
|
||||||
* totalCount: 12 // the total number of items there are between the two heights
|
|
||||||
* items: [
|
|
||||||
* {
|
|
||||||
* addresses: {
|
|
||||||
* '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX': {
|
|
||||||
* inputIndexes: [],
|
|
||||||
* outputIndexes: [0]
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* satoshis: 100,
|
|
||||||
* height: 300000,
|
|
||||||
* confirmations: 1,
|
|
||||||
* timestamp: 1442337090 // in seconds
|
|
||||||
* fees: 1000 // in satoshis
|
|
||||||
* tx: <Transaction>
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* @param {Array} addresses - An array of addresses
|
|
||||||
* @param {Object} options - The options to limit the query
|
|
||||||
* @param {Number} [options.from] - The pagination "from" index
|
|
||||||
* @param {Number} [options.to] - The pagination "to" index
|
|
||||||
* @param {Number} [options.start] - The beginning block height (e.g. 1500 the most recent block height).
|
|
||||||
* @param {Number} [options.end] - The ending block height (e.g. 0 the older block height, results are inclusive).
|
|
||||||
* @param {Boolean} [options.queryMempool] - Include the mempool in the query
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
|
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@ -1238,219 +648,5 @@ AddressService.prototype.getAddressSummary = function(addressArg, options, callb
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype._getAddressConfirmedSummary = function(address, options, callback) {
|
|
||||||
var self = this;
|
|
||||||
var baseResult = {
|
|
||||||
appearanceIds: {},
|
|
||||||
totalReceived: 0,
|
|
||||||
balance: 0,
|
|
||||||
unconfirmedAppearanceIds: {},
|
|
||||||
unconfirmedBalance: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function(next) {
|
|
||||||
self._getAddressConfirmedInputsSummary(address, baseResult, options, next);
|
|
||||||
},
|
|
||||||
function(result, next) {
|
|
||||||
self._getAddressConfirmedOutputsSummary(address, result, options, next);
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype._getAddressConfirmedInputsSummary = function(address, result, options, callback) {
|
|
||||||
$.checkArgument(address instanceof Address);
|
|
||||||
var self = this;
|
|
||||||
var error = null;
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
var inputsStream = self.createInputsStream(address, options);
|
|
||||||
inputsStream.on('data', function(input) {
|
|
||||||
var txid = input.txid;
|
|
||||||
result.appearanceIds[txid] = input.height;
|
|
||||||
|
|
||||||
count++;
|
|
||||||
|
|
||||||
if (count > self.maxInputsQueryLength) {
|
|
||||||
log.warn('Tried to query too many inputs (' + self.maxInputsQueryLength + ') for summary of address ' + address.toString());
|
|
||||||
error = new Error('Maximum number of inputs (' + self.maxInputsQueryLength + ') per query reached');
|
|
||||||
inputsStream.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
inputsStream.on('error', function(err) {
|
|
||||||
error = err;
|
|
||||||
});
|
|
||||||
|
|
||||||
inputsStream.on('end', function() {
|
|
||||||
if (error) {
|
|
||||||
return callback(error);
|
|
||||||
}
|
|
||||||
callback(null, result);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype._getAddressConfirmedOutputsSummary = function(address, result, options, callback) {
|
|
||||||
$.checkArgument(address instanceof Address);
|
|
||||||
$.checkArgument(!_.isUndefined(result) &&
|
|
||||||
!_.isUndefined(result.appearanceIds) &&
|
|
||||||
!_.isUndefined(result.unconfirmedAppearanceIds));
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
var outputStream = self.createOutputsStream(address, options);
|
|
||||||
var error = null;
|
|
||||||
|
|
||||||
outputStream.on('data', function(output) {
|
|
||||||
|
|
||||||
var txid = output.txid;
|
|
||||||
var outputIndex = output.outputIndex;
|
|
||||||
result.totalReceived += output.satoshis;
|
|
||||||
result.appearanceIds[txid] = output.height;
|
|
||||||
|
|
||||||
if(!options.noBalance) {
|
|
||||||
|
|
||||||
// Bitcoind's isSpent only works for confirmed transactions
|
|
||||||
var spentDB = self.node.services.bitcoind.isSpent(txid, outputIndex);
|
|
||||||
|
|
||||||
if(!spentDB) {
|
|
||||||
result.balance += 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 = self._encoding.encodeSpentIndexSyncKey(
|
|
||||||
new Buffer(txid, 'hex'), // TODO: get buffer directly
|
|
||||||
outputIndex
|
|
||||||
);
|
|
||||||
var spentMempool = self.mempoolSpentIndex[spentIndexSyncKey];
|
|
||||||
if(spentMempool) {
|
|
||||||
result.unconfirmedBalance -= output.satoshis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
count++;
|
|
||||||
|
|
||||||
if (count > self.maxOutputsQueryLength) {
|
|
||||||
log.warn('Tried to query too many outputs (' + self.maxOutputsQueryLength + ') for summary of address ' + address.toString());
|
|
||||||
error = new Error('Maximum number of outputs (' + self.maxOutputsQueryLength + ') per query reached');
|
|
||||||
outputStream.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
outputStream.on('error', function(err) {
|
|
||||||
error = err;
|
|
||||||
});
|
|
||||||
|
|
||||||
outputStream.on('end', function() {
|
|
||||||
if (error) {
|
|
||||||
return callback(error);
|
|
||||||
}
|
|
||||||
callback(null, result);
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype._setAndSortTxidsFromAppearanceIds = function(result, callback) {
|
|
||||||
result.txids = Object.keys(result.appearanceIds);
|
|
||||||
result.txids.sort(function(a, b) {
|
|
||||||
return result.appearanceIds[a] - result.appearanceIds[b];
|
|
||||||
});
|
|
||||||
result.unconfirmedTxids = Object.keys(result.unconfirmedAppearanceIds);
|
|
||||||
result.unconfirmedTxids.sort(function(a, b) {
|
|
||||||
return result.unconfirmedAppearanceIds[a] - result.unconfirmedAppearanceIds[b];
|
|
||||||
});
|
|
||||||
callback(null, result);
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype._getAddressMempoolSummary = function(address, options, result, callback) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Skip if the options do not want to include the mempool
|
|
||||||
if (!options.queryMempool) {
|
|
||||||
return callback(null, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
var addressStr = address.toString();
|
|
||||||
var hashBuffer = address.hashBuffer;
|
|
||||||
var hashTypeBuffer = constants.HASH_TYPES_MAP[address.type];
|
|
||||||
var addressIndexKey = self._encoding.encodeMempoolAddressIndexKey(hashBuffer, hashTypeBuffer);
|
|
||||||
|
|
||||||
if(!this.mempoolAddressIndex[addressIndexKey]) {
|
|
||||||
return callback(null, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
result.unconfirmedAppearanceIds[input.txid] = input.timestamp;
|
|
||||||
}
|
|
||||||
next(null, result);
|
|
||||||
});
|
|
||||||
|
|
||||||
}, function(result, 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];
|
|
||||||
|
|
||||||
result.unconfirmedAppearanceIds[output.txid] = output.timestamp;
|
|
||||||
|
|
||||||
if(!options.noBalance) {
|
|
||||||
var spentIndexSyncKey = self._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) {
|
|
||||||
result.unconfirmedBalance += output.satoshis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next(null, result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype._transformAddressSummaryFromResult = function(result, options) {
|
|
||||||
|
|
||||||
var confirmedTxids = result.txids;
|
|
||||||
var unconfirmedTxids = result.unconfirmedTxids;
|
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
45
lib/services/block/encoding.js
Normal file
45
lib/services/block/encoding.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function Encoding(servicePrefix) {
|
||||||
|
this.servicePrefix = servicePrefix;
|
||||||
|
this.hashPrefix = new Buffer('00', 'hex');
|
||||||
|
this.heightPrefix = new Buffer('01', 'hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
Encoding.prototype.encodeBlockHashKey = function(hash) {
|
||||||
|
return Buffer.concat([ this.servicePrefix, this.hashPrefix, new Buffer(hash, 'hex') ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
Encoding.prototype.decodeBlockHashKey = function(buffer) {
|
||||||
|
return buffer.slice(3).toString('hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
Encoding.prototype.encodeBlockHashValue = function(hash) {
|
||||||
|
return new Buffer(hash, 'hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
Encoding.prototype.decodeBlockHashValue = function(buffer) {
|
||||||
|
return buffer.toString('hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
Encoding.prototype.encodeBlockHeightKey = function(height) {
|
||||||
|
var heightBuf = new Buffer(4);
|
||||||
|
heightBuf.writeUInt32BE(height);
|
||||||
|
return Buffer.concat([ this.servicePrefix, this.heightPrefix, heightBuf ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
Encoding.prototype.decodeBlockHeightKey = function(buffer) {
|
||||||
|
return buffer.slice(3).readUInt32BE();
|
||||||
|
};
|
||||||
|
|
||||||
|
Encoding.prototype.encodeBlockHeightValue = function(height) {
|
||||||
|
var heightBuf = new Buffer(4);
|
||||||
|
heightBuf.writeUInt32BE(height);
|
||||||
|
return heightBuf;
|
||||||
|
};
|
||||||
|
|
||||||
|
Encoding.prototype.decodeBlockHeightValue = function(buffer) {
|
||||||
|
return buffer.readUInt32BE();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Encoding;
|
||||||
330
lib/services/block/index.js
Normal file
330
lib/services/block/index.js
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var BaseService = require('../../service');
|
||||||
|
var levelup = require('levelup');
|
||||||
|
var bitcore = require('bitcore-lib');
|
||||||
|
var Block = bitcore.Block;
|
||||||
|
var inherits = require('util').inherits;
|
||||||
|
var Encoding = require('./encoding');
|
||||||
|
var index = require('../../');
|
||||||
|
var log = index.log;
|
||||||
|
var BufferUtil = bitcore.util.buffer;
|
||||||
|
var utils = require('../../utils');
|
||||||
|
var Reorg = require('./reorg');
|
||||||
|
var $ = bitcore.util.preconditions;
|
||||||
|
var async = require('async');
|
||||||
|
var Sync = require('./sync');
|
||||||
|
|
||||||
|
var BlockService = function(options) {
|
||||||
|
BaseService.call(this, options);
|
||||||
|
this.bitcoind = this.node.services.bitcoind;
|
||||||
|
this.db = this.node.services.db;
|
||||||
|
this._sync = new Sync(this.node, this);
|
||||||
|
this.syncing = true;
|
||||||
|
this._lockTimes = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
inherits(BlockService, BaseService);
|
||||||
|
|
||||||
|
BlockService.dependencies = [
|
||||||
|
'bitcoind',
|
||||||
|
'db'
|
||||||
|
];
|
||||||
|
|
||||||
|
BlockService.prototype._log = function(msg, fn) {
|
||||||
|
if (!fn) {
|
||||||
|
return log.info('BlockService: ', msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.start = function(callback) {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.db.getPrefix(self.name, function(err, prefix) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prefix = prefix;
|
||||||
|
self.encoding = new Encoding(self.prefix);
|
||||||
|
self._setHandlers();
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype._setHandlers = function() {
|
||||||
|
var self = this;
|
||||||
|
self.node.once('ready', function() {
|
||||||
|
|
||||||
|
self.genesis = Block.fromBuffer(self.bitcoind.genesisBuffer);
|
||||||
|
self._loadTips(function(err) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
self._log('Bitcoin network tip is currently: ' + self.bitcoind.tiphash + ' at height: ' + self.bitcoind.height);
|
||||||
|
self._sync.sync();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.stop = function(callback) {
|
||||||
|
setImmediate(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.pauseSync = function(callback) {
|
||||||
|
var self = this;
|
||||||
|
self._lockTimes.push(process.hrtime());
|
||||||
|
if (self._sync.syncing) {
|
||||||
|
self._sync.once('synced', function() {
|
||||||
|
self._sync.paused = true;
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self._sync.paused = true;
|
||||||
|
setImmediate(callback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.resumeSync = function() {
|
||||||
|
this._log('Attempting to resume sync', log.debug);
|
||||||
|
var time = this._lockTimes.shift();
|
||||||
|
if (this._lockTimes.length === 0) {
|
||||||
|
if (time) {
|
||||||
|
this._log('sync lock held for: ' + utils.diffTime(time) + ' secs', log.debug);
|
||||||
|
}
|
||||||
|
this._sync.paused = false;
|
||||||
|
this._sync.sync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.detectReorg = function(blocks) {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!blocks || blocks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tipHash = self.reorgTipHash || self.tip.hash;
|
||||||
|
var chainMembers = [];
|
||||||
|
|
||||||
|
var loopIndex = 0;
|
||||||
|
var overallCounter = 0;
|
||||||
|
|
||||||
|
while(overallCounter < blocks.length) {
|
||||||
|
|
||||||
|
if (loopIndex >= blocks.length) {
|
||||||
|
overallCounter++;
|
||||||
|
loopIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevHash = BufferUtil.reverse(blocks[loopIndex].header.prevHash).toString('hex');
|
||||||
|
if (prevHash === tipHash) {
|
||||||
|
tipHash = blocks[loopIndex].hash;
|
||||||
|
chainMembers.push(blocks[loopIndex]);
|
||||||
|
}
|
||||||
|
loopIndex++;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for(var i = 0; i < blocks.length; i++) {
|
||||||
|
if (chainMembers.indexOf(blocks[i]) === -1) {
|
||||||
|
return blocks[i];
|
||||||
|
}
|
||||||
|
self.reorgTipHash = blocks[i].hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.handleReorg = function(forkBlock, callback) {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
self.printTipInfo('Reorg detected!');
|
||||||
|
|
||||||
|
self.reorg = true;
|
||||||
|
|
||||||
|
var reorg = new Reorg(self.node, self);
|
||||||
|
|
||||||
|
reorg.handleReorg(forkBlock.hash, function(err) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
self._log('Reorg failed! ' + err, log.error);
|
||||||
|
self.node.stop(function() {});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.printTipInfo('Reorg successful!');
|
||||||
|
self.reorg = false;
|
||||||
|
callback();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.printTipInfo = function(prependedMessage) {
|
||||||
|
|
||||||
|
this._log(
|
||||||
|
prependedMessage + ' Serial Tip: ' + this.tip.hash +
|
||||||
|
' Concurrent tip: ' + this.concurrentTip.hash +
|
||||||
|
' Bitcoind tip: ' + this.bitcoind.tiphash
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype._loadTips = function(callback) {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var tipStrings = ['tip', 'concurrentTip'];
|
||||||
|
|
||||||
|
async.each(tipStrings, function(tip, next) {
|
||||||
|
|
||||||
|
self.db.get(self.dbPrefix + tip, self.dbOptions, function(err, tipData) {
|
||||||
|
|
||||||
|
if(err && !(err instanceof levelup.errors.NotFoundError)) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash;
|
||||||
|
if (!tipData) {
|
||||||
|
hash = new Array(65).join('0');
|
||||||
|
self[tip] = {
|
||||||
|
height: -1,
|
||||||
|
hash: hash,
|
||||||
|
'__height': -1
|
||||||
|
};
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = tipData.slice(0, 32).toString('hex');
|
||||||
|
|
||||||
|
self.bitcoind.getBlock(hash, function(err, block) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
self[tip] = block;
|
||||||
|
self._log('loaded ' + tip + ' hash: ' + block.hash + ' height: ' + block.__height);
|
||||||
|
next();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}, callback);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.getConcurrentBlockOperations = function(block, add, callback) {
|
||||||
|
var operations = [];
|
||||||
|
|
||||||
|
async.each(
|
||||||
|
this.node.services,
|
||||||
|
function(mod, next) {
|
||||||
|
if(mod.concurrentBlockHandler) {
|
||||||
|
$.checkArgument(typeof mod.concurrentBlockHandler === 'function', 'concurrentBlockHandler must be a function');
|
||||||
|
|
||||||
|
mod.concurrentBlockHandler.call(mod, block, add, function(err, ops) {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
if (ops) {
|
||||||
|
$.checkArgument(Array.isArray(ops), 'concurrentBlockHandler for ' + mod.name + ' returned non-array');
|
||||||
|
operations = operations.concat(ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setImmediate(next);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, operations);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.getSerialBlockOperations = function(block, add, callback) {
|
||||||
|
var operations = [];
|
||||||
|
|
||||||
|
async.eachSeries(
|
||||||
|
this.node.services,
|
||||||
|
function(mod, next) {
|
||||||
|
if(mod.blockHandler) {
|
||||||
|
$.checkArgument(typeof mod.blockHandler === 'function', 'blockHandler must be a function');
|
||||||
|
|
||||||
|
mod.blockHandler.call(mod, block, add, function(err, ops) {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
if (ops) {
|
||||||
|
$.checkArgument(Array.isArray(ops), 'blockHandler for ' + mod.name + ' returned non-array');
|
||||||
|
operations = operations.concat(ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setImmediate(next);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, operations);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.getTipOperation = function(block, add) {
|
||||||
|
var heightBuffer = new Buffer(4);
|
||||||
|
var tipData;
|
||||||
|
|
||||||
|
if(add) {
|
||||||
|
heightBuffer.writeUInt32BE(block.__height);
|
||||||
|
tipData = Buffer.concat([new Buffer(block.hash, 'hex'), heightBuffer]);
|
||||||
|
} else {
|
||||||
|
heightBuffer.writeUInt32BE(block.__height - 1);
|
||||||
|
tipData = Buffer.concat([BufferUtil.reverse(block.header.prevHash), heightBuffer]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'put',
|
||||||
|
key: this.dbPrefix + 'tip',
|
||||||
|
value: tipData
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.getConcurrentTipOperation = function(block, add) {
|
||||||
|
var heightBuffer = new Buffer(4);
|
||||||
|
var tipData;
|
||||||
|
if(add) {
|
||||||
|
heightBuffer.writeUInt32BE(block.__height);
|
||||||
|
tipData = Buffer.concat([new Buffer(block.hash, 'hex'), heightBuffer]);
|
||||||
|
} else {
|
||||||
|
heightBuffer.writeUInt32BE(block.__height - 1);
|
||||||
|
tipData = Buffer.concat([BufferUtil.reverse(block.header.prevHash), heightBuffer]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'put',
|
||||||
|
key: this.dbPrefix + 'concurrentTip',
|
||||||
|
value: tipData
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = BlockService;
|
||||||
@ -5,9 +5,9 @@ var BufferUtil = bitcore.util.buffer;
|
|||||||
var log = index.log;
|
var log = index.log;
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
|
||||||
function Reorg(node, db) {
|
function Reorg(node, block) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
this.db = db;
|
this.block = block;
|
||||||
}
|
}
|
||||||
|
|
||||||
Reorg.prototype.handleReorg = function(newBlockHash, callback) {
|
Reorg.prototype.handleReorg = function(newBlockHash, callback) {
|
||||||
@ -18,7 +18,7 @@ Reorg.prototype.handleReorg = function(newBlockHash, callback) {
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.findCommonAncestorAndNewHashes(self.db.tip.hash, newBlockHash, function(err, commonAncestor, newHashes) {
|
self.findCommonAncestorAndNewHashes(self.block.tip.hash, newBlockHash, function(err, commonAncestor, newHashes) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
@ -36,11 +36,11 @@ Reorg.prototype.handleReorg = function(newBlockHash, callback) {
|
|||||||
Reorg.prototype.handleConcurrentReorg = function(callback) {
|
Reorg.prototype.handleConcurrentReorg = function(callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(self.db.concurrentTip.hash === self.db.tip.hash) {
|
if(self.block.concurrentTip.hash === self.block.tip.hash) {
|
||||||
return callback();
|
return callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.findCommonAncestorAndNewHashes(self.db.concurrentTip.hash, self.db.tip.hash, function(err, commonAncestor, newHashes) {
|
self.findCommonAncestorAndNewHashes(self.block.concurrentTip.hash, self.block.tip.hash, function(err, commonAncestor, newHashes) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
@ -60,28 +60,28 @@ Reorg.prototype.rewindConcurrentTip = function(commonAncestor, callback) {
|
|||||||
|
|
||||||
async.whilst(
|
async.whilst(
|
||||||
function() {
|
function() {
|
||||||
return self.db.concurrentTip.hash !== commonAncestor;
|
return self.block.concurrentTip.hash !== commonAncestor;
|
||||||
},
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
self.db.getConcurrentBlockOperations(self.db.concurrentTip, false, function(err, operations) {
|
self.block.getConcurrentBlockOperations(self.block.concurrentTip, false, function(err, operations) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.push(self.db.getConcurrentTipOperation(self.db.concurrentTip, false));
|
operations.push(self.block.getConcurrentTipOperation(self.block.concurrentTip, false));
|
||||||
self.db.batch(operations, function(err) {
|
self.block.batch(operations, function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
var prevHash = BufferUtil.reverse(self.db.concurrentTip.header.prevHash).toString('hex');
|
var prevHash = BufferUtil.reverse(self.block.concurrentTip.header.prevHash).toString('hex');
|
||||||
|
|
||||||
self.node.services.bitcoind.getBlock(prevHash, function(err, block) {
|
self.node.services.bitcoind.getBlock(prevHash, function(err, block) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.concurrentTip = block;
|
self.block.concurrentTip = block;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -102,18 +102,18 @@ Reorg.prototype.fastForwardConcurrentTip = function(newHashes, callback) {
|
|||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.getConcurrentBlockOperations(block, true, function(err, operations) {
|
self.block.getConcurrentBlockOperations(block, true, function(err, operations) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.push(self.db.getConcurrentTipOperation(block, true));
|
operations.push(self.block.getConcurrentTipOperation(block, true));
|
||||||
self.db.batch(operations, function(err) {
|
self.block.batch(operations, function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.concurrentTip = block;
|
self.block.concurrentTip = block;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -126,27 +126,27 @@ Reorg.prototype.rewindBothTips = function(commonAncestor, callback) {
|
|||||||
|
|
||||||
async.whilst(
|
async.whilst(
|
||||||
function() {
|
function() {
|
||||||
return self.db.tip.hash !== commonAncestor;
|
return self.block.tip.hash !== commonAncestor;
|
||||||
},
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
async.parallel(
|
async.parallel(
|
||||||
[
|
[
|
||||||
function(next) {
|
function(next) {
|
||||||
self.db.getConcurrentBlockOperations(self.db.concurrentTip, false, function(err, operations) {
|
self.block.getConcurrentBlockOperations(self.block.concurrentTip, false, function(err, operations) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
operations.push(self.db.getConcurrentTipOperation(self.db.concurrentTip, false));
|
operations.push(self.block.getConcurrentTipOperation(self.block.concurrentTip, false));
|
||||||
next(null, operations);
|
next(null, operations);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
self.db.getSerialBlockOperations(self.db.tip, false, function(err, operations) {
|
self.block.getSerialBlockOperations(self.block.tip, false, function(err, operations) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.push(self.db.getTipOperation(self.db.tip, false));
|
operations.push(self.block.getTipOperation(self.block.tip, false));
|
||||||
next(null, operations);
|
next(null, operations);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -157,20 +157,20 @@ Reorg.prototype.rewindBothTips = function(commonAncestor, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var operations = results[0].concat(results[1]);
|
var operations = results[0].concat(results[1]);
|
||||||
self.db.batch(operations, function(err) {
|
self.block.batch(operations, function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
var prevHash = BufferUtil.reverse(self.db.tip.header.prevHash).toString('hex');
|
var prevHash = BufferUtil.reverse(self.block.tip.header.prevHash).toString('hex');
|
||||||
|
|
||||||
self.node.services.bitcoind.getBlock(prevHash, function(err, block) {
|
self.node.services.bitcoind.getBlock(prevHash, function(err, block) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.concurrentTip = block;
|
self.block.concurrentTip = block;
|
||||||
self.db.tip = block;
|
self.block.tip = block;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -193,22 +193,22 @@ Reorg.prototype.fastForwardBothTips = function(newHashes, callback) {
|
|||||||
async.parallel(
|
async.parallel(
|
||||||
[
|
[
|
||||||
function(next) {
|
function(next) {
|
||||||
self.db.getConcurrentBlockOperations(block, true, function(err, operations) {
|
self.block.getConcurrentBlockOperations(block, true, function(err, operations) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.push(self.db.getConcurrentTipOperation(block, true));
|
operations.push(self.block.getConcurrentTipOperation(block, true));
|
||||||
next(null, operations);
|
next(null, operations);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
self.db.getSerialBlockOperations(block, true, function(err, operations) {
|
self.block.getSerialBlockOperations(block, true, function(err, operations) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.push(self.db.getTipOperation(block, true));
|
operations.push(self.block.getTipOperation(block, true));
|
||||||
next(null, operations);
|
next(null, operations);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -220,13 +220,13 @@ Reorg.prototype.fastForwardBothTips = function(newHashes, callback) {
|
|||||||
|
|
||||||
var operations = results[0].concat(results[1]);
|
var operations = results[0].concat(results[1]);
|
||||||
|
|
||||||
self.db.batch(operations, function(err) {
|
self.block.batch(operations, function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.concurrentTip = block;
|
self.block.concurrentTip = block;
|
||||||
self.db.tip = block;
|
self.block.tip = block;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -10,23 +10,24 @@ var Block = bitcore.Block;
|
|||||||
var index = require('../../index');
|
var index = require('../../index');
|
||||||
var log = index.log;
|
var log = index.log;
|
||||||
|
|
||||||
function BlockStream(highWaterMark, db, sync) {
|
function BlockStream(highWaterMark, sync) {
|
||||||
Readable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
Readable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||||
this.sync = sync;
|
this.sync = sync;
|
||||||
this.db = db;
|
this.block = this.sync.block;
|
||||||
this.dbTip = this.db.tip;
|
this.dbTip = this.block.tip;
|
||||||
this.lastReadHeight = this.dbTip.__height;
|
this.lastReadHeight = this.dbTip.__height;
|
||||||
this.lastEmittedHash = this.dbTip.hash;
|
this.lastEmittedHash = this.dbTip.hash;
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.bitcoind = this.db.bitcoind;
|
this.bitcoind = this.block.bitcoind;
|
||||||
}
|
}
|
||||||
|
|
||||||
inherits(BlockStream, Readable);
|
inherits(BlockStream, Readable);
|
||||||
|
|
||||||
function ProcessConcurrent(highWaterMark, db) {
|
function ProcessConcurrent(highWaterMark, sync) {
|
||||||
Transform.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
Transform.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||||
this.db = db;
|
this.block = sync.block;
|
||||||
|
this.db = sync.db;
|
||||||
this.operations = [];
|
this.operations = [];
|
||||||
this.lastBlock = 0;
|
this.lastBlock = 0;
|
||||||
this.blockCount = 0;
|
this.blockCount = 0;
|
||||||
@ -34,35 +35,38 @@ function ProcessConcurrent(highWaterMark, db) {
|
|||||||
|
|
||||||
inherits(ProcessConcurrent, Transform);
|
inherits(ProcessConcurrent, Transform);
|
||||||
|
|
||||||
function ProcessSerial(highWaterMark, db, tip) {
|
function ProcessSerial(highWaterMark, sync) {
|
||||||
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||||
this.db = db;
|
this.block = sync.block;
|
||||||
this.tip = tip;
|
this.db = sync.db;
|
||||||
|
this.tip = sync.block.tip;
|
||||||
this.processBlockStartTime = [];
|
this.processBlockStartTime = [];
|
||||||
this._lastReportedTime = Date.now();
|
this._lastReportedTime = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
inherits(ProcessSerial, Writable);
|
inherits(ProcessSerial, Writable);
|
||||||
|
|
||||||
function ProcessBoth(highWaterMark, db) {
|
function ProcessBoth(highWaterMark, sync) {
|
||||||
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||||
this.db = db;
|
this.db = sync.db;
|
||||||
}
|
}
|
||||||
|
|
||||||
inherits(ProcessBoth, Writable);
|
inherits(ProcessBoth, Writable);
|
||||||
|
|
||||||
function WriteStream(highWaterMark, db) {
|
function WriteStream(highWaterMark, sync) {
|
||||||
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||||
this.db = db;
|
this.db = sync.db;
|
||||||
this.writeTime = 0;
|
this.writeTime = 0;
|
||||||
this.lastConcurrentOutputHeight = 0;
|
this.lastConcurrentOutputHeight = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
inherits(WriteStream, Writable);
|
inherits(WriteStream, Writable);
|
||||||
|
|
||||||
function Sync(node, db) {
|
function Sync(node, block) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
this.db = db;
|
this.db = this.node.services.db;
|
||||||
|
this.block = block;
|
||||||
|
console.log(block);
|
||||||
this.syncing = false;
|
this.syncing = false;
|
||||||
this.paused = false; //we can't sync while one of our indexes is reading/writing separate from us
|
this.paused = false; //we can't sync while one of our indexes is reading/writing separate from us
|
||||||
this.highWaterMark = 10;
|
this.highWaterMark = 10;
|
||||||
@ -80,10 +84,10 @@ Sync.prototype.sync = function() {
|
|||||||
|
|
||||||
self.syncing = true;
|
self.syncing = true;
|
||||||
|
|
||||||
var blockStream = new BlockStream(self.highWaterMark, self.db, self);
|
var blockStream = new BlockStream(self.highWaterMark, self);
|
||||||
var processConcurrent = new ProcessConcurrent(self.highWaterMark, self.db);
|
var processConcurrent = new ProcessConcurrent(self.highWaterMark, self);
|
||||||
var writeStream = new WriteStream(self.highWaterMark, self.db);
|
var writeStream = new WriteStream(self.highWaterMark, self);
|
||||||
var processSerial = new ProcessSerial(self.highWaterMark, self.db, self.db.tip);
|
var processSerial = new ProcessSerial(self.highWaterMark, self);
|
||||||
|
|
||||||
self._handleErrors(blockStream);
|
self._handleErrors(blockStream);
|
||||||
self._handleErrors(processConcurrent);
|
self._handleErrors(processConcurrent);
|
||||||
@ -238,7 +242,7 @@ BlockStream.prototype._getBlocks = function(heights, callback) {
|
|||||||
ProcessSerial.prototype._reportStatus = function() {
|
ProcessSerial.prototype._reportStatus = function() {
|
||||||
if ((Date.now() - this._lastReportedTime) > 1000) {
|
if ((Date.now() - this._lastReportedTime) > 1000) {
|
||||||
this._lastReportedTime = Date.now();
|
this._lastReportedTime = Date.now();
|
||||||
log.info('Sync: current height is: ' + this.db.tip.__height);
|
log.info('Sync: current height is: ' + this.block.tip.__height);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -246,7 +250,7 @@ ProcessSerial.prototype._write = function(block, enc, callback) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
function check() {
|
function check() {
|
||||||
return self.db.concurrentTip.__height >= block.__height;
|
return self.block.concurrentTip.__height >= block.__height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(check()) {
|
if(check()) {
|
||||||
@ -255,7 +259,7 @@ ProcessSerial.prototype._write = function(block, enc, callback) {
|
|||||||
|
|
||||||
self.db.once('concurrentaddblock', function() {
|
self.db.once('concurrentaddblock', function() {
|
||||||
if(!check()) {
|
if(!check()) {
|
||||||
var err = new Error('Concurrent block ' + self.db.concurrentTip.__height + ' is less than ' + block.__height);
|
var err = new Error('Concurrent block ' + self.block.concurrentTip.__height + ' is less than ' + block.__height);
|
||||||
return self.emit('error', err);
|
return self.emit('error', err);
|
||||||
}
|
}
|
||||||
self._process(block, callback);
|
self._process(block, callback);
|
||||||
@ -271,7 +275,7 @@ ProcessSerial.prototype._process = function(block, callback) {
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.push(self.db.getTipOperation(block, true));
|
operations.push(self.block.getTipOperation(block, true));
|
||||||
|
|
||||||
var obj = {
|
var obj = {
|
||||||
tip: block,
|
tip: block,
|
||||||
@ -285,7 +289,7 @@ ProcessSerial.prototype._process = function(block, callback) {
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.tip = block;
|
self.block.tip = block;
|
||||||
self._reportStatus();
|
self._reportStatus();
|
||||||
self.db.emit('addblock');
|
self.db.emit('addblock');
|
||||||
|
|
||||||
@ -309,7 +313,7 @@ ProcessConcurrent.prototype._transform = function(block, enc, callback) {
|
|||||||
self.operations = self.operations.concat(operations);
|
self.operations = self.operations.concat(operations);
|
||||||
|
|
||||||
if(self.blockCount >= 1) {
|
if(self.blockCount >= 1) {
|
||||||
self.operations.push(self.db.getConcurrentTipOperation(block, true));
|
self.operations.push(self.block.getConcurrentTipOperation(block, true));
|
||||||
var obj = {
|
var obj = {
|
||||||
concurrentTip: block,
|
concurrentTip: block,
|
||||||
operations: self.operations
|
operations: self.operations
|
||||||
@ -326,7 +330,7 @@ ProcessConcurrent.prototype._transform = function(block, enc, callback) {
|
|||||||
|
|
||||||
ProcessConcurrent.prototype._flush = function(callback) {
|
ProcessConcurrent.prototype._flush = function(callback) {
|
||||||
if(this.operations.length) {
|
if(this.operations.length) {
|
||||||
this.operations.push(this.db.getConcurrentTipOperation(this.lastBlock, true));
|
this.operations.push(this.block.getConcurrentTipOperation(this.lastBlock, true));
|
||||||
this.operations = [];
|
this.operations = [];
|
||||||
return callback(null, this.operations);
|
return callback(null, this.operations);
|
||||||
}
|
}
|
||||||
@ -344,9 +348,9 @@ WriteStream.prototype._write = function(obj, enc, callback) {
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db.concurrentTip = obj.concurrentTip;
|
self.block.concurrentTip = obj.concurrentTip;
|
||||||
self.db.emit('concurrentaddblock');
|
self.db.emit('concurrentaddblock');
|
||||||
self.lastConcurrentOutputHeight = self.db.concurrentTip.__height;
|
self.lastConcurrentOutputHeight = self.block.concurrentTip.__height;
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -359,7 +363,7 @@ ProcessBoth.prototype._write = function(block, encoding, callback) {
|
|||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
operations.push(self.db.getConcurrentTipOperation(block, true));
|
operations.push(self.block.getConcurrentTipOperation(block, true));
|
||||||
next(null, operations);
|
next(null, operations);
|
||||||
});
|
});
|
||||||
}, function(next) {
|
}, function(next) {
|
||||||
@ -367,7 +371,7 @@ ProcessBoth.prototype._write = function(block, encoding, callback) {
|
|||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
operations.push(self.db.getTipOperation(block, true));
|
operations.push(self.block.getTipOperation(block, true));
|
||||||
next(null, operations);
|
next(null, operations);
|
||||||
});
|
});
|
||||||
}], function(err, results) {
|
}], function(err, results) {
|
||||||
@ -379,8 +383,8 @@ ProcessBoth.prototype._write = function(block, encoding, callback) {
|
|||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
self.db.tip = block;
|
self.block.tip = block;
|
||||||
self.db.concurrentTip = block;
|
self.block.concurrentTip = block;
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -7,25 +7,14 @@ var levelup = require('levelup');
|
|||||||
var leveldown = require('leveldown');
|
var leveldown = require('leveldown');
|
||||||
var mkdirp = require('mkdirp');
|
var mkdirp = require('mkdirp');
|
||||||
var bitcore = require('bitcore-lib');
|
var bitcore = require('bitcore-lib');
|
||||||
var BufferUtil = bitcore.util.buffer;
|
|
||||||
var Networks = bitcore.Networks;
|
var Networks = bitcore.Networks;
|
||||||
var Block = bitcore.Block;
|
var Block = bitcore.Block;
|
||||||
var $ = bitcore.util.preconditions;
|
var $ = bitcore.util.preconditions;
|
||||||
var index = require('../../');
|
var index = require('../../');
|
||||||
var log = index.log;
|
var log = index.log;
|
||||||
var Service = require('../../service');
|
var Service = require('../../service');
|
||||||
var Sync = require('./sync');
|
|
||||||
var Reorg = require('./reorg');
|
|
||||||
var Block = bitcore.Block;
|
|
||||||
var utils = require('../../utils');
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @param {Object} options
|
|
||||||
* @param {Node} options.node - A reference to the node
|
|
||||||
* @param {Node} options.store - A levelup backend store
|
|
||||||
*/
|
|
||||||
function DB(options) {
|
function DB(options) {
|
||||||
/* jshint maxstatements: 20 */
|
|
||||||
|
|
||||||
if (!(this instanceof DB)) {
|
if (!(this instanceof DB)) {
|
||||||
return new DB(options);
|
return new DB(options);
|
||||||
@ -60,43 +49,14 @@ function DB(options) {
|
|||||||
|
|
||||||
this.subscriptions = {};
|
this.subscriptions = {};
|
||||||
|
|
||||||
this._sync = new Sync(this.node, this);
|
|
||||||
this.syncing = true;
|
|
||||||
this.bitcoind = this.node.services.bitcoind;
|
this.bitcoind = this.node.services.bitcoind;
|
||||||
this._operationsQueue = [];
|
this._operationsQueue = [];
|
||||||
this._lockTimes = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
util.inherits(DB, Service);
|
util.inherits(DB, Service);
|
||||||
|
|
||||||
DB.dependencies = ['bitcoind'];
|
DB.dependencies = ['bitcoind'];
|
||||||
|
|
||||||
DB.prototype.pauseSync = function(callback) {
|
|
||||||
var self = this;
|
|
||||||
self._lockTimes.push(process.hrtime());
|
|
||||||
if (self._sync.syncing) {
|
|
||||||
self._sync.once('synced', function() {
|
|
||||||
self._sync.paused = true;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self._sync.paused = true;
|
|
||||||
setImmediate(callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.resumeSync = function() {
|
|
||||||
log.debug('Attempting to resume sync');
|
|
||||||
var time = this._lockTimes.shift();
|
|
||||||
if (this._lockTimes.length === 0) {
|
|
||||||
if (time) {
|
|
||||||
log.debug('sync lock held for: ' + utils.diffTime(time) + ' secs');
|
|
||||||
}
|
|
||||||
this._sync.paused = false;
|
|
||||||
this._sync.sync();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype._setDataPath = function() {
|
DB.prototype._setDataPath = function() {
|
||||||
$.checkState(this.node.datadir, 'Node is expected to have a "datadir" property');
|
$.checkState(this.node.datadir, 'Node is expected to have a "datadir" property');
|
||||||
if (this.node.network === Networks.livenet) {
|
if (this.node.network === Networks.livenet) {
|
||||||
@ -159,21 +119,6 @@ DB.prototype.start = function(callback) {
|
|||||||
|
|
||||||
self._store = levelup(self.dataPath, { db: self.levelupStore, keyEncoding: 'binary', valueEncoding: 'binary'});
|
self._store = levelup(self.dataPath, { db: self.levelupStore, keyEncoding: 'binary', valueEncoding: 'binary'});
|
||||||
|
|
||||||
self.node.once('ready', function() {
|
|
||||||
|
|
||||||
self.genesis = Block.fromBuffer(self.bitcoind.genesisBuffer);
|
|
||||||
self.loadTips(function(err) {
|
|
||||||
|
|
||||||
if(err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info('Bitcoin network tip is currently: ' + self.bitcoind.tiphash + ' at height: ' + self.bitcoind.height);
|
|
||||||
self._sync.sync();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
setImmediate(function() {
|
setImmediate(function() {
|
||||||
self._checkVersion(self._setVersion.bind(self, callback));
|
self._checkVersion(self._setVersion.bind(self, callback));
|
||||||
@ -239,80 +184,6 @@ DB.prototype.createKeyStream = function(op) {
|
|||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
DB.prototype.detectReorg = function(blocks) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (!blocks || blocks.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipHash = self.reorgTipHash || self.tip.hash;
|
|
||||||
var chainMembers = [];
|
|
||||||
|
|
||||||
var loopIndex = 0;
|
|
||||||
var overallCounter = 0;
|
|
||||||
|
|
||||||
while(overallCounter < blocks.length) {
|
|
||||||
|
|
||||||
if (loopIndex >= blocks.length) {
|
|
||||||
overallCounter++;
|
|
||||||
loopIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var prevHash = BufferUtil.reverse(blocks[loopIndex].header.prevHash).toString('hex');
|
|
||||||
if (prevHash === tipHash) {
|
|
||||||
tipHash = blocks[loopIndex].hash;
|
|
||||||
chainMembers.push(blocks[loopIndex]);
|
|
||||||
}
|
|
||||||
loopIndex++;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
for(var i = 0; i < blocks.length; i++) {
|
|
||||||
if (chainMembers.indexOf(blocks[i]) === -1) {
|
|
||||||
return blocks[i];
|
|
||||||
}
|
|
||||||
self.reorgTipHash = blocks[i].hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.handleReorg = function(forkBlock, callback) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
self.printTipInfo('Reorg detected!');
|
|
||||||
|
|
||||||
self.reorg = true;
|
|
||||||
|
|
||||||
var reorg = new Reorg(self.node, self);
|
|
||||||
|
|
||||||
reorg.handleReorg(forkBlock.hash, function(err) {
|
|
||||||
|
|
||||||
if(err) {
|
|
||||||
log.error('Reorg failed! ' + err);
|
|
||||||
self.node.stop(function() {});
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.printTipInfo('Reorg successful!');
|
|
||||||
self.reorg = false;
|
|
||||||
callback();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.printTipInfo = function(prependedMessage) {
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
prependedMessage + ' Serial Tip: ' + this.tip.hash +
|
|
||||||
' Concurrent tip: ' + this.concurrentTip.hash +
|
|
||||||
' Bitcoind tip: ' + this.bitcoind.tiphash
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.stop = function(callback) {
|
DB.prototype.stop = function(callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
self._stopping = true;
|
self._stopping = true;
|
||||||
@ -335,159 +206,11 @@ DB.prototype.getAPIMethods = function() {
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
DB.prototype.loadTips = function(callback) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var tipStrings = ['tip', 'concurrentTip'];
|
|
||||||
|
|
||||||
async.each(tipStrings, function(tip, next) {
|
|
||||||
|
|
||||||
self._store.get(self.dbPrefix + tip, self.dbOptions, function(err, tipData) {
|
|
||||||
|
|
||||||
if(err && !(err instanceof levelup.errors.NotFoundError)) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hash;
|
|
||||||
if (!tipData) {
|
|
||||||
hash = new Array(65).join('0');
|
|
||||||
self[tip] = {
|
|
||||||
height: -1,
|
|
||||||
hash: hash,
|
|
||||||
'__height': -1
|
|
||||||
};
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = tipData.slice(0, 32).toString('hex');
|
|
||||||
|
|
||||||
self.bitcoind.getBlock(hash, function(err, block) {
|
|
||||||
|
|
||||||
if(err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
self[tip] = block;
|
|
||||||
log.info('loaded ' + tip + ', hash: ' + block.hash + ' height: ' + block.__height);
|
|
||||||
next();
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}, callback);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.getPublishEvents = function() {
|
DB.prototype.getPublishEvents = function() {
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
DB.prototype.getConcurrentBlockOperations = function(block, add, callback) {
|
|
||||||
var operations = [];
|
|
||||||
|
|
||||||
async.each(
|
|
||||||
this.node.services,
|
|
||||||
function(mod, next) {
|
|
||||||
if(mod.concurrentBlockHandler) {
|
|
||||||
$.checkArgument(typeof mod.concurrentBlockHandler === 'function', 'concurrentBlockHandler must be a function');
|
|
||||||
|
|
||||||
mod.concurrentBlockHandler.call(mod, block, add, function(err, ops) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
if (ops) {
|
|
||||||
$.checkArgument(Array.isArray(ops), 'concurrentBlockHandler for ' + mod.name + ' returned non-array');
|
|
||||||
operations = operations.concat(ops);
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setImmediate(next);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function(err) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, operations);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.getSerialBlockOperations = function(block, add, callback) {
|
|
||||||
var operations = [];
|
|
||||||
|
|
||||||
async.eachSeries(
|
|
||||||
this.node.services,
|
|
||||||
function(mod, next) {
|
|
||||||
if(mod.blockHandler) {
|
|
||||||
$.checkArgument(typeof mod.blockHandler === 'function', 'blockHandler must be a function');
|
|
||||||
|
|
||||||
mod.blockHandler.call(mod, block, add, function(err, ops) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
if (ops) {
|
|
||||||
$.checkArgument(Array.isArray(ops), 'blockHandler for ' + mod.name + ' returned non-array');
|
|
||||||
operations = operations.concat(ops);
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setImmediate(next);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function(err) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, operations);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.getTipOperation = function(block, add) {
|
|
||||||
var heightBuffer = new Buffer(4);
|
|
||||||
var tipData;
|
|
||||||
|
|
||||||
if(add) {
|
|
||||||
heightBuffer.writeUInt32BE(block.__height);
|
|
||||||
tipData = Buffer.concat([new Buffer(block.hash, 'hex'), heightBuffer]);
|
|
||||||
} else {
|
|
||||||
heightBuffer.writeUInt32BE(block.__height - 1);
|
|
||||||
tipData = Buffer.concat([BufferUtil.reverse(block.header.prevHash), heightBuffer]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'put',
|
|
||||||
key: this.dbPrefix + 'tip',
|
|
||||||
value: tipData
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.getConcurrentTipOperation = function(block, add) {
|
|
||||||
var heightBuffer = new Buffer(4);
|
|
||||||
var tipData;
|
|
||||||
if(add) {
|
|
||||||
heightBuffer.writeUInt32BE(block.__height);
|
|
||||||
tipData = Buffer.concat([new Buffer(block.hash, 'hex'), heightBuffer]);
|
|
||||||
} else {
|
|
||||||
heightBuffer.writeUInt32BE(block.__height - 1);
|
|
||||||
tipData = Buffer.concat([BufferUtil.reverse(block.header.prevHash), heightBuffer]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'put',
|
|
||||||
key: this.dbPrefix + 'concurrentTip',
|
|
||||||
value: tipData
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
DB.prototype.getPrefix = function(service, callback) {
|
DB.prototype.getPrefix = function(service, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Encoding = require('./encoding');
|
var Encoding = require('./encoding');
|
||||||
var BaseService = require('../../service');
|
var BaseService = require('../../service');
|
||||||
var inherits = require('util').inherits;
|
var inherits = require('util').inherits;
|
||||||
var MAINNET_BITCOIN_GENESIS_TIME = 1296688602;
|
var LRU = require('lru-cache');
|
||||||
|
var utils = require('../../../lib/utils');
|
||||||
|
|
||||||
function TimestampService(options) {
|
function TimestampService(options) {
|
||||||
BaseService.call(this, options);
|
BaseService.call(this, options);
|
||||||
this.currentBlock = null;
|
this.currentBlock = null;
|
||||||
this.currentTimestamp = null;
|
this.currentTimestamp = null;
|
||||||
|
this._blockHandlerQueue = LRU(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
inherits(TimestampService, BaseService);
|
inherits(TimestampService, BaseService);
|
||||||
@ -28,86 +31,72 @@ TimestampService.prototype.start = function(callback) {
|
|||||||
self.encoding = new Encoding(self.prefix);
|
self.encoding = new Encoding(self.prefix);
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.counter = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
TimestampService.prototype.stop = function(callback) {
|
TimestampService.prototype.stop = function(callback) {
|
||||||
setImmediate(callback);
|
setImmediate(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TimestampService.prototype._processBlockHandlerQueue = function(block) {
|
||||||
|
var self = this;
|
||||||
|
//do we have a record in the queue that is waiting on me to consume it?
|
||||||
|
var prevHash = utils.getPrevHashString(block);
|
||||||
|
if (prevHash.length !== 64) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timestamp = self._blockHandlerQueue.get(prevHash);
|
||||||
|
|
||||||
|
if (timestamp) {
|
||||||
|
if (block.header.timestamp <= timestamp) {
|
||||||
|
timestamp = block.header.timestamp + 1;
|
||||||
|
}
|
||||||
|
self.counter++;
|
||||||
|
timestamp = block.header.timestamp;
|
||||||
|
self._blockHandlerQueue.del(prevHash);
|
||||||
|
} else {
|
||||||
|
timestamp = block.header.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
self._blockHandlerQueue.set(block.hash, timestamp);
|
||||||
|
return timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
TimestampService.prototype.blockHandler = function(block, connectBlock, callback) {
|
TimestampService.prototype.blockHandler = function(block, connectBlock, callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var action = 'put';
|
var action = connectBlock ? 'put' : 'del';
|
||||||
if (!connectBlock) {
|
|
||||||
action = 'del';
|
var timestamp = self._processBlockHandlerQueue(block);
|
||||||
|
|
||||||
|
if (!timestamp) {
|
||||||
|
return callback(null, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
var operations = [];
|
var operations = [];
|
||||||
|
|
||||||
function getLastTimestamp(next) {
|
operations = operations.concat([
|
||||||
if(block.__height === 0) {
|
{
|
||||||
return next(null, (block.header.timestamp - 1));
|
type: action,
|
||||||
|
key: self.encoding.encodeTimestampBlockKey(timestamp),
|
||||||
|
value: self.encoding.encodeTimestampBlockValue(block.header.hash)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: action,
|
||||||
|
key: self.encoding.encodeBlockTimestampKey(block.header.hash),
|
||||||
|
value: self.encoding.encodeBlockTimestampValue(timestamp)
|
||||||
}
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
self.getTimestamp(block.toObject().header.prevHash, next);
|
callback(null, operations);
|
||||||
}
|
|
||||||
|
|
||||||
getLastTimestamp(function(err, lastTimestamp) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
var timestamp = block.header.timestamp;
|
|
||||||
if(timestamp <= lastTimestamp) {
|
|
||||||
timestamp = lastTimestamp + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.currentBlock = block.hash;
|
|
||||||
self.currentTimestamp = timestamp;
|
|
||||||
|
|
||||||
operations = operations.concat(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
type: action,
|
|
||||||
key: self.encoding.encodeTimestampBlockKey(timestamp),
|
|
||||||
value: self.encoding.encodeTimestampBlockValue(block.header.hash)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: action,
|
|
||||||
key: self.encoding.encodeBlockTimestampKey(block.header.hash),
|
|
||||||
value: self.encoding.encodeBlockTimestampValue(timestamp)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
callback(null, operations);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
TimestampService.prototype.getTimestamp = function(hash, callback) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (hash === self.currentBlock) {
|
|
||||||
return setImmediate(function() {
|
|
||||||
callback(null, self.currentTimestamp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = self.encoding.encodeBlockTimestampKey(hash);
|
|
||||||
self.db.get(key, function(err, buffer) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback(null, self.encoding.decodeBlockTimestampValue(buffer));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
TimestampService.prototype.getBlockHeights = function(timestamps, callback) {
|
TimestampService.prototype.getBlockHeights = function(timestamps, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
timestamps.sort();
|
timestamps.sort();
|
||||||
//due to a bug in the encoding routines, thers is a minimum timestamp that
|
|
||||||
//can be searched for, which is 1296688602 (the bitcoin mainnet genesis block
|
|
||||||
timestamps = timestamps.map(function(timestamp) {
|
timestamps = timestamps.map(function(timestamp) {
|
||||||
return timestamp >= MAINNET_BITCOIN_GENESIS_TIME ? timestamp : MAINNET_BITCOIN_GENESIS_TIME;
|
return timestamp >= MAINNET_BITCOIN_GENESIS_TIME ? timestamp : MAINNET_BITCOIN_GENESIS_TIME;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -34,7 +34,7 @@ TransactionService.prototype.start = function(callback) {
|
|||||||
|
|
||||||
self.db = this.node.services.db;
|
self.db = this.node.services.db;
|
||||||
|
|
||||||
self.node.services.db.getPrefix(self.name, function(err, prefix) {
|
self.db.getPrefix(self.name, function(err, prefix) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1307,25 +1307,9 @@ WalletService.prototype._endpointJobStatus = function() {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService.prototype._endpointGetInfo = function() {
|
|
||||||
var self = this;
|
|
||||||
return function(req, res) {
|
|
||||||
res.jsonp({
|
|
||||||
result: 'ok',
|
|
||||||
dbheight: self.node.services.db.tip.__height,
|
|
||||||
dbhash: self.node.services.db.tip.hash,
|
|
||||||
bitcoindheight: self.node.services.bitcoind.height,
|
|
||||||
bitcoindhash: self.node.services.bitcoind.tiphash
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
WalletService.prototype._setupReadOnlyRoutes = function(app) {
|
WalletService.prototype._setupReadOnlyRoutes = function(app) {
|
||||||
var s = this;
|
var s = this;
|
||||||
|
|
||||||
app.get('/info',
|
|
||||||
s._endpointGetInfo()
|
|
||||||
);
|
|
||||||
app.get('/wallets/:walletId/utxos',
|
app.get('/wallets/:walletId/utxos',
|
||||||
s._endpointUTXOs()
|
s._endpointUTXOs()
|
||||||
);
|
);
|
||||||
|
|||||||
@ -103,6 +103,10 @@ WebService.prototype.setupAllRoutes = function() {
|
|||||||
var subApp = new express();
|
var subApp = new express();
|
||||||
var service = this.node.services[key];
|
var service = this.node.services[key];
|
||||||
|
|
||||||
|
this.app.get('/info',
|
||||||
|
this._endpointGetInfo()
|
||||||
|
);
|
||||||
|
|
||||||
if(service.getRoutePrefix && service.setupRoutes) {
|
if(service.getRoutePrefix && service.setupRoutes) {
|
||||||
this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp);
|
this.app.use('/' + this.node.services[key].getRoutePrefix(), subApp);
|
||||||
this.node.services[key].setupRoutes(subApp, express);
|
this.node.services[key].setupRoutes(subApp, express);
|
||||||
@ -275,4 +279,17 @@ WebService.prototype.transformHttpsOptions = function() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WebService.prototype._endpointGetInfo = function() {
|
||||||
|
var self = this;
|
||||||
|
return function(req, res) {
|
||||||
|
res.jsonp({
|
||||||
|
result: 'ok',
|
||||||
|
dbheight: self.node.services.block.tip.__height,
|
||||||
|
dbhash: self.node.services.block.tip.hash,
|
||||||
|
bitcoindheight: self.node.services.bitcoind.height,
|
||||||
|
bitcoindhash: self.node.services.bitcoind.tiphash
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = WebService;
|
module.exports = WebService;
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var async = require('async');
|
|
||||||
var levelup = require('levelup');
|
|
||||||
var bitcore = require('bitcore-lib');
|
|
||||||
var Transaction = bitcore.Transaction;
|
|
||||||
|
|
||||||
var MAX_TRANSACTION_LIMIT = 5;
|
|
||||||
|
|
||||||
Transaction.prototype.populateInputs = function(db, poolTransactions, callback) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(this.isCoinbase()) {
|
|
||||||
return setImmediate(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
async.eachLimit(
|
|
||||||
this.inputs,
|
|
||||||
db.maxTransactionLimit || MAX_TRANSACTION_LIMIT,
|
|
||||||
function(input, next) {
|
|
||||||
self._populateInput(db, input, poolTransactions, next);
|
|
||||||
},
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Transaction.prototype._populateInput = function(db, input, poolTransactions, callback) {
|
|
||||||
if (!input.prevTxId || !Buffer.isBuffer(input.prevTxId)) {
|
|
||||||
return callback(new Error('Input is expected to have prevTxId as a buffer'));
|
|
||||||
}
|
|
||||||
var txid = input.prevTxId.toString('hex');
|
|
||||||
db.getTransaction(txid, true, function(err, prevTx) {
|
|
||||||
if(err instanceof levelup.errors.NotFoundError) {
|
|
||||||
// Check the pool for transaction
|
|
||||||
for(var i = 0; i < poolTransactions.length; i++) {
|
|
||||||
if(txid === poolTransactions[i].hash) {
|
|
||||||
input.output = poolTransactions[i].outputs[input.outputIndex];
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback(new Error('Previous tx ' + input.prevTxId.toString('hex') + ' not found'));
|
|
||||||
} else if(err) {
|
|
||||||
callback(err);
|
|
||||||
} else {
|
|
||||||
input.output = prevTx.outputs[input.outputIndex];
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Transaction.prototype._checkSpent = function(db, input, poolTransactions, callback) {
|
|
||||||
// TODO check and see if another transaction in the pool spent the output
|
|
||||||
db.isSpentDB(input, function(spent) {
|
|
||||||
if(spent) {
|
|
||||||
return callback(new Error('Input already spent'));
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Transaction;
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var bitcore = require('bitcore-lib');
|
||||||
|
var BufferUtil = bitcore.util.buffer;
|
||||||
var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1
|
var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1
|
||||||
|
|
||||||
var utils = {};
|
var utils = {};
|
||||||
@ -76,4 +78,9 @@ utils.diffTime = function(time) {
|
|||||||
return (diff[0] * 1E9 + diff[1])/(1E9 * 1.0);
|
return (diff[0] * 1E9 + diff[1])/(1E9 * 1.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
utils.getPrevHashString = function(block) {
|
||||||
|
return BufferUtil.reverse(block.header.prevHash).toString('hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = utils;
|
module.exports = utils;
|
||||||
|
|||||||
263
regtest/block.js
Normal file
263
regtest/block.js
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var chai = require('chai');
|
||||||
|
var should = chai.should();
|
||||||
|
var async = require('async');
|
||||||
|
var BitcoinRPC = require('bitcoind-rpc');
|
||||||
|
var path = require('path');
|
||||||
|
var utils = require('./utils');
|
||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
var debug = true;
|
||||||
|
var bitcoreDataDir = '/tmp/bitcore';
|
||||||
|
var bitcoinDataDir = '/tmp/bitcoin';
|
||||||
|
|
||||||
|
var rpcConfig = {
|
||||||
|
protocol: 'http',
|
||||||
|
user: 'bitcoin',
|
||||||
|
pass: 'local321',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: '58332',
|
||||||
|
rejectUnauthorized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var bitcoin = {
|
||||||
|
args: {
|
||||||
|
datadir: bitcoinDataDir,
|
||||||
|
listen: 0,
|
||||||
|
regtest: 1,
|
||||||
|
server: 1,
|
||||||
|
rpcuser: rpcConfig.user,
|
||||||
|
rpcpassword: rpcConfig.pass,
|
||||||
|
rpcport: rpcConfig.port,
|
||||||
|
zmqpubrawtx: 'tcp://127.0.0.1:38332',
|
||||||
|
zmqpubhashblock: 'tcp://127.0.0.1:38332'
|
||||||
|
},
|
||||||
|
datadir: bitcoinDataDir,
|
||||||
|
exec: 'bitcoind', //if this isn't on your PATH, then provide the absolute path, e.g. /usr/local/bin/bitcoind
|
||||||
|
process: null
|
||||||
|
};
|
||||||
|
|
||||||
|
var bitcore = {
|
||||||
|
configFile: {
|
||||||
|
file: bitcoreDataDir + '/bitcore-node.json',
|
||||||
|
conf: {
|
||||||
|
network: 'regtest',
|
||||||
|
port: 53001,
|
||||||
|
datadir: bitcoreDataDir,
|
||||||
|
services: [
|
||||||
|
'bitcoind',
|
||||||
|
'db',
|
||||||
|
'block',
|
||||||
|
'web'
|
||||||
|
],
|
||||||
|
servicesConfig: {
|
||||||
|
bitcoind: {
|
||||||
|
connect: [
|
||||||
|
{
|
||||||
|
rpcconnect: rpcConfig.host,
|
||||||
|
rpcport: rpcConfig.port,
|
||||||
|
rpcuser: rpcConfig.user,
|
||||||
|
rpcpassword: rpcConfig.pass,
|
||||||
|
zmqpubrawtx: bitcoin.args.zmqpubrawtx
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
httpOpts: {
|
||||||
|
protocol: 'http:',
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 53001,
|
||||||
|
},
|
||||||
|
opts: { cwd: bitcoreDataDir },
|
||||||
|
datadir: bitcoreDataDir,
|
||||||
|
exec: path.resolve(__dirname, '../bin/bitcore-node'),
|
||||||
|
args: ['start'],
|
||||||
|
process: null
|
||||||
|
};
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
debug: debug,
|
||||||
|
bitcore: bitcore,
|
||||||
|
bitcoin: bitcoin,
|
||||||
|
bitcoinDataDir: bitcoinDataDir,
|
||||||
|
bitcoreDataDir: bitcoreDataDir,
|
||||||
|
rpc: new BitcoinRPC(rpcConfig),
|
||||||
|
walletPassphrase: 'test',
|
||||||
|
txCount: 0,
|
||||||
|
blockHeight: 0,
|
||||||
|
walletPrivKeys: [],
|
||||||
|
initialTxs: [],
|
||||||
|
fee: 100000,
|
||||||
|
feesReceived: 0,
|
||||||
|
satoshisSent: 0,
|
||||||
|
walletId: crypto.createHash('sha256').update('test').digest('hex'),
|
||||||
|
satoshisReceived: 0,
|
||||||
|
initialHeight: 150
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Wallet Operations', function() {
|
||||||
|
|
||||||
|
this.timeout(60000);
|
||||||
|
|
||||||
|
describe('Register, Upload, GetTransactions', function() {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
after(function(done) {
|
||||||
|
utils.cleanup(self.opts, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
before(function(done) {
|
||||||
|
self.opts = Object.assign({}, opts);
|
||||||
|
async.series([
|
||||||
|
utils.startBitcoind.bind(utils, self.opts),
|
||||||
|
utils.waitForBitcoinReady.bind(utils, self.opts),
|
||||||
|
utils.unlockWallet.bind(utils, self.opts),
|
||||||
|
utils.setupInitialTxs.bind(utils, self.opts),
|
||||||
|
utils.startBitcoreNode.bind(utils, self.opts),
|
||||||
|
utils.waitForBitcoreNode.bind(utils, self.opts)
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register wallet', function(done) {
|
||||||
|
|
||||||
|
utils.registerWallet.call(utils, self.opts, function(err, res) {
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.should.deep.equal(JSON.stringify({
|
||||||
|
walletId: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upload a wallet', function(done) {
|
||||||
|
|
||||||
|
utils.uploadWallet.call(utils, self.opts, done);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get a list of transactions', function(done) {
|
||||||
|
|
||||||
|
//the wallet should be fully uploaded and indexed by the time this happens
|
||||||
|
utils.sendTxs.call(utils, self.opts, function(err) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
utils.waitForBitcoreNode.call(utils, self.opts, function(err) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
utils.getListOfTxs.call(utils, self.opts, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Load addresses after syncing the blockchain', function() {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.opts = Object.assign({}, opts);
|
||||||
|
|
||||||
|
after(utils.cleanup.bind(utils, self.opts));
|
||||||
|
|
||||||
|
before(function(done) {
|
||||||
|
async.series([
|
||||||
|
utils.startBitcoind.bind(utils, self.opts),
|
||||||
|
utils.waitForBitcoinReady.bind(utils, self.opts),
|
||||||
|
utils.unlockWallet.bind(utils, self.opts),
|
||||||
|
utils.setupInitialTxs.bind(utils, self.opts),
|
||||||
|
utils.sendTxs.bind(utils, self.opts),
|
||||||
|
utils.startBitcoreNode.bind(utils, self.opts),
|
||||||
|
utils.waitForBitcoreNode.bind(utils, self.opts),
|
||||||
|
utils.registerWallet.bind(utils, self.opts),
|
||||||
|
utils.uploadWallet.bind(utils, self.opts)
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get list of transactions', function(done) {
|
||||||
|
|
||||||
|
utils.getListOfTxs.call(utils, self.opts, done);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the balance of a wallet', function(done) {
|
||||||
|
|
||||||
|
var httpOpts = utils.getHttpOpts.call(
|
||||||
|
utils,
|
||||||
|
self.opts,
|
||||||
|
{ path: '/wallet-api/wallets/' + self.opts.walletId + '/balance' });
|
||||||
|
|
||||||
|
utils.queryBitcoreNode.call(utils, httpOpts, function(err, res) {
|
||||||
|
if(err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
var results = JSON.parse(res);
|
||||||
|
results.satoshis.should.equal(self.opts.satoshisReceived);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the set of utxos for the wallet', function(done) {
|
||||||
|
|
||||||
|
var httpOpts = utils.getHttpOpts.call(
|
||||||
|
utils,
|
||||||
|
self.opts,
|
||||||
|
{ path: '/wallet-api/wallets/' + opts.walletId + '/utxos' });
|
||||||
|
|
||||||
|
utils.queryBitcoreNode.call(utils, httpOpts, function(err, res) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = JSON.parse(res);
|
||||||
|
var balance = 0;
|
||||||
|
|
||||||
|
results.utxos.forEach(function(utxo) {
|
||||||
|
balance += utxo.satoshis;
|
||||||
|
});
|
||||||
|
|
||||||
|
results.height.should.equal(self.opts.blockHeight);
|
||||||
|
balance.should.equal(self.opts.satoshisReceived);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the list of jobs', function(done) {
|
||||||
|
var httpOpts = utils.getHttpOpts.call(utils, self.opts, { path: '/wallet-api/jobs' });
|
||||||
|
utils.queryBitcoreNode.call(utils, httpOpts, function(err, res) {
|
||||||
|
if(err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
var results = JSON.parse(res);
|
||||||
|
results.jobCount.should.equal(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove all wallets', function(done) {
|
||||||
|
var httpOpts = utils.getHttpOpts.call(utils, self.opts, { path: '/wallet-api/wallets', method: 'DELETE' });
|
||||||
|
utils.queryBitcoreNode.call(utils, httpOpts, function(err, res) {
|
||||||
|
if(err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
var results = JSON.parse(res);
|
||||||
|
results.numberRemoved.should.equal(152);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
126
regtest/timestamp.js
Normal file
126
regtest/timestamp.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var chai = require('chai');
|
||||||
|
var should = chai.should();
|
||||||
|
var async = require('async');
|
||||||
|
var BitcoinRPC = require('bitcoind-rpc');
|
||||||
|
var path = require('path');
|
||||||
|
var utils = require('./utils');
|
||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
var debug = true;
|
||||||
|
var bitcoreDataDir = '/tmp/bitcore';
|
||||||
|
var bitcoinDataDir = '/tmp/bitcoin';
|
||||||
|
|
||||||
|
var rpcConfig = {
|
||||||
|
protocol: 'http',
|
||||||
|
user: 'bitcoin',
|
||||||
|
pass: 'local321',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: '58332',
|
||||||
|
rejectUnauthorized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var bitcoin = {
|
||||||
|
args: {
|
||||||
|
datadir: bitcoinDataDir,
|
||||||
|
listen: 0,
|
||||||
|
regtest: 1,
|
||||||
|
server: 1,
|
||||||
|
rpcuser: rpcConfig.user,
|
||||||
|
rpcpassword: rpcConfig.pass,
|
||||||
|
rpcport: rpcConfig.port,
|
||||||
|
zmqpubrawtx: 'tcp://127.0.0.1:38332',
|
||||||
|
zmqpubhashblock: 'tcp://127.0.0.1:38332'
|
||||||
|
},
|
||||||
|
datadir: bitcoinDataDir,
|
||||||
|
exec: 'bitcoind', //if this isn't on your PATH, then provide the absolute path, e.g. /usr/local/bin/bitcoind
|
||||||
|
process: null
|
||||||
|
};
|
||||||
|
|
||||||
|
var bitcore = {
|
||||||
|
configFile: {
|
||||||
|
file: bitcoreDataDir + '/bitcore-node.json',
|
||||||
|
conf: {
|
||||||
|
network: 'regtest',
|
||||||
|
port: 53001,
|
||||||
|
datadir: bitcoreDataDir,
|
||||||
|
services: [
|
||||||
|
'bitcoind',
|
||||||
|
'web',
|
||||||
|
'db',
|
||||||
|
'timestamp'
|
||||||
|
],
|
||||||
|
servicesConfig: {
|
||||||
|
bitcoind: {
|
||||||
|
connect: [
|
||||||
|
{
|
||||||
|
rpcconnect: rpcConfig.host,
|
||||||
|
rpcport: rpcConfig.port,
|
||||||
|
rpcuser: rpcConfig.user,
|
||||||
|
rpcpassword: rpcConfig.pass,
|
||||||
|
zmqpubrawtx: bitcoin.args.zmqpubrawtx
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
httpOpts: {
|
||||||
|
protocol: 'http:',
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 53001,
|
||||||
|
},
|
||||||
|
opts: { cwd: bitcoreDataDir },
|
||||||
|
datadir: bitcoreDataDir,
|
||||||
|
exec: path.resolve(__dirname, '../bin/bitcore-node'),
|
||||||
|
args: ['start'],
|
||||||
|
process: null
|
||||||
|
};
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
debug: debug,
|
||||||
|
bitcore: bitcore,
|
||||||
|
bitcoin: bitcoin,
|
||||||
|
bitcoinDataDir: bitcoinDataDir,
|
||||||
|
bitcoreDataDir: bitcoreDataDir,
|
||||||
|
rpc: new BitcoinRPC(rpcConfig),
|
||||||
|
walletPassphrase: 'test',
|
||||||
|
txCount: 0,
|
||||||
|
blockHeight: 0,
|
||||||
|
walletPrivKeys: [],
|
||||||
|
initialTxs: [],
|
||||||
|
fee: 100000,
|
||||||
|
feesReceived: 0,
|
||||||
|
satoshisSent: 0,
|
||||||
|
walletId: crypto.createHash('sha256').update('test').digest('hex'),
|
||||||
|
satoshisReceived: 0,
|
||||||
|
initialHeight: 150
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Timestamp Index', function() {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
self.timeout(60000);
|
||||||
|
|
||||||
|
after(function(done) {
|
||||||
|
utils.cleanup(self.opts, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
before(function(done) {
|
||||||
|
self.opts = Object.assign({}, opts);
|
||||||
|
async.series([
|
||||||
|
utils.startBitcoind.bind(utils, self.opts),
|
||||||
|
utils.waitForBitcoinReady.bind(utils, self.opts),
|
||||||
|
utils.unlockWallet.bind(utils, self.opts),
|
||||||
|
utils.sendTxs.bind(utils, self.opts),
|
||||||
|
utils.startBitcoreNode.bind(utils, self.opts),
|
||||||
|
utils.waitForBitcoreNode.bind(utils, self.opts),
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync timestamps', function(done) {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@ -99,7 +99,7 @@ utils.waitForBitcoreNode = function(opts, callback) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var httpOpts = self.getHttpOpts(opts, { path: '/wallet-api/info', errorFilter: errorFilter });
|
var httpOpts = self.getHttpOpts(opts, { path: '/info', errorFilter: errorFilter });
|
||||||
|
|
||||||
self.waitForService(self.queryBitcoreNode.bind(self, httpOpts), callback);
|
self.waitForService(self.queryBitcoreNode.bind(self, httpOpts), callback);
|
||||||
};
|
};
|
||||||
|
|||||||
48
test/services/address/index.unit.js
Normal file
48
test/services/address/index.unit.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var should = require('chai').should();
|
||||||
|
var bitcore = require('bitcore-lib');
|
||||||
|
var Script = bitcore.Script;
|
||||||
|
var PrivateKey = bitcore.PrivateKey;
|
||||||
|
|
||||||
|
|
||||||
|
var AddressService = require('../../../lib/services/address');
|
||||||
|
var utils = require('../../../lib/utils');
|
||||||
|
|
||||||
|
describe('Address Service', function() {
|
||||||
|
var address;
|
||||||
|
|
||||||
|
var sig = new Buffer('3045022100e8b654c91770402bf35d207406c7d4967605f99478954c8030cf7060160b5c730220296690debdd354d5fa17a61379cfdce9fdea136a4b234664e41c1c7cd840098901', 'hex');
|
||||||
|
|
||||||
|
var pks = [ new PrivateKey(), new PrivateKey() ];
|
||||||
|
|
||||||
|
var pubKeys = [ pks[0].publicKey, pks[1].publicKey ];
|
||||||
|
|
||||||
|
var scripts = {
|
||||||
|
p2pkhIn: Script.buildPublicKeyHashIn(pubKeys[0], sig),
|
||||||
|
p2pkhOut: Script.buildPublicKeyHashOut(pubKeys[0]),
|
||||||
|
p2shIn: Script.buildP2SHMultisigIn(pubKeys, 2, [sig, sig]),
|
||||||
|
p2shOut: Script.buildScriptHashOut(Script.fromAddress(pks[0].toAddress())),
|
||||||
|
p2pkIn: Script.buildPublicKeyIn(sig),
|
||||||
|
p2pkOut: Script.buildPublicKeyOut(pubKeys[0]),
|
||||||
|
p2bmsIn: Script.buildMultisigIn(pubKeys, 2, [sig, sig]),
|
||||||
|
p2bmsOut: Script.buildMultisigOut(pubKeys, 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
before(function(done) {
|
||||||
|
address = new AddressService({ node: { name: 'address' } });
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get an address from a script buffer', function() {
|
||||||
|
var start = process.hrtime();
|
||||||
|
for(var key in scripts) {
|
||||||
|
var ret = address.getAddressString({ script: scripts[key] });
|
||||||
|
console.log(ret);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(utils.diffTime(start));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
63
test/services/block/encoding.js
Normal file
63
test/services/block/encoding.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var should = require('chai').should();
|
||||||
|
|
||||||
|
var Encoding = require('../../../lib/services/block/encoding');
|
||||||
|
|
||||||
|
describe('Wallet-Api service encoding', function() {
|
||||||
|
|
||||||
|
var servicePrefix = new Buffer('0000', 'hex');
|
||||||
|
var encoding = new Encoding(servicePrefix);
|
||||||
|
var block = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
|
||||||
|
var height = 1;
|
||||||
|
|
||||||
|
it('should encode block hash key' , function() {
|
||||||
|
encoding.encodeBlockHashKey(block).should.deep.equal(Buffer.concat([
|
||||||
|
servicePrefix,
|
||||||
|
new Buffer('00', 'hex'),
|
||||||
|
new Buffer(block, 'hex')
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decode block hash key' , function() {
|
||||||
|
encoding.decodeBlockHashKey(Buffer.concat([
|
||||||
|
servicePrefix,
|
||||||
|
new Buffer('00', 'hex'),
|
||||||
|
new Buffer(block, 'hex')
|
||||||
|
])).should.deep.equal(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode block hash value', function() {
|
||||||
|
encoding.encodeBlockHashValue(block).should.deep.equal(
|
||||||
|
new Buffer(block, 'hex'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shound decode block hash value', function() {
|
||||||
|
encoding.decodeBlockHashValue(new Buffer(block, 'hex')).should.deep.equal(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode block height key', function() {
|
||||||
|
encoding.encodeBlockHeightKey(height).should.deep.equal(Buffer.concat([
|
||||||
|
servicePrefix,
|
||||||
|
new Buffer('01', 'hex'),
|
||||||
|
new Buffer('00000001', 'hex')
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decode block height key', function() {
|
||||||
|
encoding.decodeBlockHeightKey(Buffer.concat([
|
||||||
|
servicePrefix,
|
||||||
|
new Buffer('01', 'hex'),
|
||||||
|
new Buffer('00000001', 'hex')
|
||||||
|
])).should.deep.equal(height);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode block height value', function() {
|
||||||
|
encoding.encodeBlockHeightValue(height).should.deep.equal(new Buffer('00000001', 'hex'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decode block height value', function() {
|
||||||
|
encoding.decodeBlockHeightValue(new Buffer('00000001', 'hex')).should.deep.equal(height);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user