wip
This commit is contained in:
parent
1dbba6708d
commit
98ea052405
@ -1,6 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var assert = require('assert');
|
|
||||||
var BaseService = require('../../service');
|
var BaseService = require('../../service');
|
||||||
var inherits = require('util').inherits;
|
var inherits = require('util').inherits;
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
@ -24,12 +23,92 @@ inherits(AddressService, BaseService);
|
|||||||
AddressService.dependencies = [
|
AddressService.dependencies = [
|
||||||
'bitcoind',
|
'bitcoind',
|
||||||
'db',
|
'db',
|
||||||
|
'block',
|
||||||
'transaction'
|
'transaction'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ---- public function prototypes
|
||||||
|
AddressService.prototype.getBalance = function(address, queryMempool, callback) {
|
||||||
|
this.getUtxos(address, queryMempool, function(err, outputs) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var satoshis = outputs.map(function(output) {
|
||||||
|
return output.satoshis;
|
||||||
|
});
|
||||||
|
|
||||||
|
var sum = satoshis.reduce(function(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return callback(null, sum);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(!Array.isArray(addresses)) {
|
||||||
|
addresses = [addresses];
|
||||||
|
}
|
||||||
|
|
||||||
|
var utxos = [];
|
||||||
|
|
||||||
|
async.eachSeries(addresses, function(address, next) {
|
||||||
|
self.getUtxosForAddress(address, queryMempool, function(err, unspents) {
|
||||||
|
if(err && err instanceof errors.NoOutputs) {
|
||||||
|
return next();
|
||||||
|
} else if(err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
utxos = utxos.concat(unspents);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
callback(err, utxos);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
AddressService.prototype.getUtxosForAddress = function(address, queryMempool, callback) {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var stream = self.db.createReadStream({
|
||||||
|
gte: self._encoding.encodeUtxoIndexKey(address),
|
||||||
|
lt: self._encoding.encodeUtxoIndexKey(utils.getTerminalKey(new Buffer(address)))
|
||||||
|
});
|
||||||
|
|
||||||
|
var utxos = [];
|
||||||
|
stream.on('data', function(data) {
|
||||||
|
var key = self._encoding.decodeUtxoIndexKey(data.key);
|
||||||
|
var value = self._encoding.decodeUtxoIndexValue(data.value);
|
||||||
|
utxos.push({
|
||||||
|
address: key.address,
|
||||||
|
txid: key.txid,
|
||||||
|
outputIndex: key.outputIndex,
|
||||||
|
satoshis: value.satoshis,
|
||||||
|
height: value.height,
|
||||||
|
script: value.script
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', function() {
|
||||||
|
return callback(null, utxos);
|
||||||
|
});
|
||||||
|
stream.on('error', function(err) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
AddressService.prototype.start = function(callback) {
|
AddressService.prototype.start = function(callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
self._setListeners();
|
||||||
|
|
||||||
this.db = this.node.services.db;
|
this.db = this.node.services.db;
|
||||||
this.db.getPrefix(this.name, function(err, prefix) {
|
this.db.getPrefix(this.name, function(err, prefix) {
|
||||||
@ -60,23 +139,26 @@ AddressService.prototype.getAPIMethods = function() {
|
|||||||
AddressService.prototype.getAddressBalance = function(addresses, options, callback) {
|
AddressService.prototype.getAddressBalance = function(addresses, options, callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
var addresses = self._normalizeAddressArg(addressArg);
|
addresses = utils.normalizeAddressArg(addresses);
|
||||||
var cacheKey = addresses.join('');
|
var balance = 0;
|
||||||
var balance = self.balanceCache.get(cacheKey);
|
|
||||||
|
async.eachLimit(addresses, 4, function(address, next) {
|
||||||
|
|
||||||
|
var start = self._encoding.encodeUtxoIndexKey(address);
|
||||||
|
var criteria = {
|
||||||
|
gte: start,
|
||||||
|
lte: Buffer.concat([ start.slice(-36), new Buffer(new Array(73).join('f'), 'hex') ])
|
||||||
|
};
|
||||||
|
|
||||||
|
var stream = this._db.createReadStream(criteria);
|
||||||
|
stream.on('data', function(data) {
|
||||||
|
|
||||||
if (balance) {
|
|
||||||
return setImmediate(function() {
|
|
||||||
callback(null, balance);
|
|
||||||
});
|
});
|
||||||
} else {
|
stream.on('error', function(err) {
|
||||||
this.client.getAddressBalance({addresses: addresses}, function(err, response) {
|
|
||||||
if (err) {
|
|
||||||
return callback(self._wrapRPCError(err));
|
|
||||||
}
|
|
||||||
self.balanceCache.set(cacheKey, response.result);
|
|
||||||
callback(null, response.result);
|
|
||||||
});
|
});
|
||||||
}
|
stream.on('end', function() {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -271,6 +353,87 @@ AddressService.prototype.getAddressUnspentOutputs = function(address, options, c
|
|||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype.syncPercentage = function(callback) {
|
AddressService.prototype.syncPercentage = function(callback) {
|
||||||
|
return callback(null, ((this._tip.height / this._block.getBestBlockHeight()) * 100).toFixed(2) + '%');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
AddressService.prototype.getAddressTxidsWithHeights = function(address, options, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var opts = options || {};
|
||||||
|
var txids = {};
|
||||||
|
|
||||||
|
var start = self._encoding.encodeAddressIndexKey(address, opts.start || 0);
|
||||||
|
var end = Buffer.concat([ start.slice(0, -36), new Buffer((opts.end || 'ffffffff'), 'hex') ]);
|
||||||
|
|
||||||
|
var stream = self.db.createKeyStream({
|
||||||
|
gte: start,
|
||||||
|
lt: end
|
||||||
|
});
|
||||||
|
|
||||||
|
var streamErr = null;
|
||||||
|
|
||||||
|
stream.on('data', function(buffer) {
|
||||||
|
var key = self._encoding.decodeAddressIndexKey(buffer);
|
||||||
|
txids[key.txid] = key.height;
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', function() {
|
||||||
|
callback(streamErr, txids);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', function(err) {
|
||||||
|
streamErr = err;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- private function prototypes
|
||||||
|
AddressService.prototype._setListeners = function() {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self._db.on('error', self._onDbError.bind(self));
|
||||||
|
self.on('reorg', self._handleReorg.bind(self));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
AddressService.prototype._startSubscriptions = function() {
|
||||||
|
|
||||||
|
if (this._subscribed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._subscribed = true;
|
||||||
|
if (!this._bus) {
|
||||||
|
this._bus = this.node.openBus({remoteAddress: 'localhost'});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bus.on('block/block', this._onBlock.bind(this));
|
||||||
|
this._bus.subscribe('block/block');
|
||||||
|
};
|
||||||
|
|
||||||
|
AddressService.prototype._onBlock = function(block) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var operations = [];
|
||||||
|
|
||||||
|
block.transactions.forEach(function(tx) {
|
||||||
|
operations.concat(self._processTransaction(tx, { block: block, connect: connect }));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (operations && operations.length > 0) {
|
||||||
|
|
||||||
|
self._db.batch(operations, function(err) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
log.error('Address Service: Error saving block with hash: ' + block.hash);
|
||||||
|
this._db.emit('error', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug('Address Service: Success saving block hash ' + block.hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype._processInput = function(opts, input) {
|
AddressService.prototype._processInput = function(opts, input) {
|
||||||
@ -290,7 +453,6 @@ AddressService.prototype._processInput = function(opts, input) {
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
// prev utxo
|
// prev utxo
|
||||||
// TODO: ensure this is a good link backward
|
|
||||||
var rec = {
|
var rec = {
|
||||||
type: opts.action,
|
type: opts.action,
|
||||||
key: this._encoding.encodeUtxoIndexKey(address, input.prevTxId.toString('hex'), input.outputIndex)
|
key: this._encoding.encodeUtxoIndexKey(address, input.prevTxId.toString('hex'), input.outputIndex)
|
||||||
@ -362,140 +524,6 @@ AddressService.prototype._processTransaction = function(opts, tx) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype.onBlock = function(block, connect) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var operations = [];
|
|
||||||
|
|
||||||
block.transactions.forEach(function(tx) {
|
|
||||||
operations.concat(self._processTransaction(tx, { block: block, connect: connect }));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (operations && operations.length > 0) {
|
|
||||||
|
|
||||||
self._db.batch(operations, function(err) {
|
|
||||||
|
|
||||||
if(err) {
|
|
||||||
log.error('Address Service: Error saving block with hash: ' + block.hash);
|
|
||||||
this._db.emit('error', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug('Address Service: Success saving block hash ' + block.hash);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype.getBalance = function(address, queryMempool, callback) {
|
|
||||||
this.getUtxos(address, queryMempool, function(err, outputs) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
var satoshis = outputs.map(function(output) {
|
|
||||||
return output.satoshis;
|
|
||||||
});
|
|
||||||
|
|
||||||
var sum = satoshis.reduce(function(a, b) {
|
|
||||||
return a + b;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return callback(null, sum);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(!Array.isArray(addresses)) {
|
|
||||||
addresses = [addresses];
|
|
||||||
}
|
|
||||||
|
|
||||||
var utxos = [];
|
|
||||||
|
|
||||||
async.eachSeries(addresses, function(address, next) {
|
|
||||||
self.getUtxosForAddress(address, queryMempool, function(err, unspents) {
|
|
||||||
if(err && err instanceof errors.NoOutputs) {
|
|
||||||
return next();
|
|
||||||
} else if(err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
utxos = utxos.concat(unspents);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}, function(err) {
|
|
||||||
callback(err, utxos);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype.getUtxosForAddress = function(address, queryMempool, callback) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var stream = self.db.createReadStream({
|
|
||||||
gte: self._encoding.encodeUtxoIndexKey(address),
|
|
||||||
lt: self._encoding.encodeUtxoIndexKey(utils.getTerminalKey(new Buffer(address)))
|
|
||||||
});
|
|
||||||
|
|
||||||
var utxos = [];
|
|
||||||
stream.on('data', function(data) {
|
|
||||||
var key = self._encoding.decodeUtxoIndexKey(data.key);
|
|
||||||
var value = self._encoding.decodeUtxoIndexValue(data.value);
|
|
||||||
utxos.push({
|
|
||||||
address: key.address,
|
|
||||||
txid: key.txid,
|
|
||||||
outputIndex: key.outputIndex,
|
|
||||||
satoshis: value.satoshis,
|
|
||||||
height: value.height,
|
|
||||||
script: value.script
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('end', function() {
|
|
||||||
return callback(null, utxos);
|
|
||||||
});
|
|
||||||
stream.on('error', function(err) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype.getAddressTxidsWithHeights = function(address, options, callback) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var opts = options || {};
|
|
||||||
var txids = {};
|
|
||||||
|
|
||||||
var start = self._encoding.encodeAddressIndexKey(address, opts.start || 0); //the start and end must be the same length
|
|
||||||
var end = Buffer.concat([ start.slice(0, -36), new Buffer((opts.end || 'ffffffff'), 'hex') ]);
|
|
||||||
|
|
||||||
var stream = self.db.createKeyStream({
|
|
||||||
gte: start,
|
|
||||||
lt: end
|
|
||||||
});
|
|
||||||
|
|
||||||
var streamErr = null;
|
|
||||||
|
|
||||||
stream.on('data', function(buffer) {
|
|
||||||
var key = self._encoding.decodeAddressIndexKey(buffer);
|
|
||||||
assert(key.txid.length === 64, 'AddressService, Txid: ' + key.txid + ' with length: ' + key.txid.length + ' does not resemble a txid.');
|
|
||||||
txids[key.txid] = key.height;
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('end', function() {
|
|
||||||
callback(streamErr, txids);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('error', function(err) {
|
|
||||||
streamErr = err;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = AddressService;
|
module.exports = AddressService;
|
||||||
|
|||||||
@ -29,7 +29,7 @@ var BlockService = function(options) {
|
|||||||
// meta is [{ chainwork: chainwork, hash: hash }]
|
// meta is [{ chainwork: chainwork, hash: hash }]
|
||||||
this._meta = [];
|
this._meta = [];
|
||||||
|
|
||||||
// this is the in-memory full/raw block cache
|
// in-memory full/raw block cache
|
||||||
this._blockQueue = LRU({
|
this._blockQueue = LRU({
|
||||||
max: 50 * (1 * 1024 * 1024), // 50 MB of blocks,
|
max: 50 * (1 * 1024 * 1024), // 50 MB of blocks,
|
||||||
length: function(n) {
|
length: function(n) {
|
||||||
@ -39,6 +39,7 @@ var BlockService = function(options) {
|
|||||||
|
|
||||||
// keep track of out-of-order blocks, this is a list of chains (which are lists themselves)
|
// keep track of out-of-order blocks, this is a list of chains (which are lists themselves)
|
||||||
// e.g. [ [ block5, block4 ], [ block8, block7 ] ];
|
// e.g. [ [ block5, block4 ], [ block8, block7 ] ];
|
||||||
|
// TODO: persist this to disk, we can't hold too many blocks in memory
|
||||||
this._incompleteChains = [];
|
this._incompleteChains = [];
|
||||||
// list of all chain tips, including main chain and any chains that were orphaned after a reorg
|
// list of all chain tips, including main chain and any chains that were orphaned after a reorg
|
||||||
this._chainTips = [];
|
this._chainTips = [];
|
||||||
@ -61,14 +62,13 @@ BlockService.prototype.getAPIMethods = function() {
|
|||||||
['getRawBlock', this, this.getRawBlock, 1],
|
['getRawBlock', this, this.getRawBlock, 1],
|
||||||
['getBlockHeader', this, this.getBlockHeader, 1],
|
['getBlockHeader', this, this.getBlockHeader, 1],
|
||||||
['getBlockOverview', this, this.getBlockOverview, 1],
|
['getBlockOverview', this, this.getBlockOverview, 1],
|
||||||
['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2],
|
|
||||||
['getBestBlockHash', this, this.getBestBlockHash, 0]
|
['getBestBlockHash', this, this.getBestBlockHash, 0]
|
||||||
];
|
];
|
||||||
return methods;
|
return methods;
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype.getBestBlockHash = function(callback) {
|
BlockService.prototype.getBestBlockHash = function() {
|
||||||
callback(this._meta[this._meta.length - 1].hash);
|
return this._meta[this._meta.length - 1].hash;
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype.getBlock = function(hash, callback) {
|
BlockService.prototype.getBlock = function(hash, callback) {
|
||||||
@ -81,27 +81,10 @@ BlockService.prototype.getBlock = function(hash, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype.getBlockHashesByTimestamp = function(high, low, options, callback) {
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
if (_.isFunction(options)) {
|
|
||||||
callback = options;
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.getBlockHashes(high, low, options, function(err, response) {
|
|
||||||
if (err) {
|
|
||||||
return callback(self._wrapRPCError(err));
|
|
||||||
}
|
|
||||||
callback(null, response.result);
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
BlockService.prototype._getHash = function(blockArg) {
|
BlockService.prototype._getHash = function(blockArg) {
|
||||||
|
|
||||||
(_.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg))) &&
|
return (_.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg))) &&
|
||||||
this._meta[blockArg] ? meta.hash : null;
|
this._meta[blockArg] ? this._meta[blockArg] : null;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,8 +92,11 @@ BlockService.prototype.getBlockHeader = function(blockArg, callback) {
|
|||||||
|
|
||||||
blockArg = this._getHash(blockArg);
|
blockArg = this._getHash(blockArg);
|
||||||
|
|
||||||
// by hash
|
if (!blockArg) {
|
||||||
this._getBlock(meta.hash, function(err, block) {
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getBlock(blockArg, function(err, block) {
|
||||||
|
|
||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
@ -126,49 +112,40 @@ BlockService.prototype.getBlockHeader = function(blockArg, callback) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype.getBlockOverview = function(hash, callback) {
|
BlockService.prototype._getBlock = function(hash, callback) {
|
||||||
var self = this;
|
var block = this._blockQueue(hash);
|
||||||
|
if (block) {
|
||||||
|
return callback(null, block);
|
||||||
|
}
|
||||||
|
this._db.get(this._encoding.encodeBlockKey(hash), callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.getBlockOverview = function(hash, callback) {
|
||||||
|
|
||||||
|
this._getBlock(hash, function(err, block) {
|
||||||
|
|
||||||
function queryBlock(err, blockhash) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
var cachedBlock = self.blockOverviewCache.get(blockhash);
|
|
||||||
if (cachedBlock) {
|
|
||||||
return setImmediate(function() {
|
|
||||||
callback(null, cachedBlock);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self._tryAllClients(function(client, done) {
|
|
||||||
client.getBlock(blockhash, true, function(err, response) {
|
|
||||||
if (err) {
|
|
||||||
return done(self._wrapRPCError(err));
|
|
||||||
}
|
|
||||||
var result = response.result;
|
|
||||||
var blockOverview = {
|
|
||||||
hash: result.hash,
|
|
||||||
version: result.version,
|
|
||||||
confirmations: result.confirmations,
|
|
||||||
height: result.height,
|
|
||||||
chainWork: result.chainwork,
|
|
||||||
prevHash: result.previousblockhash,
|
|
||||||
nextHash: result.nextblockhash,
|
|
||||||
merkleRoot: result.merkleroot,
|
|
||||||
time: result.time,
|
|
||||||
medianTime: result.mediantime,
|
|
||||||
nonce: result.nonce,
|
|
||||||
bits: result.bits,
|
|
||||||
difficulty: result.difficulty,
|
|
||||||
txids: result.tx
|
|
||||||
};
|
|
||||||
self.blockOverviewCache.set(blockhash, blockOverview);
|
|
||||||
done(null, blockOverview);
|
|
||||||
});
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self._maybeGetBlockHash(blockArg, queryBlock);
|
var blockOverview = {
|
||||||
|
hash: block.hash,
|
||||||
|
version: block.header.version,
|
||||||
|
confirmations: null,
|
||||||
|
height: null,
|
||||||
|
chainWork: null,
|
||||||
|
prevHash: utils.reverseBufferToString(block.header.prevHash),
|
||||||
|
nextHash: null,
|
||||||
|
merkleRoot: block.header.merkleroot,
|
||||||
|
time: null,
|
||||||
|
medianTime: null,
|
||||||
|
nonce: block.header.nonce,
|
||||||
|
bits: block.header.bits,
|
||||||
|
difficulty: null,
|
||||||
|
txids: null
|
||||||
|
};
|
||||||
|
callback(null, blockOverview);
|
||||||
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -415,7 +392,6 @@ BlockService.prototype._getIncompleteChainIndexes = function(block) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype._handleReorg = function(block) {
|
BlockService.prototype._handleReorg = function(block) {
|
||||||
|
|
||||||
this._reorging = true;
|
this._reorging = true;
|
||||||
log.warn('Chain reorganization detected! Our current block tip is: ' +
|
log.warn('Chain reorganization detected! Our current block tip is: ' +
|
||||||
this._tip.hash + ' the current block: ' + block.hash + '.');
|
this._tip.hash + ' the current block: ' + block.hash + '.');
|
||||||
@ -431,9 +407,12 @@ BlockService.prototype._handleReorg = function(block) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.warn('A common ancestor block was found to at hash: ' + commonAncestor + '.');
|
log.warn('A common ancestor block was found to at hash: ' + commonAncestor + '.');
|
||||||
this._broadcast(this.subscriptions.reorg, 'block/reorg', [block, commonAncestor]);
|
this._broadcast(this.subscriptions.reorg, 'block/reorg', [commonAncestor, [block]]);
|
||||||
|
this._onReorg(commonAncestor, [block]);
|
||||||
this._reorging = false;
|
this._reorging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype._onReorg = function(commonAncestor, newBlockList) {
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype._isChainReorganizing = function(block) {
|
BlockService.prototype._isChainReorganizing = function(block) {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
var BaseService = require('../../service');
|
var BaseService = require('../../service');
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var bitcore = require('bitcore-lib');
|
|
||||||
var Encoding = require('./encoding');
|
var Encoding = require('./encoding');
|
||||||
var index = require('../../index');
|
var index = require('../../index');
|
||||||
var log = index.log;
|
var log = index.log;
|
||||||
@ -49,9 +48,9 @@ MempoolService.prototype._setListeners = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
MempoolService.prototype._startSubscriptions = function() {
|
MempoolService.prototype._startSubscriptions = function() {
|
||||||
var bus = self.node.openBus({ remoteAddress: 'localhost' });
|
var bus = this.node.openBus({ remoteAddress: 'localhost' });
|
||||||
bus.on('transaction/transaction', this._onTransaction.bind(self));
|
bus.on('p2p/transaction', this._onTransaction.bind(this));
|
||||||
bus.subscribe('tranaction/transaction');
|
bus.subscribe('p2p/transaction');
|
||||||
};
|
};
|
||||||
|
|
||||||
MempoolService.prototype._onTransaction = function(tx) {
|
MempoolService.prototype._onTransaction = function(tx) {
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Encoding = require('./encoding');
|
var Encoding = require('./encoding');
|
||||||
var BaseService = require('../../service');
|
|
||||||
var inherits = require('util').inherits;
|
var inherits = require('util').inherits;
|
||||||
var LRU = require('lru-cache');
|
var LRU = require('lru-cache');
|
||||||
var utils = require('../../../lib/utils');
|
var utils = require('../../../lib/utils');
|
||||||
|
|
||||||
function TimestampService(options) {
|
function TimestampService(options) {
|
||||||
BaseService.call(this, options);
|
BaseService.call(this, options);
|
||||||
this.currentBlock = null;
|
this._db = this.node.services.db;
|
||||||
this.currentTimestamp = null;
|
this._tip = null;
|
||||||
this._createConcurrencyCache();
|
|
||||||
this._concurrencyCache.set(new Array(65).join('0'), { valueItem: 0 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inherits(TimestampService, BaseService);
|
inherits(TimestampService, BaseService);
|
||||||
@ -20,69 +18,111 @@ TimestampService.dependencies = [ 'db', 'block' ];
|
|||||||
|
|
||||||
TimestampService.prototype.getAPIMethods = function() {
|
TimestampService.prototype.getAPIMethods = function() {
|
||||||
return [
|
return [
|
||||||
|
['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2],
|
||||||
['syncPercentage', this, this.syncPercentage, 0]
|
['syncPercentage', this, this.syncPercentage, 0]
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TimestampService.prototype.syncPercentage = function(callback) {
|
||||||
|
};
|
||||||
|
|
||||||
|
TimestampService.prototype.getBlockHashesByTimestamp = function(callback) {
|
||||||
|
};
|
||||||
|
|
||||||
TimestampService.prototype.start = function(callback) {
|
TimestampService.prototype.start = function(callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
self._setListeners();
|
||||||
|
|
||||||
this.db = this.node.services.db;
|
self._db.getPrefix(self.name, function(err, prefix) {
|
||||||
|
|
||||||
this.node.services.db.getPrefix(this.name, function(err, prefix) {
|
|
||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.prefix = prefix;
|
self._prefix = prefix;
|
||||||
self.encoding = new Encoding(self.prefix);
|
self._encoding = new Encoding(self._prefix);
|
||||||
callback();
|
|
||||||
|
self._db.getServiceTip(self.name, function(err, tip) {
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
self._tip = tip;
|
||||||
|
self._startSubscriptions();
|
||||||
|
callback();
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TimestampService.prototype._startSubscriptions = function() {
|
||||||
|
|
||||||
|
if (this._subscribed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._subscribed = true;
|
||||||
|
if (!this._bus) {
|
||||||
|
this._bus = this.node.openBus({remoteAddress: 'localhost'});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bus.on('block/block', this._onBlock.bind(this));
|
||||||
|
this._bus.subscribe('block/block');
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockService.prototype._sync = function() {
|
||||||
|
|
||||||
|
if (--this._p2pBlockCallsNeeded > 0) {
|
||||||
|
|
||||||
|
log.info('Blocks download progress: ' + this._numCompleted + '/' +
|
||||||
|
this._numNeeded + ' (' + (this._numCompleted/this._numNeeded*100).toFixed(2) + '%)');
|
||||||
|
this._p2p.getBlocks({ startHash: this._latestBlockHash });
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
TimestampService.prototype._setListeners = function() {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self._db.on('error', self._onDbError.bind(self));
|
||||||
|
self.on('reorg', self._handleReorg.bind(self));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
TimestampService.prototype._setTip = function(tip) {
|
||||||
|
log.debug('Timestamp Service: Setting tip to height: ' + tip.height);
|
||||||
|
log.debug('Timestamp Service: Setting tip to hash: ' + tip.hash);
|
||||||
|
this._tip = tip;
|
||||||
|
this._db.setServiceTip('block', this._tip);
|
||||||
|
};
|
||||||
|
|
||||||
TimestampService.prototype.stop = function(callback) {
|
TimestampService.prototype.stop = function(callback) {
|
||||||
setImmediate(callback);
|
setImmediate(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
TimestampService.prototype.concurrentBlockHandler = function(block, connectBlock, callback) {
|
TimestampService.prototype._onBlock = function(block) {
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var action = connectBlock ? 'put' : 'del';
|
|
||||||
|
|
||||||
var filter = function(newBlockTime, prevBlockTime) {
|
|
||||||
if (newBlockTime <= prevBlockTime) {
|
|
||||||
return prevBlockTime + 1;
|
|
||||||
}
|
|
||||||
return newBlockTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
var prevHash = utils.reverseBufferToString(block.header.prevHash);
|
var prevHash = utils.reverseBufferToString(block.header.prevHash);
|
||||||
var hash = block.hash;
|
|
||||||
var queue = self._retrieveCachedItems(hash, block.header.timestamp, prevHash, filter);
|
|
||||||
|
|
||||||
var operations = [];
|
var operations = [];
|
||||||
|
|
||||||
if (queue.length === 0) {
|
operations = operations.concat([
|
||||||
return callback(null, queue);
|
{
|
||||||
}
|
type: action,
|
||||||
|
key: self.encoding.encodeTimestampBlockKey(item.value),
|
||||||
for(var i = 0; i < queue.length; i++) {
|
value: self.encoding.encodeTimestampBlockValue(item.key)
|
||||||
|
},
|
||||||
var item = queue[i];
|
{
|
||||||
operations = operations.concat([
|
type: action,
|
||||||
{
|
key: self.encoding.encodeBlockTimestampKey(item.key),
|
||||||
type: action,
|
value: self.encoding.encodeBlockTimestampValue(item.value)
|
||||||
key: self.encoding.encodeTimestampBlockKey(item.value),
|
}
|
||||||
value: self.encoding.encodeTimestampBlockValue(item.key)
|
]);
|
||||||
},
|
|
||||||
{
|
|
||||||
type: action,
|
|
||||||
key: self.encoding.encodeBlockTimestampKey(item.key),
|
|
||||||
value: self.encoding.encodeBlockTimestampValue(item.value)
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, operations);
|
callback(null, operations);
|
||||||
|
|
||||||
@ -119,4 +159,18 @@ TimestampService.prototype._getValue = function(key, callback) {
|
|||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TimestampService.prototype._onReorg = function(commonAncestor, newBlockList) {
|
||||||
|
};
|
||||||
|
|
||||||
|
TimestampService.prototype.getBlockHashesByTimestamp = function(high, low, options, callback) {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
if (_.isFunction(options)) {
|
||||||
|
callback = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = TimestampService;
|
module.exports = TimestampService;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ inherits(TransactionService, BaseService);
|
|||||||
|
|
||||||
TransactionService.dependencies = [
|
TransactionService.dependencies = [
|
||||||
'db',
|
'db',
|
||||||
|
'block',
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'mempool'
|
'mempool'
|
||||||
];
|
];
|
||||||
@ -32,6 +33,9 @@ TransactionService.prototype.getAPIMethods = function() {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TransactionService.prototype.getSpentInfo = function(txid, callback) {
|
||||||
|
};
|
||||||
|
|
||||||
TransactionService.prototype.getRawTransaction = function(txid, callback) {
|
TransactionService.prototype.getRawTransaction = function(txid, callback) {
|
||||||
this.getTransaction(txid, function(err, tx) {
|
this.getTransaction(txid, function(err, tx) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -45,14 +49,6 @@ TransactionService.prototype.getDetailedTransaction = TransactionService.prototy
|
|||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
assert(txid.length === 64, 'Transaction, Txid: ' + txid + ' with length: ' + txid.length + ' does not resemble a txid.');
|
|
||||||
|
|
||||||
if(self.currentTransactions[txid]) {
|
|
||||||
return setImmediate(function() {
|
|
||||||
callback(null, self.currentTransactions[txid]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = self.encoding.encodeTransactionKey(txid);
|
var key = self.encoding.encodeTransactionKey(txid);
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
@ -144,8 +140,24 @@ TransactionService.prototype.sendTransaction = function(tx, callback) {
|
|||||||
this._p2p.sendTransaction(tx, callback);
|
this._p2p.sendTransaction(tx, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TransactionService.prototype._setListeners = function() {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self._db.on('error', self._onDbError.bind(self));
|
||||||
|
self.on('reorg', self._onReorg.bind(self));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
TransactionService.prototype._onDbError = function(error) {
|
||||||
|
};
|
||||||
|
|
||||||
|
TransactionService.prototype._onReorg = function(commonAncestor, newBlockList) {
|
||||||
|
};
|
||||||
|
|
||||||
TransactionService.prototype.start = function(callback) {
|
TransactionService.prototype.start = function(callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
self._setListeners();
|
||||||
|
|
||||||
self._db.getPrefix(self.name, function(err, prefix) {
|
self._db.getPrefix(self.name, function(err, prefix) {
|
||||||
|
|
||||||
@ -153,7 +165,7 @@ TransactionService.prototype.start = function(callback) {
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
self._db.getServiceTip('transaction', function(err, tip) {
|
self._db.getServiceTip(self.name, function(err, tip) {
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
@ -162,12 +174,31 @@ TransactionService.prototype.start = function(callback) {
|
|||||||
self._tip = tip;
|
self._tip = tip;
|
||||||
self.prefix = prefix;
|
self.prefix = prefix;
|
||||||
self.encoding = new Encoding(self.prefix);
|
self.encoding = new Encoding(self.prefix);
|
||||||
|
self._startSubscriptions();
|
||||||
callback();
|
callback();
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TransactionService.prototype._onTransaction = function(transaction) {
|
||||||
|
};
|
||||||
|
|
||||||
|
TransactionService.prototype._startSubscriptions = function() {
|
||||||
|
|
||||||
|
if (this._subscribed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._subscribed = true;
|
||||||
|
if (!this._bus) {
|
||||||
|
this._bus = this.node.openBus({remoteAddress: 'localhost'});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bus.on('block/block', this._onTransaction.bind(this));
|
||||||
|
this._bus.subscribe('block/block');
|
||||||
|
};
|
||||||
|
|
||||||
TransactionService.prototype.stop = function(callback) {
|
TransactionService.prototype.stop = function(callback) {
|
||||||
setImmediate(callback);
|
setImmediate(callback);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,246 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var bitcore = require('bitcore-lib');
|
|
||||||
var BufferReader = bitcore.encoding.BufferReader;
|
|
||||||
|
|
||||||
function Encoding(servicePrefix) {
|
|
||||||
this.servicePrefix = servicePrefix;
|
|
||||||
this.subKeyMap = {
|
|
||||||
transaction: {
|
|
||||||
fn: this.encodeWalletTransactionKey,
|
|
||||||
buffer: new Buffer('00', 'hex')
|
|
||||||
},
|
|
||||||
addresses: {
|
|
||||||
fn: this.encodeWalletAddressesKey,
|
|
||||||
buffer: new Buffer('01', 'hex')
|
|
||||||
},
|
|
||||||
utxo: {
|
|
||||||
fn: this.encodeWalletUtxoKey,
|
|
||||||
buffer: new Buffer('02', 'hex')
|
|
||||||
},
|
|
||||||
utxoSat: {
|
|
||||||
fn: this.encodeWalletUtxoSatoshisKey,
|
|
||||||
buffer: new Buffer('03', 'hex')
|
|
||||||
},
|
|
||||||
balance: {
|
|
||||||
fn: this.encodeWalletBalanceKey,
|
|
||||||
buffer: new Buffer('04', 'hex')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletTransactionKey = function(walletId, height, txid) {
|
|
||||||
var buffers = [this.servicePrefix, this.subKeyMap.transaction.buffer];
|
|
||||||
|
|
||||||
var walletIdSizeBuffer = new Buffer(1);
|
|
||||||
walletIdSizeBuffer.writeUInt8(walletId.length);
|
|
||||||
var walletIdBuffer = new Buffer(walletId, 'utf8');
|
|
||||||
|
|
||||||
buffers.push(walletIdSizeBuffer);
|
|
||||||
buffers.push(walletIdBuffer);
|
|
||||||
|
|
||||||
var heightBuffer = new Buffer(4);
|
|
||||||
heightBuffer.writeUInt32BE(height || 0);
|
|
||||||
buffers.push(heightBuffer);
|
|
||||||
|
|
||||||
var txidBuffer = new Buffer((txid || new Array(65).join('0')), 'hex');
|
|
||||||
buffers.push(txidBuffer);
|
|
||||||
|
|
||||||
return Buffer.concat(buffers);
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletTransactionKey = function(buffer) {
|
|
||||||
var reader = new BufferReader(buffer);
|
|
||||||
reader.read(3);
|
|
||||||
|
|
||||||
var walletSize = reader.readUInt8();
|
|
||||||
var walletId = reader.read(walletSize).toString('utf8');
|
|
||||||
var height = reader.readUInt32BE();
|
|
||||||
|
|
||||||
var txid = reader.read(32);
|
|
||||||
|
|
||||||
return {
|
|
||||||
walletId: walletId,
|
|
||||||
height: height,
|
|
||||||
txid: txid
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletUtxoKey = function(walletId, txid, outputIndex) {
|
|
||||||
var buffers = [this.servicePrefix, this.subKeyMap.utxo.buffer];
|
|
||||||
|
|
||||||
var walletIdSizeBuffer = new Buffer(1);
|
|
||||||
walletIdSizeBuffer.writeUInt8(walletId.length);
|
|
||||||
var walletIdBuffer = new Buffer(walletId, 'utf8');
|
|
||||||
|
|
||||||
buffers.push(walletIdSizeBuffer);
|
|
||||||
buffers.push(walletIdBuffer);
|
|
||||||
|
|
||||||
var txidBuffer = new Buffer(txid || new Array(65).join('0'), 'hex');
|
|
||||||
buffers.push(txidBuffer);
|
|
||||||
|
|
||||||
var outputIndexBuffer = new Buffer(4);
|
|
||||||
outputIndexBuffer.writeUInt32BE(outputIndex || 0);
|
|
||||||
buffers.push(outputIndexBuffer);
|
|
||||||
|
|
||||||
return Buffer.concat(buffers);
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletUtxoKey = function(buffer) {
|
|
||||||
var reader = new BufferReader(buffer);
|
|
||||||
reader.read(3);
|
|
||||||
|
|
||||||
var walletIdSize = reader.readUInt8();
|
|
||||||
var walletId = reader.read(walletIdSize).toString('utf8');
|
|
||||||
var txid = reader.read(32).toString('hex');
|
|
||||||
var outputIndex = reader.readUInt32BE();
|
|
||||||
return {
|
|
||||||
walletId: walletId,
|
|
||||||
txid: txid,
|
|
||||||
outputIndex: outputIndex
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletUtxoValue = function(height, satoshis, scriptBuffer) {
|
|
||||||
var heightBuffer = new Buffer(4);
|
|
||||||
heightBuffer.writeUInt32BE(height);
|
|
||||||
var satoshisBuffer = new Buffer(8);
|
|
||||||
satoshisBuffer.writeDoubleBE(satoshis);
|
|
||||||
return Buffer.concat([heightBuffer, satoshisBuffer, scriptBuffer]);
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletUtxoValue = function(buffer) {
|
|
||||||
var reader = new BufferReader(buffer);
|
|
||||||
var height = reader.readUInt32BE();
|
|
||||||
var satoshis = buffer.readDoubleBE(4);
|
|
||||||
var scriptBuffer = buffer.slice(12);
|
|
||||||
return {
|
|
||||||
height: height,
|
|
||||||
satoshis: satoshis,
|
|
||||||
script: scriptBuffer
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletUtxoSatoshisKey = function(walletId, satoshis, txid, outputIndex) {
|
|
||||||
var buffers = [this.servicePrefix, this.subKeyMap.utxoSat.buffer];
|
|
||||||
|
|
||||||
var walletIdSizeBuffer = new Buffer(1);
|
|
||||||
walletIdSizeBuffer.writeUInt8(walletId.length);
|
|
||||||
var walletIdBuffer = new Buffer(walletId, 'utf8');
|
|
||||||
|
|
||||||
buffers.push(walletIdSizeBuffer);
|
|
||||||
buffers.push(walletIdBuffer);
|
|
||||||
|
|
||||||
var satoshisBuffer = new Buffer(8);
|
|
||||||
satoshisBuffer.writeDoubleBE(satoshis || 0);
|
|
||||||
buffers.push(satoshisBuffer);
|
|
||||||
|
|
||||||
var txidBuffer = new Buffer(txid || new Array(65).join('0'), 'hex');
|
|
||||||
buffers.push(txidBuffer);
|
|
||||||
|
|
||||||
var outputIndexBuffer = new Buffer(4);
|
|
||||||
outputIndexBuffer.writeUInt32BE(outputIndex || 0);
|
|
||||||
buffers.push(outputIndexBuffer);
|
|
||||||
|
|
||||||
return Buffer.concat(buffers);
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletUtxoSatoshisKey = function(buffer) {
|
|
||||||
var walletIdSize = buffer.readUInt8(3);
|
|
||||||
var walletId = buffer.slice(4, walletIdSize + 4).toString('utf8');
|
|
||||||
var satoshis = buffer.readDoubleBE(walletIdSize + 4);
|
|
||||||
|
|
||||||
var txid = buffer.slice(walletIdSize + 12, walletIdSize + 44).toString('hex');
|
|
||||||
var outputIndex = buffer.readUInt32BE(walletIdSize + 44);
|
|
||||||
return {
|
|
||||||
walletId: walletId,
|
|
||||||
satoshis: satoshis,
|
|
||||||
txid: txid,
|
|
||||||
outputIndex: outputIndex
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletUtxoSatoshisValue = function(height, scriptBuffer) {
|
|
||||||
var heightBuffer = new Buffer(4);
|
|
||||||
heightBuffer.writeUInt32BE(height);
|
|
||||||
return Buffer.concat([heightBuffer, scriptBuffer]);
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletUtxoSatoshisValue = function(buffer) {
|
|
||||||
var reader = new BufferReader(buffer);
|
|
||||||
var height = reader.readUInt32BE();
|
|
||||||
var scriptBuffer = reader.read(buffer.length - 4);
|
|
||||||
return {
|
|
||||||
height: height,
|
|
||||||
script: scriptBuffer
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletAddressesKey = function(walletId) {
|
|
||||||
var prefix = this.subKeyMap.addresses.buffer;
|
|
||||||
var walletIdSizeBuffer = new Buffer(1);
|
|
||||||
walletIdSizeBuffer.writeUInt8(walletId.length);
|
|
||||||
var walletIdBuffer = new Buffer(walletId, 'utf8');
|
|
||||||
return Buffer.concat([this.servicePrefix, prefix, walletIdSizeBuffer, walletIdBuffer]);
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletAddressesKey = function(buffer) {
|
|
||||||
var reader = new BufferReader(buffer);
|
|
||||||
reader.read(3);
|
|
||||||
var walletSize = reader.readUInt8();
|
|
||||||
return reader.read(walletSize).toString('utf8');
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletAddressesValue = function(addresses) {
|
|
||||||
var bufferList = [];
|
|
||||||
var addressesLengthBuffer = new Buffer(4);
|
|
||||||
addressesLengthBuffer.writeUInt32BE(addresses.length);
|
|
||||||
bufferList.push(addressesLengthBuffer);
|
|
||||||
for(var i = 0; i < addresses.length; i++) {
|
|
||||||
var addressSizeBuffer = new Buffer(1);
|
|
||||||
addressSizeBuffer.writeUInt8(addresses[i].length);
|
|
||||||
bufferList.push(addressSizeBuffer);
|
|
||||||
bufferList.push(new Buffer(addresses[i], 'utf8'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Buffer.concat(bufferList);
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletAddressesValue = function(buffer) {
|
|
||||||
var reader = new BufferReader(buffer);
|
|
||||||
var addressesLength = reader.readUInt32BE();
|
|
||||||
var addresses = [];
|
|
||||||
for(var i = 0; i < addressesLength; i++) {
|
|
||||||
var addressSize = reader.readUInt8();
|
|
||||||
addresses.push(reader.read(addressSize).toString('utf8'));
|
|
||||||
}
|
|
||||||
return addresses;
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletBalanceKey = function(walletId) {
|
|
||||||
var prefix = this.subKeyMap.balance.buffer;
|
|
||||||
var walletIdSizeBuffer = new Buffer(1);
|
|
||||||
walletIdSizeBuffer.writeUInt8(walletId.length);
|
|
||||||
var walletIdBuffer = new Buffer(walletId, 'utf8');
|
|
||||||
return Buffer.concat([this.servicePrefix, prefix, walletIdSizeBuffer, walletIdBuffer]);
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletBalanceKey = function(buffer) {
|
|
||||||
var reader = new BufferReader(buffer);
|
|
||||||
reader.read(3);
|
|
||||||
var walletSize = reader.readUInt8();
|
|
||||||
return reader.read(walletSize).toString('utf8');
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.encodeWalletBalanceValue = function(balance) {
|
|
||||||
var balanceBuffer = new Buffer(8);
|
|
||||||
balanceBuffer.writeDoubleBE(balance);
|
|
||||||
return balanceBuffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
Encoding.prototype.decodeWalletBalanceValue = function(buffer) {
|
|
||||||
return buffer.readDoubleBE();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Encoding;
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,765 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var Writable = require('stream').Writable;
|
|
||||||
var assert = require('assert');
|
|
||||||
var crypto = require('crypto');
|
|
||||||
var fs = require('fs');
|
|
||||||
var inherits = require('util').inherits;
|
|
||||||
var path = require('path');
|
|
||||||
var spawn = require('child_process').spawn;
|
|
||||||
|
|
||||||
var BitcoinRPC = require('bitcoind-rpc');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var async = require('async');
|
|
||||||
var bitcore = require('bitcore-lib');
|
|
||||||
var mkdirp = require('mkdirp');
|
|
||||||
var ttyread = require('ttyread');
|
|
||||||
|
|
||||||
var exports = {};
|
|
||||||
|
|
||||||
exports.isInteger = function(value) {
|
|
||||||
return typeof value === 'number' &&
|
|
||||||
isFinite(value) &&
|
|
||||||
Math.floor(value) === value;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.normalizeTimeStamp = function(value) {
|
|
||||||
if (value > 0xffffffff) {
|
|
||||||
value = Math.round(value/1000);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Will create a directory if it does not already exist.
|
|
||||||
*
|
|
||||||
* @param {String} directory - An absolute path to the directory
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
exports.setupDirectory = function(directory, callback) {
|
|
||||||
fs.access(directory, function(err) {
|
|
||||||
if (err && err.code === 'ENOENT') {
|
|
||||||
return mkdirp(directory, callback);
|
|
||||||
} else if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will split a range of numbers "a" to "b" by sections
|
|
||||||
* of the length "max".
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* > var range = utils.splitRange(1, 10, 3);
|
|
||||||
* > [[1, 3], [4, 6], [7, 9], [10, 10]]
|
|
||||||
*
|
|
||||||
* @param {Number} a - The start index (lesser)
|
|
||||||
* @param {Number} b - The end index (greater)
|
|
||||||
* @param {Number} max - The maximum section length
|
|
||||||
*/
|
|
||||||
exports.splitRange = function(a, b, max) {
|
|
||||||
assert(b > a, '"b" is expected to be greater than "a"');
|
|
||||||
var sections = [];
|
|
||||||
var delta = b - a;
|
|
||||||
var first = a;
|
|
||||||
var last = a;
|
|
||||||
|
|
||||||
var length = Math.floor(delta / max);
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
last = first + max - 1;
|
|
||||||
sections.push([first, last]);
|
|
||||||
first += max;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (last <= b) {
|
|
||||||
sections.push([first, b]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getFileStream: Checks for the file's existence and returns a readable stream or stdin
|
|
||||||
* @param {String} path - The path to the file
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
exports.getFileStream = function(filePath, callback) {
|
|
||||||
callback(null, fs.createReadStream(filePath));
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.readWalletDatFile = function(filePath, network, callback) {
|
|
||||||
assert(_.isString(network), 'Network expected to be a string.');
|
|
||||||
var datadir = path.dirname(filePath).replace(/(\/testnet3|\/regtest)$/, '');
|
|
||||||
var name = path.basename(filePath);
|
|
||||||
var options = ['-datadir=' + datadir, '-wallet=' + name];
|
|
||||||
if (network === 'testnet') {
|
|
||||||
options.push('-testnet');
|
|
||||||
} else if (network === 'regtest') {
|
|
||||||
options.push('-regtest');
|
|
||||||
}
|
|
||||||
// TODO use ../node_modules/.bin/wallet-utility
|
|
||||||
var exec = path.resolve(__dirname, '../node_modules/bitcore-node/bin/bitcoin-0.12.1/bin/wallet-utility');
|
|
||||||
var wallet = spawn(exec, options);
|
|
||||||
|
|
||||||
var result = '';
|
|
||||||
|
|
||||||
wallet.stdout.on('data', function(data) {
|
|
||||||
result += data.toString('utf8');
|
|
||||||
});
|
|
||||||
|
|
||||||
var error;
|
|
||||||
|
|
||||||
wallet.stderr.on('data', function(data) {
|
|
||||||
error = data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.on('close', function(code) {
|
|
||||||
if (code === 0) {
|
|
||||||
var addresses;
|
|
||||||
try {
|
|
||||||
addresses = JSON.parse(result);
|
|
||||||
addresses = addresses.map(function(entry) {
|
|
||||||
return entry.addr ? entry.addr : entry;
|
|
||||||
});
|
|
||||||
} catch(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
return callback(null, addresses);
|
|
||||||
} else if (error) {
|
|
||||||
return callback(new Error(error));
|
|
||||||
} else {
|
|
||||||
var message = 'wallet-utility exited (' + code + '): ' + result;
|
|
||||||
return callback(new Error(message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.readWalletFile = function(filePath, network, callback) {
|
|
||||||
if (/\.dat$/.test(filePath)) {
|
|
||||||
exports.readWalletDatFile(filePath, network, callback);
|
|
||||||
} else {
|
|
||||||
exports.getFileStream(filePath, callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will split an array into smaller arrays by size
|
|
||||||
*
|
|
||||||
* @param {Array} array
|
|
||||||
* @param {Number} size - The length of resulting smaller arrays
|
|
||||||
*/
|
|
||||||
exports.splitArray = function(array, size) {
|
|
||||||
var results = [];
|
|
||||||
while (array.length) {
|
|
||||||
results.push(array.splice(0, size));
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to get the remote ip address from cloudflare headers.
|
|
||||||
*
|
|
||||||
* @param {Object} req - An express request object
|
|
||||||
*/
|
|
||||||
exports.getRemoteAddress = function(req) {
|
|
||||||
if (req.headers['cf-connecting-ip']) {
|
|
||||||
return req.headers['cf-connecting-ip'];
|
|
||||||
}
|
|
||||||
return req.socket.remoteAddress;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A middleware to enable CORS
|
|
||||||
*
|
|
||||||
* @param {Object} req - An express request object
|
|
||||||
* @param {Object} res - An express response object
|
|
||||||
* @param {Function} next
|
|
||||||
*/
|
|
||||||
exports.enableCORS = function(req, res, next) {
|
|
||||||
res.header('access-control-allow-origin', '*');
|
|
||||||
res.header('access-control-allow-methods', 'GET, HEAD, PUT, POST, OPTIONS');
|
|
||||||
var allowed = [
|
|
||||||
'origin',
|
|
||||||
'x-requested-with',
|
|
||||||
'content-type',
|
|
||||||
'accept',
|
|
||||||
'content-length',
|
|
||||||
'cache-control',
|
|
||||||
'cf-connecting-ip'
|
|
||||||
];
|
|
||||||
res.header('access-control-allow-headers', allowed.join(', '));
|
|
||||||
|
|
||||||
var method = req.method && req.method.toUpperCase && req.method.toUpperCase();
|
|
||||||
|
|
||||||
if (method === 'OPTIONS') {
|
|
||||||
res.statusCode = 204;
|
|
||||||
res.end();
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will send error to express response
|
|
||||||
*
|
|
||||||
* @param {Error} err - error object
|
|
||||||
* @param {Object} res - express response object
|
|
||||||
*/
|
|
||||||
exports.sendError = function(err, res) {
|
|
||||||
if (err.statusCode) {
|
|
||||||
res.status(err.statusCode).send(err.message);
|
|
||||||
} else {
|
|
||||||
console.error(err.stack);
|
|
||||||
res.status(503).send(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will create a writeable logger stream
|
|
||||||
*
|
|
||||||
* @param {Function} logger - Function to log information
|
|
||||||
* @returns {Stream}
|
|
||||||
*/
|
|
||||||
exports.createLogStream = function(logger) {
|
|
||||||
function Log(options) {
|
|
||||||
Writable.call(this, options);
|
|
||||||
}
|
|
||||||
inherits(Log, Writable);
|
|
||||||
|
|
||||||
Log.prototype._write = function (chunk, enc, callback) {
|
|
||||||
logger(chunk.slice(0, chunk.length - 1)); // remove new line and pass to logger
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
var stream = new Log();
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getWalletId = exports.generateJobId = function() {
|
|
||||||
return crypto.randomBytes(16).toString('hex');
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getClients = function(clientsConfig) {
|
|
||||||
var clients = [];
|
|
||||||
for (var i = 0; i < clientsConfig.length; i++) {
|
|
||||||
var config = clientsConfig[i];
|
|
||||||
var remoteClient = new BitcoinRPC({
|
|
||||||
protocol: config.rpcprotocol || 'http',
|
|
||||||
host: config.rpchost || '127.0.0.1',
|
|
||||||
port: config.rpcport,
|
|
||||||
user: config.rpcuser,
|
|
||||||
pass: config.rpcpassword,
|
|
||||||
rejectUnauthorized: _.isUndefined(config.rpcstrict) ? true : config.rpcstrict
|
|
||||||
});
|
|
||||||
clients.push(remoteClient);
|
|
||||||
}
|
|
||||||
return clients;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.setClients = function(obj, clients) {
|
|
||||||
obj._clients = clients;
|
|
||||||
obj._clientsIndex = 0;
|
|
||||||
Object.defineProperty(obj, 'clients', {
|
|
||||||
get: function() {
|
|
||||||
var client = obj._clients[obj._clientsIndex];
|
|
||||||
obj._clientsIndex = (obj._clientsIndex + 1) % obj._clients.length;
|
|
||||||
return client;
|
|
||||||
},
|
|
||||||
enumerable: true,
|
|
||||||
configurable: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.tryAllClients = function(obj, func, options, callback) {
|
|
||||||
if (_.isFunction(options)) {
|
|
||||||
callback = options;
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
var clientIndex = obj._clientsIndex;
|
|
||||||
var retry = function(done) {
|
|
||||||
var client = obj._clients[clientIndex];
|
|
||||||
clientIndex = (clientIndex + 1) % obj._clients.length;
|
|
||||||
func(client, done);
|
|
||||||
};
|
|
||||||
async.retry({times: obj._clients.length, interval: options.interval || 1000}, retry, callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.wrapRPCError = function(errObj) {
|
|
||||||
var err = new Error(errObj.message);
|
|
||||||
err.code = errObj.code;
|
|
||||||
return err;
|
|
||||||
};
|
|
||||||
|
|
||||||
var PUBKEYHASH = new Buffer('01', 'hex');
|
|
||||||
var SCRIPTHASH = new Buffer('02', 'hex');
|
|
||||||
|
|
||||||
exports.getAddressTypeString = function(bufferArg) {
|
|
||||||
var buffer = bufferArg;
|
|
||||||
if (!Buffer.isBuffer(bufferArg)) {
|
|
||||||
buffer = new Buffer(bufferArg, 'hex');
|
|
||||||
}
|
|
||||||
var type = buffer.slice(0, 1);
|
|
||||||
if (type.compare(PUBKEYHASH) === 0) {
|
|
||||||
return 'pubkeyhash';
|
|
||||||
} else if (type.compare(SCRIPTHASH) === 0) {
|
|
||||||
return 'scripthash';
|
|
||||||
} else {
|
|
||||||
throw new TypeError('Unknown address type');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getAddressTypeBuffer = function(address) {
|
|
||||||
var type;
|
|
||||||
if (address.type === 'pubkeyhash') {
|
|
||||||
type = PUBKEYHASH;
|
|
||||||
} else if (address.type === 'scripthash') {
|
|
||||||
type = SCRIPTHASH;
|
|
||||||
} else {
|
|
||||||
throw new TypeError('Unknown address type');
|
|
||||||
}
|
|
||||||
return type;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.splitBuffer = function(buffer, size) {
|
|
||||||
var pos = 0;
|
|
||||||
var buffers = [];
|
|
||||||
while (pos < buffer.length) {
|
|
||||||
buffers.push(buffer.slice(pos, pos + size));
|
|
||||||
pos += size;
|
|
||||||
}
|
|
||||||
return buffers;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.exitWorker = function(worker, timeout, callback) {
|
|
||||||
assert(worker, '"worker" is expected to be defined');
|
|
||||||
var exited = false;
|
|
||||||
worker.once('exit', function(code) {
|
|
||||||
if (!exited) {
|
|
||||||
exited = true;
|
|
||||||
if (code !== 0) {
|
|
||||||
var error = new Error('Worker did not exit cleanly: ' + code);
|
|
||||||
error.code = code;
|
|
||||||
return callback(error);
|
|
||||||
} else {
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
worker.kill('SIGINT');
|
|
||||||
setTimeout(function() {
|
|
||||||
if (!exited) {
|
|
||||||
exited = true;
|
|
||||||
worker.kill('SIGKILL');
|
|
||||||
return callback(new Error('Worker exit timeout, force shutdown'));
|
|
||||||
}
|
|
||||||
}, timeout).unref();
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.timestampToISOString = function(timestamp) {
|
|
||||||
return new Date(this.toIntIfNumberLike(timestamp) * 1000).toISOString();
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.satoshisToBitcoin = function(satoshis) {
|
|
||||||
return satoshis / 100000000;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getPassphrase = function(callback) {
|
|
||||||
ttyread('Enter passphrase: ', {silent: true}, callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.acquirePassphrase = function(callback) {
|
|
||||||
var first;
|
|
||||||
var second;
|
|
||||||
async.doWhilst(function(next) {
|
|
||||||
ttyread('Enter passphrase: ', {silent: true}, function(err, result) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
first = result;
|
|
||||||
ttyread('Re-enter passphrase: ', {silent: true}, function(err, result) {
|
|
||||||
second = result;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, function() {
|
|
||||||
if (first !== second) {
|
|
||||||
console.log('Passphrases do not match, please re-enter.');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}, function(err) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
callback(null, first);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Important notes:
|
|
||||||
|
|
||||||
How the encryption/decryption schemes work.
|
|
||||||
1. The user's passphrase and salt are hashed using scrypt algorithm. You must store the salt.
|
|
||||||
On modern hardware this hashing function should take 1-2 seconds.
|
|
||||||
2. The resulting hash is 48 bytes. The first 32 bytes of this hash is the "key" and the last
|
|
||||||
16 bytes is the "iv" to decrypt the master key using AES256-cbc.
|
|
||||||
3. The plaintext "master key" is always 32 bytes and should be as random as possible.
|
|
||||||
You may pass in the plaintext master key to encryptSecret -or- /dev/random will be consulted.
|
|
||||||
4. The cipherText of the master key must be stored just like the salt. For added security, you
|
|
||||||
might store the cipherText of the master key separate from the cipherText.
|
|
||||||
For example, if an attacker discovers your passphrase and salt (the most likely scenario), they would
|
|
||||||
still require the cipherText of the master key in order to decrypt the cipherText of your private keys.
|
|
||||||
Storing your encrypted master key on another device would be a better choice than keeping your salt,
|
|
||||||
the cipherText of your master key and the cipherText of your private keys on the same computer system.
|
|
||||||
5. The plaintext master key is then used to encrypt/decrypt the bitcoin private keys. The private keys'
|
|
||||||
corresponding public key is used as the IV for the procedure.
|
|
||||||
|
|
||||||
|
|
||||||
Specific notes regarding how private keys are transferred from a traditional "wallet.dat" file used with
|
|
||||||
Bitcoin Core's Wallet:
|
|
||||||
|
|
||||||
1. Bitcoin Core's Wallet uses Berkeley DB version 4.8 to store secp256k1 elliptic curve private keys in WIF format.
|
|
||||||
2. The same Berkeley DB, internally called "main", also stores compressed public keys for the above private keys,
|
|
||||||
the master keys used to encrypt the above private keys and bitcoin transaction details relevant to those private keys
|
|
||||||
3. The underlying data structure for the Berkeley database is the B-Tree (balanced tree). This is a key-value data
|
|
||||||
structure, therefore the database is a key-value database.
|
|
||||||
Berkeley DB documentation also refers to this as "key-record"
|
|
||||||
This means that the data contained in this B-Tree is organized for high speed retrieval based on a key.
|
|
||||||
In other words the database is optimized for lookups.
|
|
||||||
4. The filename for this database file is called "wallet.dat" historically,
|
|
||||||
but you can rename it to whatever suits you
|
|
||||||
|
|
||||||
*/
|
|
||||||
//this function depends on the derivation method and its params that were originally used to hash the passphrase
|
|
||||||
//this could be SHA512, scrypt, etc.
|
|
||||||
exports.sha512KDF = function(passphrase, salt, derivationOptions, callback) {
|
|
||||||
if (!derivationOptions || derivationOptions.method !== 0 || !derivationOptions.rounds) {
|
|
||||||
return callback(new Error('SHA512 KDF method was called for, ' +
|
|
||||||
'yet the derivations options for it were not supplied.'));
|
|
||||||
}
|
|
||||||
var rounds = derivationOptions.rounds || 1;
|
|
||||||
//if salt was sent in as a string, we will have to assume the default encoding type
|
|
||||||
if (!Buffer.isBuffer(salt)) {
|
|
||||||
salt = new Buffer(salt, 'utf-8');
|
|
||||||
}
|
|
||||||
var derivation = Buffer.concat([new Buffer(''), new Buffer(passphrase), salt]);
|
|
||||||
for(var i = 0; i < rounds; i++) {
|
|
||||||
derivation = crypto.createHash('sha512').update(derivation).digest();
|
|
||||||
}
|
|
||||||
callback(null, derivation);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.hashPassphrase = function() {
|
|
||||||
return exports.sha512KDF;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.decryptPrivateKey = function(opts, callback) {
|
|
||||||
exports.decryptSecret(opts, function(err, masterKey) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
opts.cipherText = opts.pkCipherText;
|
|
||||||
//decrypt the private here using the plainText master key as the "key"
|
|
||||||
//and the double sha256 compressed pub key as the "IV"
|
|
||||||
opts.key = masterKey;
|
|
||||||
opts.iv = bitcore.crypto.Hash.sha256sha256(new Buffer(opts.pubkey, 'hex'));
|
|
||||||
exports.decrypt(opts, function(err, privateKey) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
callback(null, privateKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//call decryptSecret first
|
|
||||||
exports.encryptPrivateKeys = function(opts, callback) {
|
|
||||||
if (!opts.masterKey || !opts.keys) {
|
|
||||||
return callback(new Error('A decrypted master key, ' +
|
|
||||||
'compressed public keys and private keys are required for encryption.'));
|
|
||||||
}
|
|
||||||
if (!Buffer.isBuffer(opts.masterKey)) { //we'll have to assume the master key is utf-8 encoded
|
|
||||||
opts.masterKey = new Buffer(opts.masterKey);
|
|
||||||
}
|
|
||||||
assert(opts.masterKey.length === 32, 'Master Key must be 32 bytes in length, ' +
|
|
||||||
'if you have a hex string, please pass master key in as a buffer');
|
|
||||||
//if the master key is not 32 bytes, then take the sha256 hash
|
|
||||||
var ret = [];
|
|
||||||
async.mapLimit(opts.keys, 5, function(key, next) {
|
|
||||||
var iv = bitcore.crypto.Hash.sha256sha256(new Buffer(key.pubKey, 'hex')).slice(0, 16);
|
|
||||||
//do we want to encrypt WIF's or RAW private keys or does it matter?
|
|
||||||
exports.encrypt({
|
|
||||||
secret: key.privKey,
|
|
||||||
iv: iv,
|
|
||||||
key: opts.masterKey
|
|
||||||
}, next);
|
|
||||||
}, function(err, results) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
for(var i = 0; i < results.length; i++) {
|
|
||||||
ret.push({
|
|
||||||
cipherText: results[i],
|
|
||||||
checkHash: bitcore.crypto.Hash.sha256(new Buffer(opts.keys[i].pubKey + results[i])).toString('hex'),
|
|
||||||
type: 'encrypted private key',
|
|
||||||
pubKey: opts.keys[i].pubKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
callback(null, ret);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.encrypt = function(opts, callback) {
|
|
||||||
if (!opts.key ||
|
|
||||||
!opts.iv ||
|
|
||||||
!opts.secret ||
|
|
||||||
opts.key.length !== 32 ||
|
|
||||||
opts.iv.length !== 16 ||
|
|
||||||
opts.secret.length < 1) {
|
|
||||||
return callback(new Error('Key, IV, and something to encrypt is required.'));
|
|
||||||
}
|
|
||||||
var cipher = crypto.createCipheriv('aes-256-cbc', opts.key, opts.iv);
|
|
||||||
var cipherText;
|
|
||||||
try {
|
|
||||||
cipherText = Buffer.concat([cipher.update(opts.secret), cipher.final()]).toString('hex');
|
|
||||||
} catch(e) {
|
|
||||||
return callback(e);
|
|
||||||
}
|
|
||||||
return callback(null, cipherText);
|
|
||||||
|
|
||||||
};
|
|
||||||
exports.encryptSecret = function(opts, callback) {
|
|
||||||
var hashFunc = exports.hashPassphrase(opts.derivationOptions);
|
|
||||||
hashFunc(opts.passphrase, opts.salt, opts.derivationOptions, function(err, hashedPassphrase) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
var secret = opts.secret || crypto.randomBytes(32);
|
|
||||||
assert(Buffer.isBuffer(secret), 'secret is expected to be a buffer');
|
|
||||||
secret = bitcore.crypto.Hash.sha256sha256(secret);
|
|
||||||
var firstHalf = hashedPassphrase.slice(0, 32); //AES256-cbc shared key
|
|
||||||
var secondHalf = hashedPassphrase.slice(32, 48); //AES256-cbc IV, for cbc mode, the IV will be 16 bytes
|
|
||||||
exports.encrypt({
|
|
||||||
secret: secret,
|
|
||||||
key: firstHalf,
|
|
||||||
iv: secondHalf
|
|
||||||
}, callback);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.decryptSecret = function(opts, callback) {
|
|
||||||
var hashFunc = exports.hashPassphrase(opts.derivationOptions);
|
|
||||||
hashFunc(opts.passphrase, opts.salt, opts.derivationOptions, function(err, hashedPassphrase) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
opts.key = hashedPassphrase;
|
|
||||||
exports.decrypt(opts, callback);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.decrypt = function(opts, callback) {
|
|
||||||
if (!Buffer.isBuffer(opts.key)) {
|
|
||||||
opts.key = new Buffer(opts.key, 'hex');
|
|
||||||
}
|
|
||||||
var secondHalf;
|
|
||||||
if (opts.iv) {
|
|
||||||
secondHalf = opts.iv.slice(0, 16);
|
|
||||||
} else {
|
|
||||||
secondHalf = opts.key.slice(32, 48); //AES256-cbc IV
|
|
||||||
}
|
|
||||||
var cipherText = new Buffer(opts.cipherText, 'hex');
|
|
||||||
var firstHalf = opts.key.slice(0, 32); //AES256-cbc shared key
|
|
||||||
var AESDecipher = crypto.createDecipheriv('aes-256-cbc', firstHalf, secondHalf);
|
|
||||||
var plainText;
|
|
||||||
try {
|
|
||||||
plainText = Buffer.concat([AESDecipher.update(cipherText), AESDecipher.final()]).toString('hex');
|
|
||||||
} catch(e) {
|
|
||||||
return callback(e);
|
|
||||||
}
|
|
||||||
callback(null, plainText);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.confirm = function(question, callback) {
|
|
||||||
ttyread(question + ' (y/N): ', function(err, answer) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err, false);
|
|
||||||
}
|
|
||||||
if (answer === 'y') {
|
|
||||||
return callback(null, true);
|
|
||||||
}
|
|
||||||
callback(null, false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.encryptSecretWithPassphrase = function(secret, callback) {
|
|
||||||
exports.acquirePassphrase(function(err, passphrase) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
var salt = crypto.randomBytes(32).toString('hex');
|
|
||||||
exports.encryptSecret({
|
|
||||||
secret: secret,
|
|
||||||
passphrase: passphrase,
|
|
||||||
salt: salt
|
|
||||||
}, function(err, cipherText) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
callback(null, cipherText, salt);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.generateNonce = function() {
|
|
||||||
var nonce = new Buffer(new Array(12));
|
|
||||||
nonce.writeDoubleBE(Date.now());
|
|
||||||
nonce.writeUInt32BE(process.hrtime()[1], 8);
|
|
||||||
return nonce;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.generateHashForRequest = function(method, url, nonce) {
|
|
||||||
nonce = nonce || new Buffer(0);
|
|
||||||
assert(Buffer.isBuffer(nonce), 'nonce must a buffer');
|
|
||||||
var dataToSign = Buffer.concat([nonce, new Buffer(method), new Buffer(url)]);
|
|
||||||
return bitcore.crypto.Hash.sha256sha256(dataToSign);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getWalletIdFromName = function(walletName) {
|
|
||||||
if (!Buffer.isBuffer(walletName)) {
|
|
||||||
walletName = new Buffer(walletName, 'utf8');
|
|
||||||
}
|
|
||||||
return bitcore.crypto.Hash.sha256sha256(walletName).toString('hex');
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.isRangeMoreThan = function(a, b) {
|
|
||||||
if (a && !b) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!a && !b) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!a && b) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (a.height > b.height) {
|
|
||||||
return true;
|
|
||||||
} else if (a.height < b.height) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return a.index > b.index;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.toHexBuffer = function(a) {
|
|
||||||
if (!Buffer.isBuffer(a)) {
|
|
||||||
a = new Buffer(a, 'hex');
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.toIntIfNumberLike = function(a) {
|
|
||||||
if (!/[^\d]+/.test(a)) {
|
|
||||||
return parseInt(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.delimitedStringParse = function(delim, str) {
|
|
||||||
function tryJSONparse(str) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(str);
|
|
||||||
} catch(e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var ret = [];
|
|
||||||
|
|
||||||
if (delim === null) {
|
|
||||||
return tryJSONparse(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
var list = str.split(delim);
|
|
||||||
for(var i = 0; i < list.length; i++) {
|
|
||||||
ret.push(tryJSONparse(list[i]));
|
|
||||||
}
|
|
||||||
ret = _.compact(ret);
|
|
||||||
return ret.length === 0 ? false : ret;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.diffTime = function(time) {
|
|
||||||
var diff = process.hrtime(time);
|
|
||||||
return (diff[0] * 1E9 + diff[1])/(1E9 * 1.0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* input: string representing a number + multiple of bytes, e.g. 500MB, 200KB, 100B
|
|
||||||
* output: integer representing the byte count
|
|
||||||
*/
|
|
||||||
exports.parseByteCount = function(byteCountString) {
|
|
||||||
|
|
||||||
function finish(n, m) {
|
|
||||||
var num = parseInt(n);
|
|
||||||
if (num > 0) {
|
|
||||||
return num * m;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isString(byteCountString)) {
|
|
||||||
return byteCountString;
|
|
||||||
}
|
|
||||||
var str = byteCountString.replace(/\s+/g, '');
|
|
||||||
var map = { 'MB': 1E6, 'kB': 1000, 'KB': 1000, 'MiB': (1024 * 1024),
|
|
||||||
'KiB': 1024, 'GiB': Math.pow(1024, 3), 'GB': 1E9 };
|
|
||||||
var keys = Object.keys(map);
|
|
||||||
for(var i = 0; i < keys.length; i++) {
|
|
||||||
var re = new RegExp(keys[i] + '$');
|
|
||||||
var match = str.match(re);
|
|
||||||
if (match) {
|
|
||||||
var num = str.slice(0, match.index);
|
|
||||||
return finish(num, map[keys[i]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return finish(byteCountString, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* input: arguments passed into originating function (whoever called us)
|
|
||||||
* output: bool args are valid for encoding a key to the database
|
|
||||||
*/
|
|
||||||
exports.hasRequiredArgsForEncoding = function(args) {
|
|
||||||
function exists(arg) {
|
|
||||||
return !(arg === null || arg === undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exists(args[0])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pastArgMissing;
|
|
||||||
|
|
||||||
for(var i = 1; i < args.length; i++) {
|
|
||||||
var argMissing = exists(args[i]);
|
|
||||||
if (argMissing && pastArgMissing) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
pastArgMissing = argMissing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.toJSONL = function(obj) {
|
|
||||||
//this should be a standard obj that JSON.stringify will handle
|
|
||||||
//general newlines within key values or data values are not permitted
|
|
||||||
//this is intended to be used for bitcoin tx's that don't have newlines
|
|
||||||
//within keys or values themselves
|
|
||||||
var str = JSON.stringify(obj);
|
|
||||||
str = str.replace(/\n/g, '');
|
|
||||||
return str + '\n';
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = exports;
|
|
||||||
@ -1,225 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var assert = require('assert');
|
|
||||||
|
|
||||||
var bitcore = require('bitcore-lib');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var utils = require('./utils');
|
|
||||||
|
|
||||||
var MAX_INT = 0xffffffff; // Math.pow(2, 32) - 1
|
|
||||||
|
|
||||||
exports.sanitizeRangeOptions = function(options) {
|
|
||||||
if (!options) {
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
options.height = options.height || 0;
|
|
||||||
options.index = options.index || 0;
|
|
||||||
|
|
||||||
if (!options.limit) {
|
|
||||||
options.limit = 10;
|
|
||||||
} else if (options.limit > 500) {
|
|
||||||
throw new Error('Limit exceeds maximum');
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(bitcore.util.js.isNaturalNumber(options.height), '"height" is expected to be a natural number');
|
|
||||||
assert(bitcore.util.js.isNaturalNumber(options.index), '"index" is expected to be a natural number');
|
|
||||||
assert(bitcore.util.js.isNaturalNumber(options.limit), '"limit" is expected to be a natural number');
|
|
||||||
|
|
||||||
assert(options.limit <= 500, '"limit" exceeds maximum');
|
|
||||||
|
|
||||||
if (options.end) {
|
|
||||||
assert(bitcore.util.js.isNaturalNumber(options.end.height), '"end height" is expected to be a natural number');
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.checkRangeParams = function(req, res, next) {
|
|
||||||
assert(req.bitcoinHeight, '"bitcoinHeight" is expected to be set on the request');
|
|
||||||
|
|
||||||
var range = {
|
|
||||||
height: parseInt(req.query.height),
|
|
||||||
index: parseInt(req.query.index),
|
|
||||||
limit: parseInt(req.query.limit),
|
|
||||||
end: {
|
|
||||||
height: req.bitcoinHeight,
|
|
||||||
index: MAX_INT
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (req.query.end) {
|
|
||||||
range.end.height = parseInt(req.query.end) || req.bitcoinHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
range = exports.sanitizeRangeOptions(range);
|
|
||||||
} catch(e) {
|
|
||||||
return utils.sendError({
|
|
||||||
message: 'Invalid params: ' + e.message,
|
|
||||||
statusCode: 400
|
|
||||||
}, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(range.height <= range.end.height, '\'Height\' param required to be less than \'End\' param.');
|
|
||||||
req.range = range;
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.checkAddress = function(req, res, next) {
|
|
||||||
var address;
|
|
||||||
var addressStr;
|
|
||||||
|
|
||||||
if (req.body.address) {
|
|
||||||
addressStr = req.body.address;
|
|
||||||
} else {
|
|
||||||
addressStr = req.params.address;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!addressStr) {
|
|
||||||
return utils.sendError({
|
|
||||||
message: 'Address param is expected',
|
|
||||||
statusCode: 400
|
|
||||||
}, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(req.network, '"network" is expected to be set on the request');
|
|
||||||
|
|
||||||
try {
|
|
||||||
address = new bitcore.Address(addressStr, req.network);
|
|
||||||
} catch(e) {
|
|
||||||
return utils.sendError({
|
|
||||||
message: 'Invalid address: ' + e.message,
|
|
||||||
statusCode: 400
|
|
||||||
}, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.address = address;
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.checkWalletId = function(req, res, next) {
|
|
||||||
|
|
||||||
if (!req.params.walletId) {
|
|
||||||
return utils.sendError({
|
|
||||||
message: 'Wallet id is expected',
|
|
||||||
statusCode: 400
|
|
||||||
}, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.params.walletId.length !== 64 || !bitcore.util.js.isHexa(req.params.walletId)) {
|
|
||||||
return utils.sendError({
|
|
||||||
message: 'Wallet id is expected to be a hexadecimal string with length of 64',
|
|
||||||
statusCode: 400
|
|
||||||
}, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.walletId = new Buffer(req.params.walletId, 'hex');
|
|
||||||
next();
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.checkAddresses = function(req, res, next) {
|
|
||||||
|
|
||||||
if (!req.file && req.body) {
|
|
||||||
req.addresses = req.body;
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.file || !req.file.buffer) {
|
|
||||||
generateError(406, 'Content-Type must be set to multipart/form' +
|
|
||||||
' and addresses key and value must be given.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var buf = req.file.buffer;
|
|
||||||
var bufString = buf.toString();
|
|
||||||
if (bufString.slice(-1) === ',') {
|
|
||||||
var bufString = '[' + bufString.slice(0,-1) + ']';
|
|
||||||
}
|
|
||||||
req.addresses = parse(bufString);
|
|
||||||
if (!req.addresses) {
|
|
||||||
generateError(415, 'Could not parse addresses buffer into something meaningful.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
|
|
||||||
function generateError(status, msg) {
|
|
||||||
res.status(status).jsonp({
|
|
||||||
error: msg
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//we are able to deal with json/jsonl, possibly others
|
|
||||||
function parse(string) {
|
|
||||||
var ret = false;
|
|
||||||
var delims = [null, '\n', ' '];
|
|
||||||
for(var i = 0; i < delims.length; i++) {
|
|
||||||
ret = utils.delimitedStringParse(delims[i], string);
|
|
||||||
if (_.isArray(ret)) {
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.checkAuthHeaders = function(req, res) {
|
|
||||||
var identity = req.header('x-identity');
|
|
||||||
var signature = req.header('x-signature');
|
|
||||||
var nonce = req.header('x-nonce');
|
|
||||||
if (identity && (identity.length > 130 || !bitcore.util.js.isHexa(identity))) {
|
|
||||||
utils.sendError({
|
|
||||||
message: 'x-identity is expected to be a hexadecimal string with length of less than 131',
|
|
||||||
statusCode: 400
|
|
||||||
}, res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (signature && (signature.length > 142 || !bitcore.util.js.isHexa(signature))) {
|
|
||||||
utils.sendError({
|
|
||||||
message: 'x-signature is expected to be a hexadecimal string with length of less than 143',
|
|
||||||
statusCode: 400
|
|
||||||
}, res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (nonce && (nonce.length > 128 || nonce.length % 2 !== 0 || !bitcore.util.js.isHexa(nonce))) {
|
|
||||||
utils.sendError({
|
|
||||||
message: 'x-nonce is expected to be a hexadecimal string with length of less than 129',
|
|
||||||
statusCode: 400
|
|
||||||
}, res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.checkDate = function(dateStrings) {
|
|
||||||
var errors = [];
|
|
||||||
if (!Array.isArray(dateStrings)) {
|
|
||||||
dateStrings = [dateStrings];
|
|
||||||
}
|
|
||||||
for(var i = 0; i < dateStrings.length; i++) {
|
|
||||||
internalDateCheck(dateStrings[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function internalDateCheck(dateString) {
|
|
||||||
var date = new Date(utils.toIntIfNumberLike(dateString));
|
|
||||||
if (date.toString() === 'Invalid Date') {
|
|
||||||
errors.push('The date supplied: \'' + dateString +
|
|
||||||
'\' is not a valid date string. A valid date could be: \'2016-09-01\'.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.checkDateFunction = function(callback) {
|
|
||||||
var self = this;
|
|
||||||
return function() {
|
|
||||||
var args = Array.prototype.slice.call(arguments);
|
|
||||||
var errors = self.checkDate([args[1], args[2]]);
|
|
||||||
if (errors.length > 0) {
|
|
||||||
args.unshift(errors);
|
|
||||||
} else {
|
|
||||||
args.unshift(null);
|
|
||||||
}
|
|
||||||
callback.apply(null, args);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = exports;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
var bcoin = require('bcoin').set('main');
|
|
||||||
|
|
||||||
var node = bcoin.fullnode({
|
|
||||||
checkpoints: true,
|
|
||||||
// Primary wallet passphrase
|
|
||||||
logLevel: 'info'
|
|
||||||
});
|
|
||||||
|
|
||||||
// We get a lot of errors sometimes,
|
|
||||||
// usually from peers hanging up on us.
|
|
||||||
// Just ignore them for now.
|
|
||||||
node.on('error', function(err) {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the node
|
|
||||||
node.open().then(function() {
|
|
||||||
node.connect().then(function() {
|
|
||||||
node.startSync();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user