Merge branch 'blocks'
This commit is contained in:
commit
b89864dc4b
4
.gitignore
vendored
4
.gitignore
vendored
@ -27,3 +27,7 @@ bin/SHA256SUMS.asc
|
||||
regtest/data/node1/regtest
|
||||
regtest/data/node2/regtest
|
||||
regtest/data/node3/regtest
|
||||
bitcore-node.json*
|
||||
*.bak
|
||||
*.orig
|
||||
lib/services/insight-api
|
||||
|
||||
10
.jshintrc
10
.jshintrc
@ -22,11 +22,11 @@
|
||||
"trailing": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"maxparams": 4,
|
||||
"maxstatements": 15,
|
||||
"maxparams": 6,
|
||||
"maxstatements": 25,
|
||||
"maxcomplexity": 10,
|
||||
"maxdepth": 3,
|
||||
"maxlen": 120,
|
||||
"maxdepth": 4,
|
||||
"maxlen": 140,
|
||||
"multistr": true,
|
||||
"predef": [
|
||||
"after",
|
||||
@ -39,4 +39,4 @@
|
||||
"module",
|
||||
"require"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
20
.travis.yml
20
.travis.yml
@ -1,22 +1,10 @@
|
||||
dist: trusty
|
||||
sudo: false
|
||||
language: node_js
|
||||
env:
|
||||
- CXX=g++-4.8 CC=gcc-4.8
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
- gcc-4.8
|
||||
- libzmq3-dev
|
||||
node_js:
|
||||
- "v0.10.25"
|
||||
- "v0.12.7"
|
||||
- "v4"
|
||||
- 8
|
||||
script:
|
||||
- npm run regtest
|
||||
- npm run test
|
||||
- npm run coverage
|
||||
- npm run jshint
|
||||
after_success:
|
||||
- npm run coveralls
|
||||
- npm run coveralls
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,169 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var benchmark = require('benchmark');
|
||||
var bitcoin = require('bitcoin');
|
||||
var async = require('async');
|
||||
var maxTime = 20;
|
||||
|
||||
console.log('Bitcoin Service native interface vs. Bitcoin JSON RPC interface');
|
||||
console.log('----------------------------------------------------------------------');
|
||||
|
||||
// To run the benchmarks a fully synced Bitcore Core directory is needed. The RPC comands
|
||||
// can be modified to match the settings in bitcoin.conf.
|
||||
|
||||
var fixtureData = {
|
||||
blockHashes: [
|
||||
'00000000fa7a4acea40e5d0591d64faf48fd862fa3561d111d967fc3a6a94177',
|
||||
'000000000017e9e0afc4bc55339f60ffffb9cbe883f7348a9fbc198a486d5488',
|
||||
'000000000019ddb889b534c5d85fca2c91a73feef6fd775cd228dea45353bae1',
|
||||
'0000000000977ac3d9f5261efc88a3c2d25af92a91350750d00ad67744fa8d03'
|
||||
],
|
||||
txHashes: [
|
||||
'5523b432c1bd6c101bee704ad6c560fd09aefc483f8a4998df6741feaa74e6eb',
|
||||
'ff48393e7731507c789cfa9cbfae045b10e023ce34ace699a63cdad88c8b43f8',
|
||||
'5d35c5eebf704877badd0a131b0a86588041997d40dbee8ccff21ca5b7e5e333',
|
||||
'88842f2cf9d8659c3434f6bc0c515e22d87f33e864e504d2d7117163a572a3aa',
|
||||
]
|
||||
};
|
||||
|
||||
var bitcoind = require('../').services.Bitcoin({
|
||||
node: {
|
||||
datadir: process.env.HOME + '/.bitcoin',
|
||||
network: {
|
||||
name: 'testnet'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bitcoind.on('error', function(err) {
|
||||
console.error(err.message);
|
||||
});
|
||||
|
||||
bitcoind.start(function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
console.log('Bitcoin Core started');
|
||||
});
|
||||
|
||||
bitcoind.on('ready', function() {
|
||||
|
||||
console.log('Bitcoin Core ready');
|
||||
|
||||
var client = new bitcoin.Client({
|
||||
host: 'localhost',
|
||||
port: 18332,
|
||||
user: 'bitcoin',
|
||||
pass: 'local321'
|
||||
});
|
||||
|
||||
async.series([
|
||||
function(next) {
|
||||
|
||||
var c = 0;
|
||||
var hashesLength = fixtureData.blockHashes.length;
|
||||
var txLength = fixtureData.txHashes.length;
|
||||
|
||||
function bitcoindGetBlockNative(deffered) {
|
||||
if (c >= hashesLength) {
|
||||
c = 0;
|
||||
}
|
||||
var hash = fixtureData.blockHashes[c];
|
||||
bitcoind.getBlock(hash, function(err, block) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
deffered.resolve();
|
||||
});
|
||||
c++;
|
||||
}
|
||||
|
||||
function bitcoindGetBlockJsonRpc(deffered) {
|
||||
if (c >= hashesLength) {
|
||||
c = 0;
|
||||
}
|
||||
var hash = fixtureData.blockHashes[c];
|
||||
client.getBlock(hash, false, function(err, block) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
deffered.resolve();
|
||||
});
|
||||
c++;
|
||||
}
|
||||
|
||||
function bitcoinGetTransactionNative(deffered) {
|
||||
if (c >= txLength) {
|
||||
c = 0;
|
||||
}
|
||||
var hash = fixtureData.txHashes[c];
|
||||
bitcoind.getTransaction(hash, true, function(err, tx) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
deffered.resolve();
|
||||
});
|
||||
c++;
|
||||
}
|
||||
|
||||
function bitcoinGetTransactionJsonRpc(deffered) {
|
||||
if (c >= txLength) {
|
||||
c = 0;
|
||||
}
|
||||
var hash = fixtureData.txHashes[c];
|
||||
client.getRawTransaction(hash, function(err, tx) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
deffered.resolve();
|
||||
});
|
||||
c++;
|
||||
}
|
||||
|
||||
var suite = new benchmark.Suite();
|
||||
|
||||
suite.add('bitcoind getblock (native)', bitcoindGetBlockNative, {
|
||||
defer: true,
|
||||
maxTime: maxTime
|
||||
});
|
||||
|
||||
suite.add('bitcoind getblock (json rpc)', bitcoindGetBlockJsonRpc, {
|
||||
defer: true,
|
||||
maxTime: maxTime
|
||||
});
|
||||
|
||||
suite.add('bitcoind gettransaction (native)', bitcoinGetTransactionNative, {
|
||||
defer: true,
|
||||
maxTime: maxTime
|
||||
});
|
||||
|
||||
suite.add('bitcoind gettransaction (json rpc)', bitcoinGetTransactionJsonRpc, {
|
||||
defer: true,
|
||||
maxTime: maxTime
|
||||
});
|
||||
|
||||
suite
|
||||
.on('cycle', function(event) {
|
||||
console.log(String(event.target));
|
||||
})
|
||||
.on('complete', function() {
|
||||
console.log('Fastest is ' + this.filter('fastest').pluck('name'));
|
||||
console.log('----------------------------------------------------------------------');
|
||||
next();
|
||||
})
|
||||
.run();
|
||||
}
|
||||
], function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
console.log('Finished');
|
||||
bitcoind.stop(function(err) {
|
||||
if (err) {
|
||||
console.error('Fail to stop services: ' + err);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
bitcore-node.json.sample
Normal file
22
bitcore-node.json.sample
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"network": "livenet",
|
||||
"port": 3001,
|
||||
"datadir": "/tmp",
|
||||
"services": [
|
||||
"p2p",
|
||||
"db",
|
||||
"header",
|
||||
"block",
|
||||
"transaction",
|
||||
"timestamp",
|
||||
"mempool",
|
||||
"address"
|
||||
],
|
||||
"servicesConfig": {
|
||||
"p2p": {
|
||||
"peers": [
|
||||
{ "ip": { "v4": "<some trusted full node>" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
48
contrib/printKeys.js
Normal file
48
contrib/printKeys.js
Normal file
@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
var levelup = require('levelup');
|
||||
var leveldown = require('leveldown');
|
||||
var Encoding = require('../lib/services/address/encoding');
|
||||
var dbPath = '/Users/chrisk/.bwdb/bitcore-node.db';
|
||||
var bitcore = require('bitcore-lib');
|
||||
var db = levelup(dbPath, {keyEncoding: 'binary', valueEncoding: 'binary'});
|
||||
|
||||
var prefix = new Buffer('0002', 'hex');
|
||||
var encoding = new Encoding(prefix);
|
||||
var address = '1MfDRRVVKXUe5KNVZzu8CBzUZDHTTYZM94';
|
||||
var addressLength = new Buffer(1);
|
||||
addressLength.writeUInt8(address.length);
|
||||
|
||||
//var startBuffer = prefix;
|
||||
//var endBuffer = Buffer.concat([prefix, new Buffer('ff', 'hex')]);
|
||||
|
||||
//var startBuffer = Buffer.concat([prefix, addressLength, new Buffer(address, 'utf8'), new Buffer('00', 'hex')]);
|
||||
//var endBuffer = Buffer.concat([prefix, addressLength, new Buffer(address, 'utf8'), new Buffer('01', 'hex')]);
|
||||
var start = Buffer.concat([prefix, new Buffer('0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9', 'hex')]);
|
||||
var end = Buffer.concat([prefix, new Buffer('0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9', 'hex'), new Buffer('01', 'hex')]);
|
||||
var stream = db.createReadStream({
|
||||
gte: start,
|
||||
lt: end
|
||||
});
|
||||
stream.on('data', function(data) {
|
||||
var txkey = data.key.slice(2).toString('hex');
|
||||
var height = data.value.readUInt32BE();
|
||||
var timestamp = data.value.readDoubleBE(4);
|
||||
var inputValues = [];
|
||||
var inputValuesLength = data.value.readUInt16BE(12);
|
||||
for(var i = 0; i < inputValuesLength / 8; i++) {
|
||||
inputValues.push(buffer.readDoubleBE(i * 8 + 14));
|
||||
}
|
||||
var transaction = new bitcore.Transaction(data.value.slice(inputValues.length * 8 + 14));
|
||||
transaction.__height = height;
|
||||
transaction.__inputValues = inputValues;
|
||||
transaction.__timestamp = timestamp;
|
||||
//console.log(txkey, transaction.toObject());
|
||||
console.log(data.value);
|
||||
console.log(transaction.__height, transaction.__inputValues, transaction.__timestamp);
|
||||
//console.log(data.key.toString('hex'), data.value.toString('hex'));
|
||||
});
|
||||
|
||||
stream.on('end', function() {
|
||||
console.log('end');
|
||||
});
|
||||
11
contrib/restart_bitcore_node.js
Executable file
11
contrib/restart_bitcore_node.js
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# helper script to run bwdb and/or restart it
|
||||
|
||||
# execute thie script and then simply tail /tmp/bwdb-out
|
||||
# e.g. ./contrib/restart_bwdb.sh && tail -f /tmp/bwdb-out
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
pkill -2 -x bitcore
|
||||
wait
|
||||
exec $DIR/../bin/bitcore-node start >> /tmp/bwdb-out 2>&1 &
|
||||
149
contrib/streamtest.js
Normal file
149
contrib/streamtest.js
Normal file
@ -0,0 +1,149 @@
|
||||
'use strict'
|
||||
var Readable = require('stream').Readable;
|
||||
var Writable = require('stream').Writable;
|
||||
var Transform = require('stream').Transform;
|
||||
var inherits = require('util').inherits;
|
||||
var async = require('async');
|
||||
|
||||
function main() {
|
||||
var blockStream = new BlockStream();
|
||||
var processConcurrent = new ProcessConcurrent();
|
||||
var processSerial = new ProcessSerial();
|
||||
var writeStreamFast = new WriteStreamFast();
|
||||
var writeStreamSlow = new WriteStreamSlow();
|
||||
|
||||
var start = Date.now();
|
||||
|
||||
writeStreamFast.on('finish', function() {
|
||||
var end = Date.now();
|
||||
console.log('Total time: ', (end - start) + ' ms');
|
||||
console.log('Concurrent write time: ', writeStreamSlow.writeTime + ' ms');
|
||||
console.log('Serial write time: ', writeStreamFast.writeTime + ' ms');
|
||||
});
|
||||
|
||||
blockStream
|
||||
.pipe(processConcurrent)
|
||||
.pipe(writeStreamSlow);
|
||||
|
||||
blockStream
|
||||
.pipe(processSerial)
|
||||
.pipe(writeStreamFast);
|
||||
}
|
||||
|
||||
function BlockStream() {
|
||||
Readable.call(this, {objectMode: true, highWaterMark: 10});
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
inherits(BlockStream, Readable);
|
||||
|
||||
BlockStream.prototype._read = function() {
|
||||
var self = this;
|
||||
console.log('_read');
|
||||
|
||||
setTimeout(function() {
|
||||
self.height++;
|
||||
if(self.height > 40) {
|
||||
self.push(null);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('ReadStream block ', self.height);
|
||||
console.log(self.push({height: self.height}));
|
||||
}, 500);
|
||||
};
|
||||
|
||||
function ProcessSerial() {
|
||||
Transform.call(this, {objectMode: true, highWaterMark: 10});
|
||||
}
|
||||
|
||||
inherits(ProcessSerial, Transform);
|
||||
|
||||
ProcessSerial.prototype._transform = function(block, enc, callback) {
|
||||
var operations = [{index1: block.height}, {index2: block.height}];
|
||||
setTimeout(function() {
|
||||
var obj = {
|
||||
tipHeight: block.height,
|
||||
operations: operations
|
||||
};
|
||||
|
||||
callback(null, obj);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
function ProcessConcurrent() {
|
||||
Transform.call(this, {objectMode: true, highWaterMark: 10});
|
||||
this.operations = [];
|
||||
this.lastHeight = 0;
|
||||
};
|
||||
|
||||
inherits(ProcessConcurrent, Transform);
|
||||
|
||||
ProcessConcurrent.prototype._transform = function(block, enc, callback) {
|
||||
var self = this;
|
||||
|
||||
self.lastHeight = block.height;
|
||||
|
||||
setTimeout(function() {
|
||||
self.operations = self.operations.concat([{index3: block.height}, {index4: block.height}]);
|
||||
|
||||
console.log(self.operations.length);
|
||||
if(self.operations.length >= 10) {
|
||||
var obj = {
|
||||
concurrentTipHeight: self.lastHeight,
|
||||
operations: self.operations
|
||||
};
|
||||
self.operations = [];
|
||||
|
||||
return callback(null, obj);
|
||||
}
|
||||
|
||||
callback();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
ProcessConcurrent.prototype._flush = function(callback) {
|
||||
if(this.operations.length) {
|
||||
var obj = {
|
||||
concurrentTipHeight: this.lastHeight,
|
||||
operations: this.operations
|
||||
};
|
||||
|
||||
this.operations = [];
|
||||
return callback(null, operations);
|
||||
}
|
||||
};
|
||||
|
||||
function WriteStreamSlow() {
|
||||
Writable.call(this, {objectMode: true, highWaterMark: 10});
|
||||
this.writeTime = 0;
|
||||
}
|
||||
|
||||
inherits(WriteStreamSlow, Writable);
|
||||
|
||||
WriteStreamSlow.prototype._write = function(operations, enc, callback) {
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
console.log('WriteStreamSlow block ', operations.concurrentTipHeight);
|
||||
self.writeTime += 2000;
|
||||
callback();
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
function WriteStreamFast() {
|
||||
Writable.call(this, {objectMode: true, highWaterMark: 1});
|
||||
this.writeTime = 0;
|
||||
}
|
||||
|
||||
inherits(WriteStreamFast, Writable);
|
||||
|
||||
WriteStreamFast.prototype._write = function(operations, enc, callback) {
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
console.log('WriteStreamFast block ', operations.tipHeight);
|
||||
self.writeTime += 1000;
|
||||
callback();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
main();
|
||||
20
contrib/systemd_bitcore.service
Normal file
20
contrib/systemd_bitcore.service
Normal file
@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=BWDB
|
||||
Requires=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/usr/opt/bitcore
|
||||
ExecStart=/usr/bin/bwdb
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=15
|
||||
User=bitcore
|
||||
ExecStartPre=/bin/mkdir -p /run/bwdb
|
||||
ExecStartPre=/bin/chown bitcore:bitcore /run/bwdb
|
||||
ExecStartPre=/bin/chmod 755 /run/bwdb
|
||||
PermissionsStartOnly=true
|
||||
TimeoutStopSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
1
contrib/weirdtx.json
Normal file
1
contrib/weirdtx.json
Normal file
File diff suppressed because one or more lines are too long
1
index.js
1
index.js
@ -6,7 +6,6 @@ module.exports.Service = require('./lib/service');
|
||||
module.exports.errors = require('./lib/errors');
|
||||
|
||||
module.exports.services = {};
|
||||
module.exports.services.Bitcoin = require('./lib/services/bitcoind');
|
||||
module.exports.services.Web = require('./lib/services/web');
|
||||
|
||||
module.exports.scaffold = {};
|
||||
|
||||
13
lib/constants.js
Normal file
13
lib/constants.js
Normal file
@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
module.exports = {
|
||||
BITCOIN_GENESIS_HASH: {
|
||||
livenet: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f',
|
||||
regtest: '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206',
|
||||
testnet: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', //this is testnet3
|
||||
testnet5: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943' //this is testnet5
|
||||
},
|
||||
DB_PREFIX: new Buffer('ffff', 'hex')
|
||||
};
|
||||
|
||||
@ -38,7 +38,9 @@ Logger.prototype.error = function() {
|
||||
* #debug
|
||||
*/
|
||||
Logger.prototype.debug = function() {
|
||||
this._log.apply(this, ['magenta', 'debug'].concat(Array.prototype.slice.call(arguments)));
|
||||
if (process.env.BITCORE_ENV === 'debug') {
|
||||
this._log.apply(this, ['green', 'debug'].concat(Array.prototype.slice.call(arguments)));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -63,7 +65,10 @@ Logger.prototype._log = function(color) {
|
||||
var typeString = colors[color].italic(level + ':');
|
||||
args[0] = '[' + date.toISOString() + ']' + ' ' + typeString + ' ' + args[0];
|
||||
}
|
||||
var fn = console[level] || console.log;
|
||||
var fn = console.log;
|
||||
if (level === 'error') {
|
||||
fn = console.error;
|
||||
}
|
||||
fn.apply(console, args);
|
||||
};
|
||||
|
||||
|
||||
172
lib/node.js
172
lib/node.js
@ -3,90 +3,53 @@
|
||||
var util = require('util');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var Networks = bitcore.Networks;
|
||||
var $ = bitcore.util.preconditions;
|
||||
var _ = bitcore.deps._;
|
||||
var index = require('./');
|
||||
var log = index.log;
|
||||
var Bus = require('./bus');
|
||||
var errors = require('./errors');
|
||||
|
||||
/**
|
||||
* A node is a hub of services, and will manage starting and stopping the services in
|
||||
* the correct order based the the dependency chain. The node also holds common configuration
|
||||
* properties that can be shared across services, such as network settings.
|
||||
*
|
||||
* The array of services should have the format:
|
||||
* ```js
|
||||
* {
|
||||
* name: 'bitcoind',
|
||||
* config: {}, // options to pass into constructor
|
||||
* module: ServiceConstructor
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param {Object} config - The configuration of the node
|
||||
* @param {Array} config.formatLogs - Option to disable formatting of logs
|
||||
* @param {Array} config.services - The array of services
|
||||
* @param {Number} config.port - The HTTP port for services
|
||||
* @param {Boolean} config.https - Enable https
|
||||
* @param {Object} config.httpsOptions - Options for https
|
||||
* @param {String} config.httpsOptions.key - Path to key file
|
||||
* @param {String} config.httpsOptions.cert - Path to cert file
|
||||
* @param {}
|
||||
*/
|
||||
function Node(config) {
|
||||
/* jshint maxstatements: 20 */
|
||||
|
||||
if(!(this instanceof Node)) {
|
||||
return new Node(config);
|
||||
}
|
||||
this.configPath = config.path;
|
||||
this.errors = errors;
|
||||
this.log = log;
|
||||
|
||||
this._init(config);
|
||||
|
||||
if (!_.isUndefined(config.formatLogs)) {
|
||||
this.log.formatting = config.formatLogs ? true : false;
|
||||
}
|
||||
|
||||
this.network = null;
|
||||
this.services = {};
|
||||
this._unloadedServices = [];
|
||||
|
||||
// TODO type check the arguments of config.services
|
||||
if (config.services) {
|
||||
$.checkArgument(Array.isArray(config.services));
|
||||
this._unloadedServices = config.services;
|
||||
}
|
||||
this.port = config.port;
|
||||
this.https = config.https;
|
||||
this.httpsOptions = config.httpsOptions;
|
||||
this._setNetwork(config);
|
||||
}
|
||||
|
||||
util.inherits(Node, EventEmitter);
|
||||
|
||||
/**
|
||||
* Will set the this.network based on a network string.
|
||||
* @param {Object} config
|
||||
* @param {String} config.network - Possible options "testnet", "regtest" or "livenet"
|
||||
*/
|
||||
Node.prototype._setNetwork = function(config) {
|
||||
if (config.network === 'testnet') {
|
||||
this.network = Networks.get('testnet');
|
||||
} else if (config.network === 'regtest') {
|
||||
Networks.enableRegtest();
|
||||
this.network = Networks.get('regtest');
|
||||
} else {
|
||||
this.network = Networks.defaultNetwork;
|
||||
}
|
||||
$.checkState(this.network, 'Unrecognized network');
|
||||
Node.prototype._init = function(config) {
|
||||
this.configPath = config.path;
|
||||
this.errors = errors;
|
||||
this.log = log;
|
||||
|
||||
this.datadir = config.datadir;
|
||||
this.network = null;
|
||||
this.services = {};
|
||||
this._unloadedServices = [];
|
||||
|
||||
this.port = config.port;
|
||||
this.https = config.https;
|
||||
this.httpsOptions = config.httpsOptions;
|
||||
this._setNetwork(config);
|
||||
};
|
||||
|
||||
Node.prototype._setNetwork = function(config) {
|
||||
this.network = config.network;
|
||||
};
|
||||
|
||||
/**
|
||||
* Will instantiate a new Bus for this node.
|
||||
* @returns {Bus}
|
||||
*/
|
||||
Node.prototype.openBus = function(options) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
@ -94,10 +57,6 @@ Node.prototype.openBus = function(options) {
|
||||
return new Bus({node: this, remoteAddress: options.remoteAddress});
|
||||
};
|
||||
|
||||
/**
|
||||
* Will get an array of API method descriptions from all of the available services.
|
||||
* @returns {Array}
|
||||
*/
|
||||
Node.prototype.getAllAPIMethods = function() {
|
||||
var methods = [];
|
||||
for(var i in this.services) {
|
||||
@ -109,10 +68,6 @@ Node.prototype.getAllAPIMethods = function() {
|
||||
return methods;
|
||||
};
|
||||
|
||||
/**
|
||||
* Will get an array of events from all of the available services.
|
||||
* @returns {Array}
|
||||
*/
|
||||
Node.prototype.getAllPublishEvents = function() {
|
||||
var events = [];
|
||||
for (var i in this.services) {
|
||||
@ -124,16 +79,8 @@ Node.prototype.getAllPublishEvents = function() {
|
||||
return events;
|
||||
};
|
||||
|
||||
/**
|
||||
* Will organize services into the order that they should be started
|
||||
* based on the service's dependencies.
|
||||
* @returns {Array}
|
||||
*/
|
||||
Node.prototype.getServiceOrder = function() {
|
||||
Node.prototype._getServiceOrder = function(services) {
|
||||
|
||||
var services = this._unloadedServices;
|
||||
|
||||
// organize data for sorting
|
||||
var names = [];
|
||||
var servicesByName = {};
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
@ -150,12 +97,10 @@ Node.prototype.getServiceOrder = function() {
|
||||
|
||||
var name = names[i];
|
||||
var service = servicesByName[name];
|
||||
$.checkState(service, 'Required dependency "' + name + '" not available.');
|
||||
assert(service, 'Required dependency "' + name + '" not available.');
|
||||
|
||||
// first add the dependencies
|
||||
addToStack(service.module.dependencies);
|
||||
|
||||
// add to the stack if it hasn't been added
|
||||
if(!stackNames[name]) {
|
||||
stack.push(service);
|
||||
stackNames[name] = true;
|
||||
@ -169,17 +114,6 @@ Node.prototype.getServiceOrder = function() {
|
||||
return stack;
|
||||
};
|
||||
|
||||
/**
|
||||
* Will instantiate an instance of the service module, add it to the node
|
||||
* services, start the service and add available API methods to the node and
|
||||
* checking for any conflicts.
|
||||
* @param {Object} serviceInfo
|
||||
* @param {String} serviceInfo.name - The name of the service
|
||||
* @param {Object} serviceInfo.module - The service module constructor
|
||||
* @param {Object} serviceInfo.config - Options to pass into the constructor
|
||||
* @param {Function} callback - Called when the service is started
|
||||
* @private
|
||||
*/
|
||||
Node.prototype._startService = function(serviceInfo, callback) {
|
||||
var self = this;
|
||||
|
||||
@ -187,9 +121,9 @@ Node.prototype._startService = function(serviceInfo, callback) {
|
||||
|
||||
var config;
|
||||
if (serviceInfo.config) {
|
||||
$.checkState(_.isObject(serviceInfo.config));
|
||||
$.checkState(!serviceInfo.config.node);
|
||||
$.checkState(!serviceInfo.config.name);
|
||||
assert(_.isObject(serviceInfo.config));
|
||||
assert(!serviceInfo.config.node);
|
||||
assert(!serviceInfo.config.name);
|
||||
config = serviceInfo.config;
|
||||
} else {
|
||||
config = {};
|
||||
@ -199,7 +133,6 @@ Node.prototype._startService = function(serviceInfo, callback) {
|
||||
config.name = serviceInfo.name;
|
||||
var service = new serviceInfo.module(config);
|
||||
|
||||
// include in loaded services
|
||||
self.services[serviceInfo.name] = service;
|
||||
|
||||
service.start(function(err) {
|
||||
@ -207,7 +140,6 @@ Node.prototype._startService = function(serviceInfo, callback) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// add API methods
|
||||
if (service.getAPIMethods) {
|
||||
var methodData = service.getAPIMethods();
|
||||
var methodNameConflicts = [];
|
||||
@ -239,18 +171,16 @@ Node.prototype._startService = function(serviceInfo, callback) {
|
||||
Node.prototype._logTitle = function() {
|
||||
if (this.configPath) {
|
||||
log.info('Using config:', this.configPath);
|
||||
log.info('Using network:', this.getNetworkName());
|
||||
log.info('Using network:', this.network);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Will start all running services in the order based on the dependency chain.
|
||||
* @param {Function} callback - Called when all services are started
|
||||
*/
|
||||
Node.prototype.start = function(callback) {
|
||||
var self = this;
|
||||
var servicesOrder = this.getServiceOrder();
|
||||
|
||||
var services = this._unloadedServices;
|
||||
|
||||
var servicesOrder = this._getServiceOrder(services);
|
||||
|
||||
self._logTitle();
|
||||
|
||||
@ -260,9 +190,11 @@ Node.prototype.start = function(callback) {
|
||||
self._startService(service, next);
|
||||
},
|
||||
function(err) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.emit('ready');
|
||||
callback();
|
||||
}
|
||||
@ -277,32 +209,32 @@ Node.prototype.getNetworkName = function() {
|
||||
return network;
|
||||
};
|
||||
|
||||
/**
|
||||
* Will stop all running services in the reverse order that they
|
||||
* were initially started.
|
||||
* @param {Function} callback - Called when all services are stopped
|
||||
*/
|
||||
Node.prototype.stop = function(callback) {
|
||||
|
||||
log.info('Beginning shutdown');
|
||||
var self = this;
|
||||
var services = this.getServiceOrder().reverse();
|
||||
var services = this._getServiceOrder(this._unloadedServices).reverse();
|
||||
|
||||
this.stopping = true;
|
||||
this.emit('stopping');
|
||||
|
||||
async.eachSeries(
|
||||
services,
|
||||
function(service, next) {
|
||||
if (self.services[service.name]) {
|
||||
log.info('Stopping ' + service.name);
|
||||
self.services[service.name].stop(next);
|
||||
} else {
|
||||
log.info('Stopping ' + service.name + ' (not started)');
|
||||
setImmediate(next);
|
||||
}
|
||||
},
|
||||
callback
|
||||
);
|
||||
|
||||
services,
|
||||
function(service, next) {
|
||||
if (self.services[service.name]) {
|
||||
log.info('Stopping ' + service.name);
|
||||
self.services[service.name].stop(next);
|
||||
} else {
|
||||
log.info('Stopping ' + service.name + ' (not started)');
|
||||
setImmediate(next);
|
||||
}
|
||||
},
|
||||
function() {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Node;
|
||||
|
||||
@ -9,7 +9,7 @@ var path = require('path');
|
||||
var packageFile = require('../../package.json');
|
||||
var mkdirp = require('mkdirp');
|
||||
var fs = require('fs');
|
||||
var defaultBaseConfig = require('./default-base-config');
|
||||
var defaultConfig = require('./default-config');
|
||||
|
||||
var version = '^' + packageFile.version;
|
||||
|
||||
@ -55,7 +55,7 @@ function createConfigDirectory(options, configDir, isGlobal, done) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var configInfo = defaultBaseConfig(options);
|
||||
var configInfo = defaultConfig(options);
|
||||
var config = configInfo.config;
|
||||
|
||||
var configJSON = JSON.stringify(config, null, 2);
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
|
||||
/**
|
||||
* Will return the path and default bitcore-node configuration on environment variables
|
||||
* or default locations.
|
||||
* @param {Object} options
|
||||
* @param {String} options.network - "testnet" or "livenet"
|
||||
* @param {String} options.datadir - Absolute path to bitcoin database directory
|
||||
*/
|
||||
function getDefaultBaseConfig(options) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
return {
|
||||
path: process.cwd(),
|
||||
config: {
|
||||
network: options.network || 'livenet',
|
||||
port: 3001,
|
||||
services: ['bitcoind', 'web'],
|
||||
servicesConfig: {
|
||||
bitcoind: {
|
||||
spawn: {
|
||||
datadir: options.datadir || path.resolve(process.env.HOME, '.bitcoin'),
|
||||
exec: path.resolve(__dirname, '../../bin/bitcoind')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getDefaultBaseConfig;
|
||||
@ -3,6 +3,11 @@
|
||||
var path = require('path');
|
||||
var mkdirp = require('mkdirp');
|
||||
var fs = require('fs');
|
||||
var packageJson = require('../../package');
|
||||
|
||||
function getMajorVersion(versionString) {
|
||||
return parseInt(versionString.split('.')[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return the path and default bitcore-node configuration. It will search for the
|
||||
@ -24,27 +29,41 @@ function getDefaultConfig(options) {
|
||||
mkdirp.sync(defaultPath);
|
||||
}
|
||||
|
||||
var defaultServices = ['bitcoind', 'web'];
|
||||
if (options.additionalServices) {
|
||||
defaultServices = defaultServices.concat(options.additionalServices);
|
||||
if (fs.existsSync(defaultConfigFile)) {
|
||||
var currentConfig = require(defaultConfigFile);
|
||||
|
||||
// config must have a `version` field with major equal to package major version
|
||||
if(currentConfig.version && getMajorVersion(packageJson.version) === getMajorVersion(currentConfig.version)) {
|
||||
return {
|
||||
path: defaultPath,
|
||||
config: currentConfig
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`The configuration file at '${defaultConfigFile}' is incompatible with this version of Bitcore.`);
|
||||
|
||||
var now = new Date();
|
||||
// bitcore-node.YYYY-MM-DD.UnixTimestamp.json
|
||||
var backupFileName = `bitcore-node.${now.getUTCFullYear()}-${now.getUTCMonth()}-${now.getUTCDate()}.${now.getTime()}.json`;
|
||||
var backupFile = path.resolve(defaultPath, backupFileName);
|
||||
fs.renameSync(defaultConfigFile, backupFile);
|
||||
console.log(`The previous configuration file has been moved to: ${backupFile}.`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(defaultConfigFile)) {
|
||||
var defaultConfig = {
|
||||
network: 'livenet',
|
||||
port: 3001,
|
||||
services: defaultServices,
|
||||
servicesConfig: {
|
||||
bitcoind: {
|
||||
spawn: {
|
||||
datadir: path.resolve(defaultPath, './data'),
|
||||
exec: path.resolve(__dirname, '../../bin/bitcoind')
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
fs.writeFileSync(defaultConfigFile, JSON.stringify(defaultConfig, null, 2));
|
||||
}
|
||||
console.log(`Creating a new configuration file at: ${defaultConfigFile}.`);
|
||||
|
||||
var defaultServices = [
|
||||
'address',
|
||||
'block',
|
||||
'db',
|
||||
'fee',
|
||||
'header',
|
||||
'mempool',
|
||||
'p2p',
|
||||
'timestamp',
|
||||
'transaction',
|
||||
'web'
|
||||
];
|
||||
|
||||
var defaultDataDir = path.resolve(defaultPath, './data');
|
||||
|
||||
@ -52,6 +71,23 @@ function getDefaultConfig(options) {
|
||||
mkdirp.sync(defaultDataDir);
|
||||
}
|
||||
|
||||
var defaultConfig = {
|
||||
version: packageJson.version,
|
||||
network: 'livenet',
|
||||
port: 3001,
|
||||
services: options.additionalServices ? defaultServices.concat(options.additionalServices) : defaultServices,
|
||||
datadir: defaultDataDir,
|
||||
servicesConfig: {
|
||||
'insight-api': {
|
||||
cwdRequirePath: 'node_modules/insight-api'
|
||||
},
|
||||
'insight-ui': {
|
||||
cwdRequirePath: 'node_modules/insight-ui'
|
||||
}
|
||||
}
|
||||
};
|
||||
fs.writeFileSync(defaultConfigFile, JSON.stringify(defaultConfig, null, 2));
|
||||
|
||||
var config = JSON.parse(fs.readFileSync(defaultConfigFile, 'utf-8'));
|
||||
|
||||
return {
|
||||
|
||||
@ -7,81 +7,21 @@ var bitcore = require('bitcore-lib');
|
||||
var _ = bitcore.deps._;
|
||||
var log = index.log;
|
||||
var shuttingDown = false;
|
||||
var fs = require('fs');
|
||||
|
||||
log.debug = function() {};
|
||||
|
||||
/**
|
||||
* Checks for configuration options from version 2. This includes an "address" and
|
||||
* "db" service, or having "datadir" at the root of the config.
|
||||
*/
|
||||
function checkConfigVersion2(fullConfig) {
|
||||
var datadirUndefined = _.isUndefined(fullConfig.datadir);
|
||||
var addressDefined = (fullConfig.services.indexOf('address') >= 0);
|
||||
var dbDefined = (fullConfig.services.indexOf('db') >= 0);
|
||||
|
||||
if (!datadirUndefined || addressDefined || dbDefined) {
|
||||
|
||||
console.warn('\nConfiguration file is not compatible with this version. \n' +
|
||||
'A reindex for bitcoind is necessary for this upgrade with the "reindex=1" bitcoin.conf option. \n' +
|
||||
'There are changes necessary in both bitcoin.conf and bitcore-node.json. \n\n' +
|
||||
'To upgrade please see the details below and documentation at: \n' +
|
||||
'https://github.com/bitpay/bitcore-node/blob/bitcoind/docs/upgrade.md \n');
|
||||
|
||||
if (!datadirUndefined) {
|
||||
console.warn('Please remove "datadir" and add it to the config at ' + fullConfig.path + ' with:');
|
||||
var missingConfig = {
|
||||
servicesConfig: {
|
||||
bitcoind: {
|
||||
spawn: {
|
||||
datadir: fullConfig.datadir,
|
||||
exec: path.resolve(__dirname, '../../bin/bitcoind')
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
console.warn(JSON.stringify(missingConfig, null, 2) + '\n');
|
||||
}
|
||||
|
||||
if (addressDefined || dbDefined) {
|
||||
console.warn('Please remove "address" and/or "db" from "services" in: ' + fullConfig.path + '\n');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will instantiate and start a Node, requiring the necessary service
|
||||
* modules, and registering event handlers.
|
||||
* @param {Object} options
|
||||
* @param {Object} options.servicesPath - The path to the location of service modules
|
||||
* @param {String} options.path - The absolute path of the configuration file
|
||||
* @param {Object} options.config - The parsed bitcore-node.json configuration file
|
||||
* @param {Array} options.config.services - An array of services names.
|
||||
* @param {Object} options.config.servicesConfig - Parameters to pass to each service
|
||||
* @param {String} options.config.network - 'livenet', 'testnet' or 'regtest
|
||||
* @param {Number} options.config.port - The port to use for the web service
|
||||
*/
|
||||
function start(options) {
|
||||
/* jshint maxstatements: 20 */
|
||||
|
||||
var fullConfig = _.clone(options.config);
|
||||
|
||||
var servicesPath;
|
||||
if (options.servicesPath) {
|
||||
servicesPath = options.servicesPath; // services are in a different directory than the config
|
||||
servicesPath = options.servicesPath;
|
||||
} else {
|
||||
servicesPath = options.path; // defaults to the same directory
|
||||
servicesPath = options.path;
|
||||
}
|
||||
|
||||
fullConfig.path = path.resolve(options.path, './bitcore-node.json');
|
||||
|
||||
if (checkConfigVersion2(fullConfig)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fullConfig.services = start.setupServices(require, servicesPath, options.config);
|
||||
|
||||
var node = new BitcoreNode(fullConfig);
|
||||
@ -116,39 +56,106 @@ function start(options) {
|
||||
* @param {Object} service
|
||||
*/
|
||||
function checkService(service) {
|
||||
// check that the service supports expected methods
|
||||
if (!service.module.prototype ||
|
||||
!service.module.dependencies ||
|
||||
!service.module.prototype.start ||
|
||||
!service.module.prototype.stop) {
|
||||
throw new Error(
|
||||
'Could not load service "' + service.name + '" as it does not support necessary methods and properties.'
|
||||
);
|
||||
'Could not load service "' +
|
||||
service.name +
|
||||
'" as it does not support necessary methods and properties.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will require a module from local services directory first
|
||||
* and then from available node_modules
|
||||
* @param {Function} req
|
||||
* @param {Object} service
|
||||
*/
|
||||
function loadModule(req, service) {
|
||||
try {
|
||||
// first try in the built-in bitcore-node services directory
|
||||
service.module = req(path.resolve(__dirname, '../services/' + service.name));
|
||||
} catch(e) {
|
||||
function lookInRequirePathConfig(req, service) {
|
||||
if (!service.config.requirePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the package.json specifies a specific file to use
|
||||
try {
|
||||
if (fs.statSync(service.config.requirePath).isDirectory()) {
|
||||
return req(service.config.requirePath);
|
||||
}
|
||||
var serviceFile = service.config.requirePath.replace(/.js$/, '');
|
||||
return req(serviceFile);
|
||||
} catch(e) {
|
||||
log.info('Checked the service\'s requirePath value, ' +
|
||||
'but could not find the service, checking elsewhere. ' +
|
||||
'Error caught: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function lookInCwd(req, service) {
|
||||
var location = service.config.cwdRequirePath ? service.config.cwdRequirePath : service.name;
|
||||
try {
|
||||
return req(process.cwd() + '/' + location);
|
||||
} catch(e) {
|
||||
if(e.code !== 'MODULE_NOT_FOUND') {
|
||||
log.error(e);
|
||||
}
|
||||
log.info('Checked the current working directory for service: ' + location);
|
||||
}
|
||||
}
|
||||
|
||||
function lookInBuiltInPath(req, service) {
|
||||
try {
|
||||
var serviceFile = path.resolve(__dirname, '../services/' + service.name);
|
||||
return req(serviceFile);
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
if(e.code !== 'MODULE_NOT_FOUND') {
|
||||
log.error(e);
|
||||
}
|
||||
log.info('Checked the built-in path: lib/services, for service: ' + service.name);
|
||||
}
|
||||
}
|
||||
|
||||
function lookInModuleManifest(req, service) {
|
||||
try {
|
||||
var servicePackage = req(service.name + '/package.json');
|
||||
var serviceModule = service.name;
|
||||
if (servicePackage.bitcoreNode) {
|
||||
serviceModule = service.name + '/' + servicePackage.bitcoreNode;
|
||||
serviceModule = serviceModule + '/' + servicePackage.bitcoreNode;
|
||||
return req(serviceModule);
|
||||
}
|
||||
service.module = req(serviceModule);
|
||||
} catch(e) {
|
||||
log.info('Checked the module\'s package.json for service: ' + service.name);
|
||||
}
|
||||
}
|
||||
|
||||
function loadModule(req, service) {
|
||||
var serviceCode;
|
||||
|
||||
//first, if we have explicitly set the require path for our service:
|
||||
serviceCode = lookInRequirePathConfig(req, service);
|
||||
|
||||
//second, look in the current working directory (of the controlling terminal, if there is one) for the service code
|
||||
if(!serviceCode) {
|
||||
serviceCode = lookInCwd(req, service);
|
||||
}
|
||||
|
||||
//third, try the built-in services
|
||||
if(!serviceCode) {
|
||||
serviceCode = lookInBuiltInPath(req, service);
|
||||
}
|
||||
|
||||
//fourth, see if there is directory in our module search path that has a
|
||||
//package.json file, if so, then see if there is a bitcoreNode field, if so
|
||||
//use this as the path to the service module
|
||||
if(!serviceCode) {
|
||||
serviceCode = lookInModuleManifest(req, service);
|
||||
}
|
||||
|
||||
if (!serviceCode) {
|
||||
throw new Error('Attempted to load the ' + service.name + ' service from: ' +
|
||||
'the requirePath in the services\' config, then "' +
|
||||
process.cwd() + '" then from: "' + __dirname + '/../lib/services' + '" finally from: "' +
|
||||
process.cwd() + '/package.json" - bitcoreNode field. All paths failed to find valid nodeJS code.');
|
||||
}
|
||||
|
||||
service.module = serviceCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will loop over the configuration for services and require the
|
||||
* specified modules, and assemble an array in this format:
|
||||
@ -173,6 +180,7 @@ function setupServices(req, servicesPath, config) {
|
||||
if (config.services) {
|
||||
for (var i = 0; i < config.services.length; i++) {
|
||||
var service = {};
|
||||
|
||||
service.name = config.services[i];
|
||||
|
||||
var hasConfig = config.servicesConfig && config.servicesConfig[service.name];
|
||||
@ -187,11 +195,6 @@ function setupServices(req, servicesPath, config) {
|
||||
return services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will shutdown a node and then the process
|
||||
* @param {Object} _process - The Node.js process object
|
||||
* @param {Node} node - The Bitcore Node instance
|
||||
*/
|
||||
function cleanShutdown(_process, node) {
|
||||
node.stop(function(err) {
|
||||
if(err) {
|
||||
@ -203,15 +206,6 @@ function cleanShutdown(_process, node) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will handle all the shutdown tasks that need to take place to ensure a safe exit
|
||||
* @param {Object} options
|
||||
* @param {String} options.sigint - The signal given was a SIGINT
|
||||
* @param {Array} options.exit - The signal given was an uncaughtException
|
||||
* @param {Object} _process - The Node.js process
|
||||
* @param {Node} node
|
||||
* @param {Error} error
|
||||
*/
|
||||
function exitHandler(options, _process, node, err) {
|
||||
if (err) {
|
||||
log.error('uncaught exception:', err);
|
||||
@ -233,17 +227,8 @@ function exitHandler(options, _process, node, err) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will register event handlers to stop the node for `process` events
|
||||
* `uncaughtException` and `SIGINT`.
|
||||
* @param {Object} _process - The Node.js process
|
||||
* @param {Node} node
|
||||
*/
|
||||
function registerExitHandlers(_process, node) {
|
||||
//catches uncaught exceptions
|
||||
_process.on('uncaughtException', exitHandler.bind(null, {exit:true}, _process, node));
|
||||
|
||||
//catches ctrl+c event
|
||||
_process.on('SIGINT', exitHandler.bind(null, {sigint:true}, _process, node));
|
||||
}
|
||||
|
||||
@ -252,4 +237,3 @@ module.exports.registerExitHandlers = registerExitHandlers;
|
||||
module.exports.exitHandler = exitHandler;
|
||||
module.exports.setupServices = setupServices;
|
||||
module.exports.cleanShutdown = cleanShutdown;
|
||||
module.exports.checkConfigVersion2 = checkConfigVersion2;
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
'use strict';
|
||||
/* exported LRU, assert, constants */
|
||||
|
||||
var util = require('util');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var LRU = require('lru-cache');
|
||||
var assert = require('assert');
|
||||
var constants = require('./constants');
|
||||
|
||||
var Service = function(options) {
|
||||
EventEmitter.call(this);
|
||||
|
||||
118
lib/services/address/encoding.js
Normal file
118
lib/services/address/encoding.js
Normal file
@ -0,0 +1,118 @@
|
||||
'use strict';
|
||||
|
||||
function Encoding(servicePrefix) {
|
||||
this.servicePrefix = servicePrefix;
|
||||
}
|
||||
|
||||
Encoding.prototype.encodeAddressIndexKey = function(address, height, txid, index, input, timestamp) {
|
||||
var prefix = new Buffer('00', 'hex');
|
||||
var buffers = [this.servicePrefix, prefix];
|
||||
|
||||
var addressSizeBuffer = new Buffer(1);
|
||||
addressSizeBuffer.writeUInt8(address.length);
|
||||
var addressBuffer = new Buffer(address, 'utf8');
|
||||
|
||||
buffers.push(addressSizeBuffer);
|
||||
buffers.push(addressBuffer);
|
||||
|
||||
var heightBuffer = new Buffer(4);
|
||||
heightBuffer.writeUInt32BE(height || 0);
|
||||
buffers.push(heightBuffer);
|
||||
|
||||
var txidBuffer = new Buffer(txid || Array(65).join('0'), 'hex');
|
||||
buffers.push(txidBuffer);
|
||||
|
||||
var indexBuffer = new Buffer(4);
|
||||
indexBuffer.writeUInt32BE(index || 0);
|
||||
buffers.push(indexBuffer);
|
||||
|
||||
// this is whether the address appears in an input (1) or output (0)
|
||||
var inputBuffer = new Buffer(1);
|
||||
inputBuffer.writeUInt8(input || 0);
|
||||
buffers.push(inputBuffer);
|
||||
|
||||
var timestampBuffer = new Buffer(4);
|
||||
timestampBuffer.writeUInt32BE(timestamp || 0);
|
||||
buffers.push(timestampBuffer);
|
||||
|
||||
return Buffer.concat(buffers);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeAddressIndexKey = function(buffer) {
|
||||
|
||||
var addressSize = buffer.readUInt8(3);
|
||||
var address = buffer.slice(4, addressSize + 4).toString('utf8');
|
||||
var height = buffer.readUInt32BE(addressSize + 4);
|
||||
var txid = buffer.slice(addressSize + 8, addressSize + 40).toString('hex');
|
||||
var index = buffer.readUInt32BE(addressSize + 40);
|
||||
var input = buffer.readUInt8(addressSize + 44);
|
||||
var timestamp = buffer.readUInt32BE(addressSize + 45);
|
||||
return {
|
||||
address: address,
|
||||
height: height,
|
||||
txid: txid,
|
||||
index: index,
|
||||
input: input,
|
||||
timestamp: timestamp
|
||||
};
|
||||
};
|
||||
|
||||
Encoding.prototype.encodeUtxoIndexKey = function(address, txid, outputIndex) {
|
||||
var prefix = new Buffer('01', 'hex');
|
||||
var buffers = [this.servicePrefix, prefix];
|
||||
|
||||
var addressSizeBuffer = new Buffer(1);
|
||||
addressSizeBuffer.writeUInt8(address.length);
|
||||
var addressBuffer = new Buffer(address, 'utf8');
|
||||
|
||||
buffers.push(addressSizeBuffer);
|
||||
buffers.push(addressBuffer);
|
||||
|
||||
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.decodeUtxoIndexKey = function(buffer) {
|
||||
var addressSize = buffer.readUInt8(3);
|
||||
var address = buffer.slice(4, addressSize + 4).toString('utf8');
|
||||
var txid = buffer.slice(addressSize + 4, addressSize + 36).toString('hex');
|
||||
var outputIndex = buffer.readUInt32BE(addressSize + 36);
|
||||
|
||||
return {
|
||||
address: address,
|
||||
txid: txid,
|
||||
outputIndex: outputIndex
|
||||
};
|
||||
};
|
||||
|
||||
Encoding.prototype.encodeUtxoIndexValue = function(height, satoshis, timestamp, scriptBuffer) {
|
||||
var heightBuffer = new Buffer(4);
|
||||
heightBuffer.writeUInt32BE(height);
|
||||
var satoshisBuffer = new Buffer(8);
|
||||
satoshisBuffer.writeDoubleBE(satoshis);
|
||||
var timestampBuffer = new Buffer(4);
|
||||
timestampBuffer.writeUInt32BE(timestamp || 0);
|
||||
return Buffer.concat([heightBuffer, satoshisBuffer, timestampBuffer, scriptBuffer]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeUtxoIndexValue = function(buffer) {
|
||||
var height = buffer.readUInt32BE();
|
||||
var satoshis = buffer.readDoubleBE(4);
|
||||
var timestamp = buffer.readUInt32BE(12);
|
||||
var scriptBuffer = buffer.slice(16);
|
||||
return {
|
||||
height: height,
|
||||
satoshis: satoshis,
|
||||
timestamp: timestamp,
|
||||
script: scriptBuffer
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Encoding;
|
||||
|
||||
556
lib/services/address/index.js
Normal file
556
lib/services/address/index.js
Normal file
@ -0,0 +1,556 @@
|
||||
'use strict';
|
||||
|
||||
var BaseService = require('../../service');
|
||||
var inherits = require('util').inherits;
|
||||
var async = require('async');
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
var bitcore = require('bitcore-lib');
|
||||
var Unit = bitcore.Unit;
|
||||
var _ = bitcore.deps._;
|
||||
var Encoding = require('./encoding');
|
||||
var utils = require('../../utils');
|
||||
var Transform = require('stream').Transform;
|
||||
var assert = require('assert');
|
||||
|
||||
var AddressService = function(options) {
|
||||
BaseService.call(this, options);
|
||||
this._db = this.node.services.db;
|
||||
this._tx = this.node.services.transaction;
|
||||
this._header = this.node.services.header;
|
||||
this._timestamp = this.node.services.timestamp;
|
||||
this._network = this.node.network;
|
||||
if (this._network === 'livenet') {
|
||||
this._network = 'main';
|
||||
}
|
||||
};
|
||||
|
||||
inherits(AddressService, BaseService);
|
||||
|
||||
AddressService.dependencies = [
|
||||
'p2p',
|
||||
'db',
|
||||
'block',
|
||||
'transaction',
|
||||
'timestamp'
|
||||
];
|
||||
|
||||
// ---- public function prototypes
|
||||
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
|
||||
var self = this;
|
||||
|
||||
options = options || {};
|
||||
options.from = options.from || 0;
|
||||
options.to = options.to || 0xffffffff;
|
||||
options.queryMempool = _.isUndefined(options.queryMempool) ? true : false;
|
||||
|
||||
async.mapLimit(addresses, 4, function(address, next) {
|
||||
|
||||
self._getAddressHistory(address, options, next);
|
||||
|
||||
}, function(err, txLists) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var txList = _.flatten(txLists);
|
||||
|
||||
var results = {
|
||||
totalItems: txList.length,
|
||||
from: options.from,
|
||||
to: options.to,
|
||||
items: txList
|
||||
};
|
||||
|
||||
callback(null, results);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
AddressService.prototype.getAddressSummary = function(address, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
options = options || {};
|
||||
options.from = options.from || 0;
|
||||
options.to = options.to || 0xffffffff;
|
||||
options.queryMempool = _.isUndefined(options.queryMempool) ? true : false;
|
||||
|
||||
var result = {
|
||||
addrStr: address,
|
||||
balance: 0,
|
||||
balanceSat: 0,
|
||||
totalReceived: 0,
|
||||
totalReceivedSat: 0,
|
||||
totalSent: 0,
|
||||
totalSentSat: 0,
|
||||
unconfirmedBalance: 0,
|
||||
unconfirmedBalanceSat: 0,
|
||||
unconfirmedTxApperances: 0,
|
||||
txApperances: 0,
|
||||
transactions: []
|
||||
};
|
||||
|
||||
// txid criteria
|
||||
var start = self._encoding.encodeAddressIndexKey(address, options.from);
|
||||
var end = self._encoding.encodeAddressIndexKey(address, options.to);
|
||||
|
||||
var criteria = {
|
||||
gte: start,
|
||||
lt: end
|
||||
};
|
||||
|
||||
// txid stream
|
||||
var txidStream = self._db.createKeyStream(criteria);
|
||||
|
||||
txidStream.on('close', function() {
|
||||
txidStream.unpipe();
|
||||
});
|
||||
|
||||
// tx stream
|
||||
var txStream = new Transform({ objectMode: true, highWaterMark: 1000 });
|
||||
|
||||
txStream.on('end', function() {
|
||||
result.balance = Unit.fromSatoshis(result.balanceSat).toBTC();
|
||||
result.totalReceived = Unit.fromSatoshis(result.totalReceivedSat).toBTC();
|
||||
result.totalSent = result.totalSentSat;
|
||||
result.unconfirmedBalance = result.unconfirmedBalanceSat;
|
||||
callback(null, result);
|
||||
});
|
||||
|
||||
// pipe txids into tx stream for processing
|
||||
txidStream.pipe(txStream);
|
||||
|
||||
txStream._transform = function(chunk, enc, callback) {
|
||||
|
||||
var key = self._encoding.decodeAddressIndexKey(chunk);
|
||||
|
||||
self._tx.getTransaction(key.txid, options, function(err, tx) {
|
||||
|
||||
if(err) {
|
||||
log.error(err);
|
||||
txStream.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
log.error('Could not find tx for txid: ' + key.txid + '. This should not be possible, check indexes.');
|
||||
txStream.emit('error', new Error('Txid should map to a tx.'));
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmations = self._header.getBestHeight() - key.height;
|
||||
|
||||
result.transactions.push(tx.txid());
|
||||
result.txApperances++;
|
||||
// is this an input?
|
||||
if (key.input) {
|
||||
|
||||
return self._transaction.getInputValues(key.txid, null, function(err, tx) {
|
||||
|
||||
if(err) {
|
||||
log.error(err);
|
||||
txStream.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
result.balanceSat -= tx.__inputValues[key.index];
|
||||
result.totalSentSat += tx.__inputValues[key.index];
|
||||
|
||||
if (confirmations < 1) {
|
||||
result.unconfirmedBalanceSat -= tx.__inputValues[key.index];
|
||||
result.unconfirmedTxApperances++;
|
||||
}
|
||||
|
||||
callback();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
result.balanceSat += tx.outputs[key.index].value;
|
||||
result.totalReceivedSat += tx.outputs[key.index].value;
|
||||
|
||||
if (confirmations < 1) {
|
||||
result.unconfirmedBalanceSat += tx.__inputValues[key.index];
|
||||
result.unconfirmedTxApperances++;
|
||||
}
|
||||
|
||||
callback();
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
txStream.on('error', function(err) {
|
||||
log.error(err);
|
||||
txStream.unpipe();
|
||||
});
|
||||
|
||||
txStream._flush = function(callback) {
|
||||
txStream.emit('end');
|
||||
callback();
|
||||
};
|
||||
};
|
||||
|
||||
AddressService.prototype.getAddressUnspentOutputs = function(address, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
options = options || {};
|
||||
options.from = options.from || 0;
|
||||
options.to = options.to || 0xffffffff;
|
||||
options.queryMempool = _.isUndefined(options.queryMempool) ? true : false;
|
||||
|
||||
var results = [];
|
||||
|
||||
var start = self._encoding.encodeUtxoIndexKey(address);
|
||||
var final = new Buffer(new Array(73).join('f'), 'hex');
|
||||
var end = Buffer.concat([ start.slice(0, -36), final ]);
|
||||
|
||||
var criteria = {
|
||||
gte: start,
|
||||
lt: end
|
||||
};
|
||||
|
||||
var utxoStream = self._db.createReadStream(criteria);
|
||||
|
||||
var streamErr;
|
||||
|
||||
utxoStream.on('end', function() {
|
||||
|
||||
if (streamErr) {
|
||||
return callback(streamErr);
|
||||
}
|
||||
|
||||
callback(null, results);
|
||||
|
||||
});
|
||||
|
||||
utxoStream.on('error', function(err) {
|
||||
streamErr = err;
|
||||
});
|
||||
|
||||
utxoStream.on('data', function(data) {
|
||||
|
||||
var key = self._encoding.decodeUtxoIndexKey(data.key);
|
||||
var value = self._encoding.decodeUtxoIndexValue(data.value);
|
||||
|
||||
results.push({
|
||||
address: address,
|
||||
txid: key.txid,
|
||||
vout: key.outputIndex,
|
||||
ts: value.timestamp,
|
||||
scriptPubKey: value.script.toString('hex'),
|
||||
amount: Unit.fromSatoshis(value.satoshis).toBTC(),
|
||||
confirmations: self._header.getBestHeight() - value.height,
|
||||
satoshis: value.satoshis,
|
||||
confirmationsFromCache: true
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
AddressService.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getAddressHistory', this, this.getAddressHistory, 2],
|
||||
['getAddressSummary', this, this.getAddressSummary, 1],
|
||||
['getAddressUnspentOutputs', this, this.getAddressUnspentOutputs, 1]
|
||||
];
|
||||
};
|
||||
|
||||
AddressService.prototype.start = function(callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
this._db.getPrefix(this.name, function(err, prefix) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
self._encoding = new Encoding(prefix);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
AddressService.prototype.stop = function(callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
|
||||
// ---- start private function prototypes
|
||||
AddressService.prototype._getAddressHistory = function(address, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
options = options || {};
|
||||
options.start = options.start || 0;
|
||||
options.end = options.end || 0xffffffff;
|
||||
|
||||
var endHeightBuf = new Buffer(4);
|
||||
endHeightBuf.writeUInt32BE(options.end);
|
||||
|
||||
if (_.isUndefined(options.queryMempool)) {
|
||||
options.queryMempool = true;
|
||||
}
|
||||
|
||||
var results = [];
|
||||
var start = self._encoding.encodeAddressIndexKey(address, options.start);
|
||||
var end = Buffer.concat([
|
||||
start.slice(0, address.length + 4),
|
||||
endHeightBuf,
|
||||
new Buffer(new Array(83).join('f'), 'hex')
|
||||
]);
|
||||
|
||||
var criteria = {
|
||||
gte: start,
|
||||
lte: end
|
||||
};
|
||||
|
||||
// txid stream
|
||||
var txidStream = self._db.createKeyStream(criteria);
|
||||
|
||||
txidStream.on('close', function() {
|
||||
txidStream.unpipe();
|
||||
});
|
||||
|
||||
// tx stream
|
||||
var txStream = new Transform({ objectMode: true, highWaterMark: 1000 });
|
||||
|
||||
var streamErr;
|
||||
txStream.on('end', function() {
|
||||
|
||||
if (streamErr) {
|
||||
return callback(streamErr);
|
||||
}
|
||||
|
||||
callback(null, results);
|
||||
|
||||
});
|
||||
|
||||
// pipe txids into tx stream for processing
|
||||
txidStream.pipe(txStream);
|
||||
|
||||
txStream._transform = function(chunk, enc, callback) {
|
||||
|
||||
var key = self._encoding.decodeAddressIndexKey(chunk);
|
||||
|
||||
self._tx.getTransaction(key.txid, options, function(err, tx) {
|
||||
|
||||
if(err) {
|
||||
log.error(err);
|
||||
txStream.emit('error', err);
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
log.error('Could not find tx for txid: ' + key.txid + '. This should not be possible, check indexes.');
|
||||
txStream.emit('error', err);
|
||||
return callback();
|
||||
}
|
||||
|
||||
results.push(tx);
|
||||
|
||||
callback();
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
txStream.on('error', function(err) {
|
||||
log.error(err);
|
||||
txStream.unpipe();
|
||||
});
|
||||
|
||||
txStream._flush = function(callback) {
|
||||
txStream.emit('end');
|
||||
callback();
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
AddressService.prototype.onReorg = function(args, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var oldBlockList = args[1];
|
||||
|
||||
var removalOps = [];
|
||||
// for every tx, remove the address index key for every input and output
|
||||
// TODO: DRY self up!
|
||||
for(var i = 0; i < oldBlockList.length; i++) {
|
||||
|
||||
var block = oldBlockList[i];
|
||||
//txs
|
||||
for(var j = 0; j < block.txs.length; j++) {
|
||||
|
||||
var tx = block.txs[j];
|
||||
|
||||
//inputs
|
||||
var address;
|
||||
|
||||
for(var k = 0; k < tx.inputs.length; k++) {
|
||||
|
||||
var input = tx.inputs[k];
|
||||
address = input.getAddress();
|
||||
|
||||
if (!address) {
|
||||
continue;
|
||||
}
|
||||
|
||||
address.network = self._network;
|
||||
address = address.toString();
|
||||
|
||||
removalOps.push({
|
||||
type: 'del',
|
||||
key: self._encoding.encodeAddressIndexKey(address, block.height, tx.txid(), k, 1, block.ts)
|
||||
});
|
||||
}
|
||||
|
||||
//outputs
|
||||
for(k = 0; k < tx.outputs.length; k++) {
|
||||
|
||||
var output = tx.outputs[k];
|
||||
address = output.getAddress();
|
||||
|
||||
if (!address) {
|
||||
continue;
|
||||
}
|
||||
|
||||
address.network = self._network;
|
||||
address = address.toString();
|
||||
|
||||
removalOps.push({
|
||||
type: 'del',
|
||||
key: self._encoding.encodeAddressIndexKey(address, block.height, tx.txid(), k, 0, block.ts)
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, removalOps);
|
||||
};
|
||||
|
||||
AddressService.prototype.onBlock = function(block, callback) {
|
||||
var self = this;
|
||||
|
||||
if (self.node.stopping) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var operations = [];
|
||||
|
||||
for(var i = 0; i < block.txs.length; i++) {
|
||||
var tx = block.txs[i];
|
||||
var ops = self._processTransaction(tx, { block: block });
|
||||
operations.push(ops);
|
||||
}
|
||||
|
||||
operations = _.flatten(operations);
|
||||
callback(null, operations);
|
||||
};
|
||||
|
||||
AddressService.prototype._processInput = function(tx, input, opts) {
|
||||
|
||||
var address = input.getAddress();
|
||||
|
||||
if(!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
address.network = this._network;
|
||||
address = address.toString();
|
||||
var txid = tx.txid();
|
||||
var timestamp = this._timestamp.getTimestampSync(opts.block.rhash());
|
||||
assert(timestamp, 'Must have a timestamp in order to process input.');
|
||||
|
||||
// address index
|
||||
var addressKey = this._encoding.encodeAddressIndexKey(address, opts.block.height, txid, opts.inputIndex, 1, timestamp);
|
||||
|
||||
var operations = [{
|
||||
type: 'put',
|
||||
key: addressKey
|
||||
}];
|
||||
|
||||
// prev utxo
|
||||
var rec = {
|
||||
type: 'del',
|
||||
key: this._encoding.encodeUtxoIndexKey(address, input.prevout.txid(), input.prevout.index)
|
||||
};
|
||||
|
||||
// In the event where we are reorg'ing,
|
||||
// this is where we are putting a utxo back in, we don't know what the original height, sats, or scriptBuffer
|
||||
// since this only happens on reorg and the utxo that was spent in the chain we are reorg'ing away from will likely
|
||||
// be spent again sometime soon, we will not add the value back in, just the key
|
||||
|
||||
operations.push(rec);
|
||||
|
||||
return operations;
|
||||
};
|
||||
|
||||
AddressService.prototype._processOutput = function(tx, output, index, opts) {
|
||||
var address = output.getAddress();
|
||||
|
||||
if(!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
address.network = this._network;
|
||||
address = address.toString();
|
||||
var txid = tx.txid();
|
||||
var timestamp = this._timestamp.getTimestampSync(opts.block.rhash());
|
||||
assert(timestamp, 'Must have a timestamp in order to process output.');
|
||||
|
||||
var addressKey = this._encoding.encodeAddressIndexKey(address, opts.block.height, txid, opts.outputIndex, 0, timestamp);
|
||||
|
||||
var utxoKey = this._encoding.encodeUtxoIndexKey(address, txid, index);
|
||||
var utxoValue = this._encoding.encodeUtxoIndexValue(
|
||||
opts.block.height,
|
||||
output.value,
|
||||
timestamp,
|
||||
output.script.toRaw()
|
||||
);
|
||||
|
||||
var operations = [{
|
||||
type: 'put',
|
||||
key: addressKey
|
||||
}];
|
||||
|
||||
operations.push({
|
||||
type: 'put',
|
||||
key: utxoKey,
|
||||
value: utxoValue
|
||||
});
|
||||
|
||||
return operations;
|
||||
|
||||
};
|
||||
|
||||
AddressService.prototype._processTransaction = function(tx, opts) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var _opts = { block: opts.block };
|
||||
|
||||
var outputOperations = tx.outputs.map(function(output, index) {
|
||||
return self._processOutput(tx, output, index, _opts);
|
||||
});
|
||||
|
||||
outputOperations = _.flatten(_.compact(outputOperations));
|
||||
|
||||
var inputOperations = tx.inputs.map(function(input, index) {
|
||||
_opts.inputIndex = index;
|
||||
return self._processInput(tx, input, _opts);
|
||||
});
|
||||
|
||||
inputOperations = _.flatten(_.compact(inputOperations));
|
||||
|
||||
outputOperations.concat(inputOperations);
|
||||
return outputOperations;
|
||||
|
||||
};
|
||||
|
||||
module.exports = AddressService;
|
||||
File diff suppressed because it is too large
Load Diff
375
lib/services/block/block_handler.js
Normal file
375
lib/services/block/block_handler.js
Normal file
@ -0,0 +1,375 @@
|
||||
'use strict';
|
||||
var Readable = require('stream').Readable;
|
||||
var Writable = require('stream').Writable;
|
||||
var Transform = require('stream').Transform;
|
||||
var inherits = require('util').inherits;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var async = require('async');
|
||||
var index = require('../../index');
|
||||
var log = index.log;
|
||||
var _ = require('lodash');
|
||||
|
||||
function BlockStream(highWaterMark, sync) {
|
||||
Readable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||
this.sync = sync;
|
||||
this.block = this.sync.block;
|
||||
this.dbTip = this.block.tip;
|
||||
this.lastReadHeight = this._getTipHeight();
|
||||
this.lastEmittedHash = this.dbTip.hash;
|
||||
this.queue = [];
|
||||
this.processing = false;
|
||||
var self = this;
|
||||
self.block.on('reorg', function() {
|
||||
self.push(null);
|
||||
});
|
||||
}
|
||||
|
||||
inherits(BlockStream, Readable);
|
||||
|
||||
function ProcessConcurrent(highWaterMark, sync) {
|
||||
Transform.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||
this.block = sync.block;
|
||||
this.db = sync.db;
|
||||
this.operations = [];
|
||||
this.lastBlock = 0;
|
||||
this.blockCount = 0;
|
||||
}
|
||||
|
||||
inherits(ProcessConcurrent, Transform);
|
||||
|
||||
function ProcessSerial(highWaterMark, sync) {
|
||||
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||
this.block = sync.block;
|
||||
this.db = sync.db;
|
||||
this.block = sync.block;
|
||||
this.tip = sync.block.tip;
|
||||
this.processBlockStartTime = [];
|
||||
this._lastReportedTime = Date.now();
|
||||
}
|
||||
|
||||
inherits(ProcessSerial, Writable);
|
||||
|
||||
function ProcessBoth(highWaterMark, sync) {
|
||||
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||
this.db = sync.db;
|
||||
}
|
||||
|
||||
inherits(ProcessBoth, Writable);
|
||||
|
||||
function WriteStream(highWaterMark, sync) {
|
||||
Writable.call(this, {objectMode: true, highWaterMark: highWaterMark});
|
||||
this.db = sync.db;
|
||||
this.writeTime = 0;
|
||||
this.lastConcurrentOutputHeight = 0;
|
||||
this.block = sync.block;
|
||||
}
|
||||
|
||||
inherits(WriteStream, Writable);
|
||||
|
||||
function BlockHandler(node, block) {
|
||||
this.node = node;
|
||||
this.db = this.node.services.db;
|
||||
this.block = block;
|
||||
this.syncing = false;
|
||||
this.paused = false;
|
||||
this.blockQueue = [];
|
||||
this.highWaterMark = 10;
|
||||
}
|
||||
|
||||
inherits(BlockHandler, EventEmitter);
|
||||
|
||||
BlockHandler.prototype.sync = function(block) {
|
||||
var self = this;
|
||||
|
||||
if (block) {
|
||||
self.blockQueue.push(block);
|
||||
}
|
||||
|
||||
if(this.syncing || this.paused) {
|
||||
log.debug('Sync lock held, not able to sync at the moment');
|
||||
return;
|
||||
}
|
||||
|
||||
self.syncing = true;
|
||||
|
||||
self._setupStreams();
|
||||
|
||||
};
|
||||
|
||||
BlockHandler.prototype._setupStreams = function() {
|
||||
var self = this;
|
||||
var blockStream = new BlockStream(self.highWaterMark, self);
|
||||
var processConcurrent = new ProcessConcurrent(self.highWaterMark, self);
|
||||
var writeStream = new WriteStream(self.highWaterMark, self);
|
||||
var processSerial = new ProcessSerial(self.highWaterMark, self);
|
||||
|
||||
self._handleErrors(blockStream);
|
||||
self._handleErrors(processConcurrent);
|
||||
self._handleErrors(processSerial);
|
||||
self._handleErrors(writeStream);
|
||||
|
||||
blockStream
|
||||
.pipe(processConcurrent)
|
||||
.pipe(writeStream);
|
||||
blockStream
|
||||
.pipe(processSerial);
|
||||
|
||||
processSerial.on('finish', self._onFinish.bind(self));
|
||||
|
||||
};
|
||||
|
||||
BlockHandler.prototype._onFinish = function() {
|
||||
|
||||
var self = this;
|
||||
self.syncing = false;
|
||||
|
||||
self.emit('synced');
|
||||
|
||||
};
|
||||
|
||||
BlockHandler.prototype._handleErrors = function(stream) {
|
||||
var self = this;
|
||||
|
||||
stream.on('error', function(err) {
|
||||
self.syncing = false;
|
||||
self.emit('error', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
BlockStream.prototype._read = function() {
|
||||
|
||||
if (this.lastEmittedHash === this.block.getNetworkTipHash()) {
|
||||
return this.push(null);
|
||||
}
|
||||
|
||||
if (this.sync.blockQueue.length === 0) {
|
||||
this.queue.push(++this.lastReadHeight);
|
||||
} else {
|
||||
var block = this.sync.blockQueue.shift();
|
||||
if (block) {
|
||||
this.lastReadHeight = block.__height;
|
||||
this.queue.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
this._process();
|
||||
};
|
||||
|
||||
BlockStream.prototype._process = function() {
|
||||
var self = this;
|
||||
|
||||
if(self.processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
|
||||
async.whilst(
|
||||
function() {
|
||||
return self.queue.length;
|
||||
}, function(next) {
|
||||
|
||||
var blockArgs = self.queue.slice(0, Math.min(5, self.queue.length));
|
||||
self.queue = self.queue.slice(blockArgs.length);
|
||||
|
||||
if (_.isNumber(blockArgs[0])) {
|
||||
self.block.getBlocks(blockArgs, function(err, blocks) {
|
||||
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self._pushBlocks(blocks);
|
||||
next();
|
||||
|
||||
});
|
||||
} else {
|
||||
|
||||
self._pushBlocks(blockArgs);
|
||||
next();
|
||||
|
||||
}
|
||||
|
||||
}, function(err) {
|
||||
|
||||
if(err) {
|
||||
return self.emit('error', err);
|
||||
}
|
||||
self.processing = false;
|
||||
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
BlockStream.prototype._pushBlocks = function(blocks) {
|
||||
|
||||
var self = this;
|
||||
|
||||
for(var i = 0; i < blocks.length; i++) {
|
||||
|
||||
self.lastEmittedHash = blocks[i].hash;
|
||||
self.push(blocks[i]);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
BlockStream.prototype._getTipHeight = function() {
|
||||
if (this.dbTip.__height === 0) {
|
||||
return -1;
|
||||
}
|
||||
return this.dbTip.__height;
|
||||
};
|
||||
|
||||
ProcessSerial.prototype._reportStatus = function() {
|
||||
if ((Date.now() - this._lastReportedTime) > 1000) {
|
||||
this._lastReportedTime = Date.now();
|
||||
log.info('Sync: current height is: ' + this.block.tip.__height);
|
||||
}
|
||||
};
|
||||
|
||||
ProcessSerial.prototype._write = function(block, enc, callback) {
|
||||
var self = this;
|
||||
|
||||
function check() {
|
||||
return self.block.concurrentTip.__height >= block.__height;
|
||||
}
|
||||
|
||||
if(check()) {
|
||||
return self._process(block, callback);
|
||||
}
|
||||
|
||||
self.block.once('concurrentaddblock', function() {
|
||||
if(!check()) {
|
||||
var err = new Error('Concurrent block ' + self.block.concurrentTip.__height + ' is less than ' + block.__height);
|
||||
return self.emit('error', err);
|
||||
}
|
||||
self._process(block, callback);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
ProcessSerial.prototype._process = function(block, callback) {
|
||||
var self = this;
|
||||
|
||||
self.block.getBlockOperations(block, true, 'serial', function(err, operations) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
operations.push(self.block.getTipOperation(block, true));
|
||||
|
||||
var obj = {
|
||||
tip: block,
|
||||
operations: operations
|
||||
};
|
||||
|
||||
self.tip = block;
|
||||
|
||||
self.db.batch(obj.operations, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.block.tip = block;
|
||||
self._reportStatus();
|
||||
self.block.emit('addblock');
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ProcessConcurrent.prototype._transform = function(block, enc, callback) {
|
||||
var self = this;
|
||||
|
||||
this.lastBlock = block;
|
||||
|
||||
self.block.getBlockOperations(block, true, 'concurrent', function(err, operations) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.blockCount++;
|
||||
self.operations = self.operations.concat(operations);
|
||||
|
||||
if(self.blockCount >= 1) {
|
||||
self.operations.push(self.block.getTipOperation(block, true, 'concurrentTip'));
|
||||
var obj = {
|
||||
concurrentTip: block,
|
||||
operations: self.operations
|
||||
};
|
||||
self.operations = [];
|
||||
self.blockCount = 0;
|
||||
|
||||
return callback(null, obj);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
ProcessConcurrent.prototype._flush = function(callback) {
|
||||
if(this.operations.length) {
|
||||
this.operations.push(this.block.getTipOperation(this.lastBlock, true));
|
||||
this.operations = [];
|
||||
return callback(null, this.operations);
|
||||
}
|
||||
};
|
||||
|
||||
WriteStream.prototype._write = function(obj, enc, callback) {
|
||||
var self = this;
|
||||
|
||||
if (self.db.node.stopping) {
|
||||
return setImmediate(callback);
|
||||
}
|
||||
|
||||
self.db.batch(obj.operations, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.block.concurrentTip = obj.concurrentTip;
|
||||
self.block.emit('concurrentaddblock');
|
||||
self.lastConcurrentOutputHeight = self.block.concurrentTip.__height;
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
ProcessBoth.prototype._write = function(block, encoding, callback) {
|
||||
var self = this;
|
||||
|
||||
async.parallel([function(next) {
|
||||
self.block.getBlockOperations(block, true, 'concurrent', function(err, operations) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
operations.push(self.block.getTipOperation(block, true, 'concurrentTip'));
|
||||
next(null, operations);
|
||||
});
|
||||
}, function(next) {
|
||||
self.block.getBlockOperations(block, true, 'serial', function(err, operations) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
operations.push(self.block.getTipOperation(block, true));
|
||||
next(null, operations);
|
||||
});
|
||||
}], function(err, results) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
var operations = results[0].concat(results[1]);
|
||||
self.db.batch(operations, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
self.block.tip = block;
|
||||
self.block.concurrentTip = block;
|
||||
callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = BlockHandler;
|
||||
28
lib/services/block/encoding.js
Normal file
28
lib/services/block/encoding.js
Normal file
@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
var Block = require('bcoin').block;
|
||||
// stores -- block header as key, block itself as value (optionally)
|
||||
|
||||
function Encoding(servicePrefix) {
|
||||
this._servicePrefix = servicePrefix;
|
||||
}
|
||||
|
||||
|
||||
// ---- hash --> rawblock
|
||||
Encoding.prototype.encodeBlockKey = function(hash) {
|
||||
return Buffer.concat([ this._servicePrefix, new Buffer(hash, 'hex') ]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeBlockKey = function(buffer) {
|
||||
return buffer.slice(2).toString('hex');
|
||||
};
|
||||
|
||||
Encoding.prototype.encodeBlockValue = function(block) {
|
||||
return block.toRaw();
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeBlockValue = function(buffer) {
|
||||
return Block.fromRaw(buffer);
|
||||
};
|
||||
|
||||
module.exports = Encoding;
|
||||
631
lib/services/block/index.js
Normal file
631
lib/services/block/index.js
Normal file
@ -0,0 +1,631 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var BaseService = require('../../service');
|
||||
var inherits = require('util').inherits;
|
||||
var Encoding = require('./encoding');
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
var utils = require('../../utils');
|
||||
var assert = require('assert');
|
||||
var constants = require('../../constants');
|
||||
var bcoin = require('bcoin');
|
||||
|
||||
var BlockService = function(options) {
|
||||
|
||||
BaseService.call(this, options);
|
||||
|
||||
this._tip = null;
|
||||
this._db = this.node.services.db;
|
||||
this._p2p = this.node.services.p2p;
|
||||
this._header = this.node.services.header;
|
||||
this._timestamp = this.node.services.timestamp;
|
||||
|
||||
this._subscriptions = {};
|
||||
this._subscriptions.block = [];
|
||||
this._subscriptions.reorg = [];
|
||||
|
||||
this._blockCount = 0;
|
||||
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.network];
|
||||
this._initialSync = true;
|
||||
};
|
||||
|
||||
inherits(BlockService, BaseService);
|
||||
|
||||
BlockService.dependencies = [ 'timestamp', 'p2p', 'db', 'header' ];
|
||||
|
||||
// --- public prototype functions
|
||||
BlockService.prototype.getAPIMethods = function() {
|
||||
var methods = [
|
||||
['getBlock', this, this.getBlock, 1],
|
||||
['getRawBlock', this, this.getRawBlock, 1],
|
||||
['getBlockOverview', this, this.getBlockOverview, 1],
|
||||
['getBestBlockHash', this, this.getBestBlockHash, 0],
|
||||
['syncPercentage', this, this.syncPercentage, 0],
|
||||
['isSynced', this, this.isSynced, 0]
|
||||
];
|
||||
return methods;
|
||||
};
|
||||
|
||||
BlockService.prototype.isSynced = function(callback) {
|
||||
callback(null, !this._initialSync);
|
||||
};
|
||||
|
||||
BlockService.prototype.getBestBlockHash = function(callback) {
|
||||
var hash = this._header.getLastHeader().hash;
|
||||
callback(null, hash);
|
||||
};
|
||||
|
||||
BlockService.prototype.getTip = function() {
|
||||
return this._tip;
|
||||
};
|
||||
|
||||
BlockService.prototype.getBlock = function(arg, callback) {
|
||||
|
||||
var self = this;
|
||||
self._getHash(arg, function(err, hash) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!hash) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
self._getBlock(hash, callback);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype.getBlockOverview = function(hash, callback) {
|
||||
|
||||
this._getBlock(hash, function(err, block) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var header = block.toHeaders().toJSON();
|
||||
|
||||
var blockOverview = {
|
||||
hash: block.rhash(),
|
||||
version: block.version,
|
||||
confirmations: null,
|
||||
height: header.height,
|
||||
chainWork: header.chainwork,
|
||||
prevHash: header.prevBlock,
|
||||
nextHash: null,
|
||||
merkleRoot: block.merkleroot,
|
||||
time: block.ts,
|
||||
medianTime: null,
|
||||
nonce: block.nonce,
|
||||
bits: block.bits,
|
||||
difficulty: null,
|
||||
txids: null
|
||||
};
|
||||
|
||||
callback(null, blockOverview);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype.getPublishEvents = function() {
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'block/block',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'block'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'block')
|
||||
}
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype.getRawBlock = function(hash, callback) {
|
||||
this.getBlock(hash, function(err, block) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, block.toRaw().toString('hex'));
|
||||
});
|
||||
};
|
||||
|
||||
BlockService.prototype.start = function(callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
async.waterfall([
|
||||
function(next) {
|
||||
self._db.getPrefix(self.name, next);
|
||||
},
|
||||
function(prefix, next) {
|
||||
self._prefix = prefix;
|
||||
self._encoding = new Encoding(self._prefix);
|
||||
self._db.getServiceTip('block', next);
|
||||
}
|
||||
], function(err, tip) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
assert(tip.height >= 0, 'tip is not initialized');
|
||||
self._setTip(tip);
|
||||
self._setListeners();
|
||||
self._startSubscriptions();
|
||||
callback();
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype.stop = function(callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
BlockService.prototype.subscribe = function(name, emitter) {
|
||||
|
||||
this._subscriptions[name].push(emitter);
|
||||
log.info(emitter.remoteAddress, 'subscribe:', 'block/' + name, 'total:', this._subscriptions[name].length);
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype._syncPercentage = function() {
|
||||
var height = this._header.getLastHeader().height;
|
||||
var ratio = this._tip.height/height;
|
||||
return (ratio*100).toFixed(2);
|
||||
};
|
||||
|
||||
BlockService.prototype.syncPercentage = function(callback) {
|
||||
callback(null, this._syncPercentage());
|
||||
};
|
||||
|
||||
BlockService.prototype.unsubscribe = function(name, emitter) {
|
||||
|
||||
var index = this._subscriptions[name].indexOf(emitter);
|
||||
|
||||
if (index > -1) {
|
||||
this._subscriptions[name].splice(index, 1);
|
||||
}
|
||||
|
||||
log.info(emitter.remoteAddress, 'unsubscribe:', 'block/' + name, 'total:', this._subscriptions[name].length);
|
||||
|
||||
};
|
||||
|
||||
// --- start private prototype functions
|
||||
|
||||
BlockService.prototype._broadcast = function(subscribers, name, entity) {
|
||||
for (var i = 0; i < subscribers.length; i++) {
|
||||
subscribers[i].emit(name, entity);
|
||||
}
|
||||
};
|
||||
|
||||
BlockService.prototype._detectReorg = function(block) {
|
||||
var prevHash = bcoin.util.revHex(block.prevBlock);
|
||||
if (this._tip.hash !== prevHash) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
BlockService.prototype._findCommonAncestor = function(hash, allHeaders, callback) {
|
||||
|
||||
var self = this;
|
||||
var count = 0;
|
||||
var _oldTip = this._tip.hash;
|
||||
var _newTip = hash;
|
||||
var oldBlocks = [];
|
||||
|
||||
assert(_oldTip, 'We don\'t have a tip hash to reorg away from!');
|
||||
|
||||
async.whilst(
|
||||
// test case
|
||||
function() {
|
||||
|
||||
return _oldTip !== _newTip && ++count < allHeaders.size;
|
||||
|
||||
},
|
||||
// get block
|
||||
function(next) {
|
||||
|
||||
// old tip (our current tip) has to be in database
|
||||
self._db.get(self._encoding.encodeBlockKey(_oldTip), function(err, data) {
|
||||
|
||||
if (err || !data) {
|
||||
return next(err || new Error('missing block'));
|
||||
}
|
||||
|
||||
// once we've found the old tip, we will find its prev and check to see if matches new tip's prev
|
||||
var block = self._encoding.decodeBlockValue(data);
|
||||
// apply the block's height
|
||||
var blockHdr = allHeaders.get(block.rhash());
|
||||
if (!blockHdr) {
|
||||
return next(new Error('Could not find block in list of headers: ' + block.rhash()));
|
||||
}
|
||||
block.height = blockHdr.height;
|
||||
assert(block.height >= 0, 'We mamaged to save a header with an incorrect height.');
|
||||
|
||||
// apply the block's timestamp
|
||||
self._timestamp.getTimestamp(block.rhash(), function(err, timestamp) {
|
||||
|
||||
if (err || !timestamp) {
|
||||
return next(err || new Error('missing timestamp'));
|
||||
}
|
||||
|
||||
block.ts = timestamp;
|
||||
// we will squirrel away the block because our services will need to remove it after we've found the common ancestor
|
||||
oldBlocks.push(block);
|
||||
|
||||
// this is our current tip's prev hash
|
||||
_oldTip = bcoin.util.revHex(block.prevBlock);
|
||||
|
||||
// our current headers have the correct state of the chain, so consult that for its prev aash
|
||||
var header = allHeaders.get(_newTip);
|
||||
|
||||
if (!header) {
|
||||
return next(new Error('Header missing from list of headers'));
|
||||
}
|
||||
|
||||
// set new tip to the prev hash
|
||||
_newTip = header.prevHash;
|
||||
|
||||
next();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}, function(err) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var commonAncestorHash = _newTip;
|
||||
callback(null, commonAncestorHash, oldBlocks);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
BlockService.prototype._getBlock = function(hash, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
this._db.get(this._encoding.encodeBlockKey(hash), function(err, data) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var block = self._encoding.decodeBlockValue(data);
|
||||
callback(null, block);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
BlockService.prototype._getHash = function(blockArg, callback) {
|
||||
|
||||
if (utils.isHeight(blockArg)) {
|
||||
|
||||
this._header.getHeaderByHeight(blockArg, function(err, header) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!header) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
callback(null, header.hash);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return callback(null, blockArg);
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype._handleReorg = function(hash, allHeaders, block) {
|
||||
|
||||
// hash is the hash of the new block that we are reorging to.
|
||||
assert(hash, 'We were asked to reorg to a non-existent hash.');
|
||||
var self = this;
|
||||
|
||||
self._reorging = true;
|
||||
|
||||
log.warn('Block Service: Chain reorganization detected! Our current block tip is: ' +
|
||||
self._tip.hash + ' the current block: ' + hash + '.');
|
||||
|
||||
self._findCommonAncestor(hash, allHeaders, function(err, commonAncestorHash, oldBlocks) {
|
||||
|
||||
if (err) {
|
||||
|
||||
log.error('Block Service: A common ancestor block between hash: ' +
|
||||
self._tip.hash + ' (our current tip) and: ' + hash +
|
||||
' (the forked block) could not be found. Bitcore-node must exit.');
|
||||
|
||||
self.node.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
var commonAncestorHeader = allHeaders.get(commonAncestorHash);
|
||||
|
||||
log.info('Block Service: A common ancestor block was found to at hash: ' + commonAncestorHeader.hash);
|
||||
|
||||
self._processReorg(commonAncestorHeader, oldBlocks, block);
|
||||
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
|
||||
// this JUST rewinds the chain back to the common ancestor block, nothing more
|
||||
BlockService.prototype._onReorg = function(commonAncestorHeader, oldBlockList, newBlock) {
|
||||
|
||||
// set the tip to the common ancestor in case something goes wrong with the reorg
|
||||
var self = this;
|
||||
self._setTip({ hash: commonAncestorHeader.hash, height: commonAncestorHeader.height });
|
||||
var tipOps = utils.encodeTip(self._tip, self.name);
|
||||
|
||||
var removalOps = [{
|
||||
type: 'put',
|
||||
key: tipOps.key,
|
||||
value: tipOps.value
|
||||
}];
|
||||
|
||||
// remove all the old blocks that we reorg from
|
||||
oldBlockList.forEach(function(block) {
|
||||
removalOps.push({
|
||||
type: 'del',
|
||||
key: self._encoding.encodeBlockKey(block.rhash()),
|
||||
});
|
||||
});
|
||||
|
||||
self._db.batch(removalOps, function() {
|
||||
|
||||
self._reorging = false;
|
||||
|
||||
if (newBlock) {
|
||||
self._onBlock(newBlock);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype._onAllHeaders = function() {
|
||||
this._startSync();
|
||||
};
|
||||
|
||||
|
||||
BlockService.prototype._processReorg = function(commonAncestorHeader, oldBlocks, newBlock) {
|
||||
|
||||
var self = this;
|
||||
var operations = [];
|
||||
var services = self.node.services;
|
||||
|
||||
async.eachSeries(
|
||||
services,
|
||||
function(mod, next) {
|
||||
if(mod.onReorg) {
|
||||
mod.onReorg.call(mod, [commonAncestorHeader, oldBlocks], function(err, ops) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (ops) {
|
||||
operations = operations.concat(ops);
|
||||
}
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
},
|
||||
|
||||
function(err) {
|
||||
|
||||
if (err) {
|
||||
if (!self.node.stopping) {
|
||||
log.error('Block Service: Error: ' + err);
|
||||
self.node.stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self._db.batch(operations, function(err) {
|
||||
|
||||
if (err && !self.node.stopping) {
|
||||
log.error('Block Service: Error: ' + err);
|
||||
self.node.stop();
|
||||
}
|
||||
|
||||
self._onReorg(commonAncestorHeader, oldBlocks, newBlock);
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
BlockService.prototype._processBlock = function(block) {
|
||||
|
||||
var self = this;
|
||||
var operations = [];
|
||||
var services = self.node.services;
|
||||
|
||||
async.eachSeries(
|
||||
services,
|
||||
function(mod, next) {
|
||||
if(mod.onBlock) {
|
||||
mod.onBlock.call(mod, block, function(err, ops) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (ops) {
|
||||
operations = operations.concat(ops);
|
||||
}
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
},
|
||||
|
||||
function(err) {
|
||||
|
||||
if (err) {
|
||||
if (!self.node.stopping) {
|
||||
log.error('Block Service: Error: ' + err);
|
||||
self.node.stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self._db.batch(operations, function(err) {
|
||||
|
||||
if (err) {
|
||||
if (!self.node.stopping) {
|
||||
log.error('Block Service: Error: ' + err);
|
||||
self.node.stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self._tip.height = self._tip.height + 1;
|
||||
self._tip.hash = block.rhash();
|
||||
var tipOps = utils.encodeTip(self._tip, self.name);
|
||||
|
||||
self._db.put(tipOps.key, tipOps.value, function(err) {
|
||||
|
||||
if (err) {
|
||||
if (!self.node.stopping) {
|
||||
log.error('Block Service: Error: ' + err);
|
||||
self.node.stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self._sync();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
BlockService.prototype.onBlock = function(block, callback) {
|
||||
var self = this;
|
||||
|
||||
setImmediate(function() {
|
||||
callback(null, [{
|
||||
type: 'put',
|
||||
key: self._encoding.encodeBlockKey(block.rhash()),
|
||||
value: self._encoding.encodeBlockValue(block)
|
||||
}]);
|
||||
});
|
||||
};
|
||||
|
||||
BlockService.prototype._onBlock = function(block) {
|
||||
|
||||
if (this.node.stopping || this._reorging) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this service must receive blocks in order
|
||||
var prevHash = bcoin.util.revHex(block.prevBlock);
|
||||
if (this._tip.hash !== prevHash) {
|
||||
return;
|
||||
}
|
||||
log.debug('Block Service: new block: ' + block.rhash());
|
||||
block.height = this._tip.height + 1;
|
||||
this._processBlock(block);
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype._setListeners = function() {
|
||||
|
||||
var self = this;
|
||||
self._header.once('headers', self._onAllHeaders.bind(self));
|
||||
self._header.on('reorg', function(hash, headers, block) {
|
||||
if (!self._reorging && !this._initialSync) {
|
||||
log.debug('Block Service: detected a reorg from the header service.');
|
||||
self._handleReorg(hash, headers, block);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
BlockService.prototype._setTip = function(tip) {
|
||||
log.debug('Block Service: Setting tip to height: ' + tip.height);
|
||||
log.debug('Block Service: Setting tip to hash: ' + tip.hash);
|
||||
this._tip = tip;
|
||||
};
|
||||
|
||||
BlockService.prototype._startSync = function() {
|
||||
|
||||
this._numNeeded = this._header.getLastHeader().height - this._tip.height;
|
||||
|
||||
log.info('Block Service: Gathering: ' + this._numNeeded + ' block(s) from the peer-to-peer network.');
|
||||
|
||||
this._sync();
|
||||
};
|
||||
|
||||
BlockService.prototype._startSubscriptions = function() {
|
||||
|
||||
if (this._subscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscribed = true;
|
||||
if (!this._bus) {
|
||||
this._bus = this.node.openBus({remoteAddress: 'localhost-block'});
|
||||
}
|
||||
|
||||
this._bus.on('header/block', this._onBlock.bind(this));
|
||||
this._bus.subscribe('header/block');
|
||||
};
|
||||
|
||||
BlockService.prototype._sync = function() {
|
||||
|
||||
var self = this;
|
||||
|
||||
|
||||
if (self.node.stopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lastHeaderIndex = self._header.getLastHeader().height;
|
||||
|
||||
if (self._tip.height < lastHeaderIndex) {
|
||||
|
||||
if (self._tip.height % 144 === 0) {
|
||||
log.info('Block Service: Blocks download progress: ' +
|
||||
self._tip.height + '/' + lastHeaderIndex +
|
||||
' (' + self._syncPercentage() + '%)');
|
||||
}
|
||||
|
||||
return self._header.getNextHash(self._tip, function(err, hash) {
|
||||
|
||||
if(err) {
|
||||
log.error(err);
|
||||
self.node.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
self._p2p.getBlocks({ startHash: self._tip.hash, endHash: hash });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
this._header.blockServiceSyncing = false;
|
||||
this._initialSync = false;
|
||||
log.info('Block Service: The best block hash is: ' + self._tip.hash +
|
||||
' at height: ' + self._tip.height);
|
||||
|
||||
};
|
||||
|
||||
module.exports = BlockService;
|
||||
347
lib/services/block/reorg.js
Normal file
347
lib/services/block/reorg.js
Normal file
@ -0,0 +1,347 @@
|
||||
'use strict';
|
||||
var bitcore = require('bitcore-lib');
|
||||
var BufferUtil = bitcore.util.buffer;
|
||||
var async = require('async');
|
||||
|
||||
function Reorg(node, block) {
|
||||
this.node = node;
|
||||
this.block = block;
|
||||
this.db = block.db;
|
||||
}
|
||||
|
||||
Reorg.prototype.handleReorg = function(newBlockHash, callback) {
|
||||
var self = this;
|
||||
|
||||
self.handleConcurrentReorg(function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.findCommonAncestorAndNewHashes(self.block.tip.hash, newBlockHash, function(err, commonAncestor, newHashes) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.rewindBothTips(commonAncestor, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
self.fastForwardBothTips(newHashes, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reorg.prototype.handleConcurrentReorg = function(callback) {
|
||||
var self = this;
|
||||
|
||||
if(self.block.concurrentTip.hash === self.block.tip.hash) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
self.findCommonAncestorAndNewHashes(
|
||||
self.block.concurrentTip.hash,
|
||||
self.block.tip.hash,
|
||||
function(err, commonAncestor, newHashes) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.rewindConcurrentTip(commonAncestor, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.fastForwardConcurrentTip(newHashes, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reorg.prototype.rewindConcurrentTip = function(commonAncestor, callback) {
|
||||
var self = this;
|
||||
|
||||
async.whilst(
|
||||
function() {
|
||||
return self.block.concurrentTip.hash !== commonAncestor;
|
||||
},
|
||||
function(next) {
|
||||
self.block.getBlockOperations(self.block.concurrentTip, false, 'concurrent', function(err, operations) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
operations.push(self.block.getTipOperation(self.block.concurrentTip, false, 'concurrentTip'));
|
||||
self.db.batch(operations, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var prevHash = BufferUtil.reverse(self.block.concurrentTip.header.prevHash).toString('hex');
|
||||
|
||||
self.block.getBlocks([prevHash], function(err, blocks) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.block.concurrentTip = blocks[0];
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
Reorg.prototype.fastForwardConcurrentTip = function(newHashes, callback) {
|
||||
var self = this;
|
||||
|
||||
async.eachSeries(newHashes, function(hash, next) {
|
||||
self.block.getBlocks([hash], function(err, blocks) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.block.getBlockOperations(blocks[0], true, 'concurrent', function(err, operations) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
operations.push(self.block.getTipOperation(blocks[0], true, 'concurrentTip'));
|
||||
self.db.batch(operations, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.block.concurrentTip = blocks[0];
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Reorg.prototype.rewindBothTips = function(commonAncestor, callback) {
|
||||
var self = this;
|
||||
|
||||
async.whilst(
|
||||
function() {
|
||||
return self.block.tip.hash !== commonAncestor;
|
||||
},
|
||||
function(next) {
|
||||
async.parallel(
|
||||
[
|
||||
function(next) {
|
||||
self.block.getBlockOperations(self.block.concurrentTip, false, 'concurrent', function(err, operations) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
operations.push(self.block.getTipOperation(self.block.concurrentTip, false, 'concurrentTip'));
|
||||
next(null, operations);
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
self.block.getBlockOperations(self.block.tip, false, 'serial', function(err, operations) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
operations.push(self.block.getTipOperation(self.block.tip, false));
|
||||
next(null, operations);
|
||||
});
|
||||
}
|
||||
],
|
||||
function(err, results) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var operations = results[0].concat(results[1]);
|
||||
self.db.batch(operations, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var prevHash = BufferUtil.reverse(self.block.tip.header.prevHash).toString('hex');
|
||||
|
||||
self.block.getBlocks([prevHash], function(err, blocks) {
|
||||
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.block.concurrentTip = blocks[0];
|
||||
self.block.tip = blocks[0];
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
Reorg.prototype.fastForwardBothTips = function(newHashes, callback) {
|
||||
var self = this;
|
||||
|
||||
async.eachSeries(newHashes, function(hash, next) {
|
||||
self.block.getBlocks([hash], function(err, blocks) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
async.parallel(
|
||||
[
|
||||
function(next) {
|
||||
self.block.getBlockOperations(blocks[0], true, 'concurrent', function(err, operations) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
operations.push(self.block.getTipOperation(blocks[0], true, 'concurrentTip'));
|
||||
next(null, operations);
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
self.block.getBlockOperations(blocks[0], true, 'serial', function(err, operations) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
operations.push(self.block.getTipOperation(blocks[0], true));
|
||||
next(null, operations);
|
||||
});
|
||||
}
|
||||
],
|
||||
function(err, results) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var operations = results[0].concat(results[1]);
|
||||
|
||||
self.db.batch(operations, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.block.concurrentTip = blocks[0];
|
||||
self.block.tip = blocks[0];
|
||||
next();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Reorg.prototype.findCommonAncestorAndNewHashes = function(oldTipHash, newTipHash, callback) {
|
||||
var self = this;
|
||||
|
||||
var mainPosition = oldTipHash;
|
||||
var forkPosition = newTipHash;
|
||||
|
||||
var mainHashesMap = {};
|
||||
var forkHashesMap = {};
|
||||
|
||||
mainHashesMap[mainPosition] = true;
|
||||
forkHashesMap[forkPosition] = true;
|
||||
|
||||
var commonAncestor = null;
|
||||
var newHashes = [forkPosition];
|
||||
|
||||
async.whilst(
|
||||
function() {
|
||||
return !commonAncestor;
|
||||
},
|
||||
function(next) {
|
||||
async.parallel(
|
||||
[
|
||||
function(next) {
|
||||
if(!mainPosition) {
|
||||
return next();
|
||||
}
|
||||
|
||||
self.block.getBlockHeader(mainPosition, function(err, mainBlockHeader) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if(mainBlockHeader && mainBlockHeader.prevHash) {
|
||||
mainHashesMap[mainBlockHeader.prevHash] = true;
|
||||
mainPosition = mainBlockHeader.prevHash;
|
||||
} else {
|
||||
mainPosition = null;
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
if(!forkPosition) {
|
||||
return next();
|
||||
}
|
||||
|
||||
self.block.getBlockHeader(forkPosition, function(err, forkBlockHeader) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if(forkBlockHeader && forkBlockHeader.prevHash) {
|
||||
forkHashesMap[forkBlockHeader.prevHash] = true;
|
||||
forkPosition = forkBlockHeader.prevHash;
|
||||
newHashes.unshift(forkPosition);
|
||||
} else {
|
||||
forkPosition = null;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
],
|
||||
function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if(forkPosition && mainHashesMap[forkPosition]) {
|
||||
commonAncestor = forkPosition;
|
||||
}
|
||||
|
||||
if(mainPosition && forkHashesMap[mainPosition]) {
|
||||
commonAncestor = mainPosition;
|
||||
}
|
||||
|
||||
if(!mainPosition && !forkPosition) {
|
||||
return next(new Error('Unknown common ancestor'));
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
);
|
||||
},
|
||||
function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// New hashes are those that are > common ancestor
|
||||
var commonAncestorFound = false;
|
||||
for(var i = newHashes.length - 1; i >= 0; i--) {
|
||||
if(newHashes[i] === commonAncestor) {
|
||||
commonAncestorFound = true;
|
||||
}
|
||||
|
||||
if(commonAncestorFound) {
|
||||
newHashes.shift();
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, commonAncestor, newHashes);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = Reorg;
|
||||
0
lib/services/db/encoding.js
Normal file
0
lib/services/db/encoding.js
Normal file
315
lib/services/db/index.js
Normal file
315
lib/services/db/index.js
Normal file
@ -0,0 +1,315 @@
|
||||
'use strict';
|
||||
|
||||
var util = require('util');
|
||||
var fs = require('fs');
|
||||
var async = require('async');
|
||||
var levelup = require('levelup');
|
||||
var leveldown = require('leveldown');
|
||||
var mkdirp = require('mkdirp');
|
||||
var Service = require('../../service');
|
||||
var constants = require('../../constants');
|
||||
var log = require('../../index').log;
|
||||
var assert = require('assert');
|
||||
|
||||
function DB(options) {
|
||||
|
||||
if (!(this instanceof DB)) {
|
||||
return new DB(options);
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
Service.call(this, options);
|
||||
|
||||
this._dbPrefix = constants.DB_PREFIX;
|
||||
|
||||
this.version = 1;
|
||||
|
||||
this.network = this.node.network;
|
||||
|
||||
this._setDataPath();
|
||||
|
||||
this.levelupStore = leveldown;
|
||||
if (options.store) {
|
||||
this.levelupStore = options.store;
|
||||
}
|
||||
|
||||
this.subscriptions = {};
|
||||
|
||||
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.network];
|
||||
|
||||
this.node.on('stopping', function() {
|
||||
log.warn('Node is stopping, gently closing the database. Please wait, this could take a while.');
|
||||
});
|
||||
}
|
||||
|
||||
util.inherits(DB, Service);
|
||||
|
||||
DB.dependencies = [];
|
||||
|
||||
DB.prototype._onError = function(err) {
|
||||
if (!this._stopping) {
|
||||
log.error('Db Service: error: ' + err);
|
||||
this.node.stop();
|
||||
}
|
||||
};
|
||||
|
||||
DB.prototype._setDataPath = function() {
|
||||
assert(fs.existsSync(this.node.datadir), 'Node is expected to have a "datadir" property');
|
||||
if (this.node.network === 'livenet' || this.node.network === 'mainnet') {
|
||||
this.dataPath = this.node.datadir + '/bitcorenode.db';
|
||||
} else if (this.node.network === 'regtest') {
|
||||
this.dataPath = this.node.datadir + '/regtest/bitcorenode.db';
|
||||
} else if (this.node.network === 'testnet') {
|
||||
this.dataPath = this.node.datadir + '/testnet/bitcorenode.db';
|
||||
} else {
|
||||
throw new Error('Unknown network: ' + this.network);
|
||||
}
|
||||
};
|
||||
|
||||
DB.prototype._setVersion = function(callback) {
|
||||
var versionBuffer = new Buffer(new Array(4));
|
||||
versionBuffer.writeUInt32BE(this.version);
|
||||
this.put(Buffer.concat([ this._dbPrefix, new Buffer('version', 'utf8') ]), versionBuffer, callback);
|
||||
};
|
||||
|
||||
DB.prototype.start = function(callback) {
|
||||
|
||||
if (!fs.existsSync(this.dataPath)) {
|
||||
mkdirp.sync(this.dataPath);
|
||||
}
|
||||
|
||||
this._store = levelup(this.dataPath, { db: this.levelupStore, keyEncoding: 'binary', valueEncoding: 'binary'});
|
||||
|
||||
setImmediate(callback);
|
||||
|
||||
};
|
||||
|
||||
DB.prototype.get = function(key, options, callback) {
|
||||
|
||||
var cb = callback;
|
||||
var opts = options;
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
cb = options;
|
||||
opts = {};
|
||||
}
|
||||
|
||||
if (!this._stopping) {
|
||||
|
||||
this._store.get(key, opts, function(err, data) {
|
||||
|
||||
if(err && err instanceof levelup.errors.NotFoundError) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
cb(null, data);
|
||||
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
cb(new Error('Shutdown sequence underway, not able to complete the query'));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
DB.prototype.put = function(key, value, callback) {
|
||||
|
||||
assert(Buffer.isBuffer(key), 'key NOT a buffer as expected.');
|
||||
|
||||
if (value) {
|
||||
|
||||
assert(Buffer.isBuffer(value), 'value exists but NOT a buffer as expected.');
|
||||
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
if (self._stopping) {
|
||||
callback();
|
||||
}
|
||||
|
||||
self._store.put(key, value, callback);
|
||||
};
|
||||
|
||||
DB.prototype.batch = function(ops, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
if (self._stopping) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
for(var i = 0; i < ops.length; i++) {
|
||||
|
||||
assert(Buffer.isBuffer(ops[i].key), 'key NOT a buffer as expected.');
|
||||
|
||||
if (ops[i].value) {
|
||||
|
||||
assert(Buffer.isBuffer(ops[i].value), 'value exists but NOT a buffer as expected.');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
self._store.batch(ops, callback);
|
||||
|
||||
};
|
||||
|
||||
DB.prototype.createReadStream = function(op) {
|
||||
if (this._stopping) {
|
||||
return;
|
||||
}
|
||||
var stream = this._store.createReadStream(op);
|
||||
stream.on('error', this._onError.bind(this));
|
||||
return stream;
|
||||
};
|
||||
|
||||
DB.prototype.createKeyStream = function(op) {
|
||||
if (this._stopping) {
|
||||
return;
|
||||
}
|
||||
var stream = this._store.createKeyStream(op);
|
||||
stream.on('error', this._onError.bind(this));
|
||||
return stream;
|
||||
};
|
||||
|
||||
DB.prototype.stop = function(callback) {
|
||||
this._stopping = true;
|
||||
this.close(callback);
|
||||
};
|
||||
|
||||
DB.prototype.close = function(callback) {
|
||||
if (this._store && this._store.isOpen()) {
|
||||
this._store.close(callback);
|
||||
return;
|
||||
}
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
DB.prototype.getAPIMethods = function() {
|
||||
return [];
|
||||
};
|
||||
|
||||
|
||||
DB.prototype.getPublishEvents = function() {
|
||||
return [];
|
||||
};
|
||||
|
||||
DB.prototype.getServiceTip = function(serviceName, callback) {
|
||||
|
||||
var keyBuf = Buffer.concat([ this._dbPrefix, new Buffer('tip-' + serviceName, 'utf8') ]);
|
||||
|
||||
var self = this;
|
||||
self.get(keyBuf, function(err, tipBuf) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var tip;
|
||||
if (tipBuf) {
|
||||
|
||||
tip = {
|
||||
height: tipBuf.readUInt32BE(0,4),
|
||||
hash: tipBuf.slice(4).toString('hex')
|
||||
};
|
||||
|
||||
} else {
|
||||
|
||||
tip = {
|
||||
height: 0,
|
||||
hash: self.GENESIS_HASH
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
callback(null, tip);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
DB.prototype.getPrefix = function(service, callback) {
|
||||
var self = this;
|
||||
|
||||
var keyBuf = Buffer.concat([ self._dbPrefix, new Buffer('prefix-', 'utf8'), new Buffer(service, 'utf8') ]);
|
||||
var unusedBuf = Buffer.concat([ self._dbPrefix, new Buffer('nextUnused', 'utf8') ]);
|
||||
|
||||
function getPrefix(next) {
|
||||
|
||||
self.get(keyBuf, function(err, buf) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!buf) {
|
||||
return next();
|
||||
}
|
||||
callback(null, buf);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getUnused(next) {
|
||||
|
||||
self.get(unusedBuf, function(err, buffer) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(!buffer) {
|
||||
return next(null, new Buffer('0001', 'hex'));
|
||||
}
|
||||
|
||||
next(null, buffer);
|
||||
});
|
||||
}
|
||||
|
||||
function putPrefix(buffer, next) {
|
||||
|
||||
self.put(keyBuf, buffer, function(err) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
next(null, buffer);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function putUnused(buffer, next) {
|
||||
|
||||
var prefix = buffer.readUInt16BE();
|
||||
var nextUnused = new Buffer(2);
|
||||
nextUnused.writeUInt16BE(prefix + 1);
|
||||
|
||||
self.put(unusedBuf, nextUnused, function(err) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
next(null, buffer);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
getPrefix,
|
||||
getUnused,
|
||||
putPrefix,
|
||||
putUnused
|
||||
],
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = DB;
|
||||
60
lib/services/fee/index.js
Normal file
60
lib/services/fee/index.js
Normal file
@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
var BaseService = require('../../service');
|
||||
var inherits = require('util').inherits;
|
||||
var BitcoreRPC = require('bitcoind-rpc');
|
||||
|
||||
var FeeService = function(options) {
|
||||
this._config = options.rpc || {
|
||||
user: 'bitcoin',
|
||||
pass: 'local321',
|
||||
host: 'localhost',
|
||||
protocol: 'http',
|
||||
port: 8332
|
||||
};
|
||||
BaseService.call(this, options);
|
||||
this._client = new BitcoreRPC(this._config);
|
||||
};
|
||||
|
||||
inherits(FeeService, BaseService);
|
||||
|
||||
FeeService.dependencies = [];
|
||||
|
||||
FeeService.prototype.start = function() {
|
||||
return this.node.network.port - 1;
|
||||
};
|
||||
|
||||
FeeService.prototype.start = function(callback) {
|
||||
callback();
|
||||
};
|
||||
|
||||
FeeService.prototype.stop = function(callback) {
|
||||
callback();
|
||||
};
|
||||
|
||||
FeeService.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['estimateFee', this, this.estimateFee, 1]
|
||||
];
|
||||
};
|
||||
|
||||
FeeService.prototype.estimateFee = function(blocks, callback) {
|
||||
|
||||
this._client.estimateFee(blocks || 4, function(err, res) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
callback();
|
||||
}
|
||||
|
||||
callback(null, res.result);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports = FeeService;
|
||||
|
||||
73
lib/services/header/encoding.js
Normal file
73
lib/services/header/encoding.js
Normal file
@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
function Encoding(servicePrefix) {
|
||||
this._servicePrefix = servicePrefix;
|
||||
this._hashPrefix = new Buffer('00', 'hex');
|
||||
this._heightPrefix = new Buffer('01', 'hex');
|
||||
}
|
||||
|
||||
// ---- hash --> header
|
||||
Encoding.prototype.encodeHeaderHashKey = function(hash) {
|
||||
var hashBuf = new Buffer(hash, 'hex');
|
||||
return Buffer.concat([ this._servicePrefix, this._hashPrefix, hashBuf ]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeHeaderHashKey = function(buffer) {
|
||||
return buffer.slice(3).toString('hex');
|
||||
};
|
||||
|
||||
// ---- height --> header
|
||||
Encoding.prototype.encodeHeaderHeightKey = function(height) {
|
||||
var heightBuf = new Buffer(4);
|
||||
heightBuf.writeUInt32BE(height);
|
||||
return Buffer.concat([ this._servicePrefix, this._heightPrefix, heightBuf ]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeHeaderHeightKey = function(buffer) {
|
||||
return buffer.readUInt32BE(3);
|
||||
};
|
||||
|
||||
Encoding.prototype.encodeHeaderValue = function(header) {
|
||||
var hashBuf = new Buffer(header.hash, 'hex');
|
||||
var versionBuf = new Buffer(4);
|
||||
versionBuf.writeInt32BE(header.version);
|
||||
var prevHash = new Buffer(header.prevHash, 'hex');
|
||||
var merkleRoot = new Buffer(header.merkleRoot, 'hex');
|
||||
var tsBuf = new Buffer(4);
|
||||
tsBuf.writeUInt32BE(header.timestamp || header.time);
|
||||
var bitsBuf = new Buffer(4);
|
||||
bitsBuf.writeUInt32BE(header.bits);
|
||||
var nonceBuf = new Buffer(4);
|
||||
nonceBuf.writeUInt32BE(header.nonce);
|
||||
var heightBuf = new Buffer(4);
|
||||
heightBuf.writeUInt32BE(header.height);
|
||||
var chainworkBuf = new Buffer(header.chainwork, 'hex');
|
||||
return Buffer.concat([hashBuf, versionBuf, prevHash, merkleRoot, tsBuf, bitsBuf, nonceBuf, heightBuf, chainworkBuf ]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeHeaderValue = function(buffer) {
|
||||
var hash = buffer.slice(0, 32).toString('hex');
|
||||
var version = buffer.readInt32BE(32);
|
||||
var prevHash = buffer.slice(36, 68).toString('hex');
|
||||
var merkleRoot = buffer.slice(68, 100).toString('hex');
|
||||
var ts = buffer.readUInt32BE(100);
|
||||
var bits = buffer.readUInt32BE(104);
|
||||
var nonce = buffer.readUInt32BE(108);
|
||||
var height = buffer.readUInt32BE(112);
|
||||
var chainwork = buffer.slice(116).toString('hex');
|
||||
return {
|
||||
hash: hash,
|
||||
version: version,
|
||||
prevHash: prevHash,
|
||||
merkleRoot: merkleRoot,
|
||||
timestamp: ts,
|
||||
bits: bits,
|
||||
nonce: nonce,
|
||||
height: height,
|
||||
chainwork: chainwork
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Encoding;
|
||||
|
||||
718
lib/services/header/index.js
Normal file
718
lib/services/header/index.js
Normal file
@ -0,0 +1,718 @@
|
||||
'use strict';
|
||||
|
||||
var BaseService = require('../../service');
|
||||
var inherits = require('util').inherits;
|
||||
var Encoding = require('./encoding');
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
var utils = require('../../utils');
|
||||
var async = require('async');
|
||||
var BN = require('bn.js');
|
||||
var consensus = require('bcoin').consensus;
|
||||
var assert = require('assert');
|
||||
var constants = require('../../constants');
|
||||
var bcoin = require('bcoin');
|
||||
|
||||
var HeaderService = function(options) {
|
||||
|
||||
BaseService.call(this, options);
|
||||
|
||||
this._tip = null;
|
||||
this._p2p = this.node.services.p2p;
|
||||
this._db = this.node.services.db;
|
||||
this._hashes = [];
|
||||
|
||||
this.subscriptions = {};
|
||||
this.subscriptions.block = [];
|
||||
this._checkpoint = options.checkpoint || 2000; // set to -1 to resync all headers.
|
||||
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.network];
|
||||
this._lastHeader = null;
|
||||
this.blockServiceSyncing = true;
|
||||
|
||||
};
|
||||
|
||||
inherits(HeaderService, BaseService);
|
||||
|
||||
HeaderService.dependencies = [ 'p2p', 'db' ];
|
||||
|
||||
HeaderService.MAX_CHAINWORK = new BN(1).ushln(256);
|
||||
HeaderService.STARTING_CHAINWORK = '0000000000000000000000000000000000000000000000000000000100010001';
|
||||
|
||||
// --- public prototype functions
|
||||
HeaderService.prototype.subscribe = function(name, emitter) {
|
||||
this.subscriptions[name].push(emitter);
|
||||
log.info(emitter.remoteAddress, 'subscribe:', 'header/' + name, 'total:', this.subscriptions[name].length);
|
||||
};
|
||||
|
||||
HeaderService.prototype.unsubscribe = function(name, emitter) {
|
||||
|
||||
var index = this.subscriptions[name].indexOf(emitter);
|
||||
|
||||
if (index > -1) {
|
||||
this.subscriptions[name].splice(index, 1);
|
||||
}
|
||||
|
||||
log.info(emitter.remoteAddress, 'unsubscribe:', 'header/' + name, 'total:', this.subscriptions[name].length);
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype.getAPIMethods = function() {
|
||||
|
||||
var methods = [
|
||||
['getAllHeaders', this, this.getAllHeaders, 0],
|
||||
['getBestHeight', this, this.getBestHeight, 0],
|
||||
['getInfo', this, this.getInfo, 0],
|
||||
['getBlockHeader', this, this.getBlockHeader, 1]
|
||||
];
|
||||
|
||||
return methods;
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype.getCurrentDifficulty = function() {
|
||||
var target = bcoin.mining.common.getTarget(this._lastHeader.bits);
|
||||
return bcoin.mining.common.getDifficulty(target);
|
||||
};
|
||||
|
||||
HeaderService.prototype.getInfo = function(callback) {
|
||||
callback(null, {
|
||||
blocks: this._lastHeader.height,
|
||||
connections: this._p2p.getNumberOfPeers(),
|
||||
timeoffset: 0,
|
||||
proxy: '',
|
||||
testnet: this.node.network === 'livenet' ? false: true,
|
||||
errors: '',
|
||||
network: this.node.network,
|
||||
relayFee: 0,
|
||||
version: 'bitcore-1.1.2',
|
||||
protocolversion: 700001,
|
||||
difficulty: this.getCurrentDifficulty()
|
||||
});
|
||||
};
|
||||
|
||||
HeaderService.prototype.getAllHeaders = function(callback) {
|
||||
|
||||
var self = this;
|
||||
var start = self._encoding.encodeHeaderHeightKey(0);
|
||||
var end = self._encoding.encodeHeaderHeightKey(self._tip.height + 1);
|
||||
var allHeaders = new utils.SimpleMap();
|
||||
|
||||
var criteria = {
|
||||
gte: start,
|
||||
lt: end
|
||||
};
|
||||
|
||||
var stream = self._db.createReadStream(criteria);
|
||||
|
||||
var streamErr;
|
||||
|
||||
stream.on('error', function(error) {
|
||||
streamErr = error;
|
||||
});
|
||||
|
||||
stream.on('data', function(data) {
|
||||
var header = self._encoding.decodeHeaderValue(data.value);
|
||||
allHeaders.set(header.hash, header, header.height);
|
||||
});
|
||||
|
||||
stream.on('end', function() {
|
||||
|
||||
if (streamErr) {
|
||||
return streamErr;
|
||||
}
|
||||
|
||||
callback(null, allHeaders);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
HeaderService.prototype.getBlockHeader = function(arg, callback) {
|
||||
|
||||
if (utils.isHeight(arg)) {
|
||||
return this._getHeader(arg, null, callback);
|
||||
}
|
||||
|
||||
return this._getHeader(null, arg, callback);
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype.getBestHeight = function() {
|
||||
return this._tip.height;
|
||||
};
|
||||
|
||||
HeaderService.prototype.start = function(callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
async.waterfall([
|
||||
function(next) {
|
||||
self._db.getPrefix(self.name, next);
|
||||
},
|
||||
function(prefix, next) {
|
||||
self._encoding = new Encoding(prefix);
|
||||
self._db.getServiceTip(self.name, next);
|
||||
},
|
||||
function(tip, next) {
|
||||
|
||||
self._tip = tip;
|
||||
log.debug('Header Service: original tip height is: ' + self._tip.height);
|
||||
log.debug('Header Service: original tip hash is: ' + self._tip.hash);
|
||||
|
||||
self._originalTip = { height: self._tip.height, hash: self._tip.hash };
|
||||
|
||||
if (self._tip.height === 0) {
|
||||
|
||||
assert(self._tip.hash === self.GENESIS_HASH, 'Expected tip hash to be genesis hash, but it was not.');
|
||||
|
||||
var genesisHeader = {
|
||||
hash: self.GENESIS_HASH,
|
||||
height: 0,
|
||||
chainwork: HeaderService.STARTING_CHAINWORK,
|
||||
version: 1,
|
||||
prevHash: new Array(65).join('0'),
|
||||
timestamp: 1231006505,
|
||||
nonce: 2083236893,
|
||||
bits: 0x1d00ffff,
|
||||
merkleRoot: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b'
|
||||
};
|
||||
|
||||
self._lastHeader = genesisHeader;
|
||||
|
||||
var dbOps = [
|
||||
{
|
||||
type: 'put',
|
||||
key: self._encoding.encodeHeaderHeightKey(0),
|
||||
value: self._encoding.encodeHeaderValue(genesisHeader)
|
||||
},
|
||||
{
|
||||
type: 'put',
|
||||
key: self._encoding.encodeHeaderHashKey(self.GENESIS_HASH),
|
||||
value: self._encoding.encodeHeaderValue(genesisHeader)
|
||||
}
|
||||
];
|
||||
|
||||
return self._db.batch(dbOps, next);
|
||||
|
||||
}
|
||||
self._getLastHeader(next);
|
||||
},
|
||||
], function(err) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self._setListeners();
|
||||
self._bus = self.node.openBus({remoteAddress: 'localhost-header'});
|
||||
self._startHeaderSubscription();
|
||||
callback();
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype.stop = function(callback) {
|
||||
callback();
|
||||
};
|
||||
|
||||
HeaderService.prototype._startHeaderSubscription = function() {
|
||||
|
||||
this._bus.on('p2p/headers', this._onHeaders.bind(this));
|
||||
this._bus.subscribe('p2p/headers');
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype.getPublishEvents = function() {
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'header/block',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'block'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'block')
|
||||
}
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._onBlock = function(block) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var hash = block.rhash();
|
||||
var prevHash = bcoin.util.revHex(block.prevBlock);
|
||||
var newBlock = prevHash === self._lastHeader.hash;
|
||||
|
||||
var header = block.toHeaders().toJSON();
|
||||
header.timestamp = header.ts;
|
||||
header.prevHash = header.prevBlock;
|
||||
|
||||
if (newBlock) {
|
||||
|
||||
log.debug('Header Service: new block: ' + hash);
|
||||
self._saveHeaders(self._onHeader(header));
|
||||
|
||||
}
|
||||
|
||||
// this is the rare case that a block comes to us out of order or is a reorg'ed block
|
||||
// in almost all cases, this will be a reorg
|
||||
if (!newBlock && !self.blockServiceSyncing) {
|
||||
|
||||
return self._detectReorg(block, function(err, reorg) {
|
||||
|
||||
if (err) {
|
||||
log.error(err);
|
||||
self.node.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reorg) {
|
||||
return self._handleReorg(block, header, function(err) {
|
||||
|
||||
if (err) {
|
||||
log.error(err);
|
||||
self.node.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
self._saveHeaders(self._onHeader(header));
|
||||
|
||||
}); // this sets the last header
|
||||
}
|
||||
|
||||
self._broadcast(block);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
setImmediate(function() {
|
||||
self._broadcast(block);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._broadcast = function(block) {
|
||||
for (var i = 0; i < this.subscriptions.block.length; i++) {
|
||||
this.subscriptions.block[i].emit('header/block', block);
|
||||
}
|
||||
};
|
||||
|
||||
HeaderService.prototype._onHeader = function(header) {
|
||||
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
|
||||
header.height = this._lastHeader.height + 1;
|
||||
header.chainwork = this._getChainwork(header, this._lastHeader).toString(16, 64);
|
||||
if (!header.timestamp) {
|
||||
header.timestamp = header.time;
|
||||
}
|
||||
this._lastHeader = header;
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'put',
|
||||
key: this._encoding.encodeHeaderHashKey(header.hash),
|
||||
value: this._encoding.encodeHeaderValue(header)
|
||||
},
|
||||
{
|
||||
type: 'put',
|
||||
key: this._encoding.encodeHeaderHeightKey(header.height),
|
||||
value: this._encoding.encodeHeaderValue(header)
|
||||
}
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._onHeaders = function(headers) {
|
||||
|
||||
log.debug('Header Service: Received: ' + headers.length + ' header(s).');
|
||||
|
||||
var dbOps = [];
|
||||
|
||||
for(var i = 0; i < headers.length; i++) {
|
||||
|
||||
var header = headers[i];
|
||||
|
||||
header = header.toObject();
|
||||
|
||||
var ops = this._onHeader(header);
|
||||
|
||||
dbOps = dbOps.concat(ops);
|
||||
|
||||
this._tip.height = header.height;
|
||||
this._tip.hash = header.hash;
|
||||
}
|
||||
|
||||
this._saveHeaders(dbOps);
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._saveHeaders = function(dbOps) {
|
||||
|
||||
var tipOps = utils.encodeTip(this._tip, this.name);
|
||||
|
||||
dbOps.push({
|
||||
type: 'put',
|
||||
key: tipOps.key,
|
||||
value: tipOps.value
|
||||
});
|
||||
|
||||
this._db.batch(dbOps, this._onHeadersSave.bind(this));
|
||||
};
|
||||
|
||||
HeaderService.prototype._onHeadersSave = function(err) {
|
||||
|
||||
var self = this;
|
||||
|
||||
if (err) {
|
||||
log.error(err);
|
||||
self.node.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self._syncComplete()) {
|
||||
|
||||
self._sync();
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
self._startBlockSubscription();
|
||||
|
||||
self._setBestHeader();
|
||||
|
||||
self._detectStartupReorg(function(err, reorg) {
|
||||
|
||||
if (err) {
|
||||
log.error(err);
|
||||
self.node.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reorg) {
|
||||
return self._handleReorg(null, null, function(err) {
|
||||
if (err) {
|
||||
log.error(err);
|
||||
this.node.stop();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log.debug('Header Service: emitting headers to block service.');
|
||||
|
||||
self.emit('headers');
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._startBlockSubscription = function() {
|
||||
|
||||
if (this._subscribedBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscribedBlock = true;
|
||||
|
||||
this._bus.on('p2p/block', this._onBlock.bind(this));
|
||||
this._bus.subscribe('p2p/block');
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._syncComplete = function() {
|
||||
|
||||
return this._tip.height >= this._bestHeight;
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._setBestHeader = function() {
|
||||
|
||||
var bestHeader = this._lastHeader;
|
||||
this._tip.height = bestHeader.height;
|
||||
this._tip.hash = bestHeader.hash;
|
||||
|
||||
log.debug('Header Service: ' + bestHeader.hash + ' is the best block hash.');
|
||||
};
|
||||
|
||||
HeaderService.prototype._getHeader = function(height, hash, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
/*jshint -W018 */
|
||||
if (!hash && !(height >= 0)) {
|
||||
/*jshint +W018 */
|
||||
return callback(new Error('invalid arguments'));
|
||||
}
|
||||
|
||||
|
||||
var key;
|
||||
if (hash) {
|
||||
key = self._encoding.encodeHeaderHashKey(hash);
|
||||
} else {
|
||||
key = self._encoding.encodeHeaderHeightKey(height);
|
||||
}
|
||||
|
||||
self._db.get(key, function(err, data) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
callback(null, self._encoding.decodeHeaderValue(data));
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._detectReorg = function(block, callback) {
|
||||
|
||||
assert(block, 'Block is needed to detect reorg.');
|
||||
|
||||
var key = this._encoding.encodeHeaderHashKey(bcoin.util.revHex(block.prevBlock));
|
||||
|
||||
this._db.get(key, function(err, val) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// is this block's prevHash already referenced in the database? If so, reorg
|
||||
if (val) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
callback(null, false);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._detectStartupReorg = function(callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
self._getHeader(self._originalTip.height, null, function(err, header) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!header) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
if (header.hash !== self._originalTip.hash) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
callback(null, false);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._handleReorg = function(block, header, callback) {
|
||||
|
||||
var self = this;
|
||||
self.getAllHeaders(function(err, headers) {
|
||||
|
||||
if (err || !headers) {
|
||||
return callback(err || new Error('Missing headers'));
|
||||
}
|
||||
|
||||
var hash = headers.getIndex(self._originalTip.height).hash;
|
||||
|
||||
if (block && header) {
|
||||
hash = block.rhash();
|
||||
self._lastHeader = headers.get(header.prevHash);
|
||||
assert(self._lastHeader, 'Expected our reorg block to have a header entry, but it did not.');
|
||||
headers.set(hash, header); // appends to the end
|
||||
self.emit('reorg', hash, headers, block);
|
||||
}
|
||||
|
||||
assert(hash, 'To reorg, we need a hash to reorg to.');
|
||||
self.emit('reorg', hash, headers);
|
||||
callback();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._setListeners = function() {
|
||||
|
||||
this._p2p.once('bestHeight', this._onBestHeight.bind(this));
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._onBestHeight = function(height) {
|
||||
assert(height >= this._tip.height, 'Our peer does not seem to be fully synced: best height: ' +
|
||||
height + ' tip height: ' + this._tip.height);
|
||||
log.debug('Header Service: Best Height is: ' + height);
|
||||
this._bestHeight = height;
|
||||
this._startSync();
|
||||
};
|
||||
|
||||
HeaderService.prototype._startSync = function() {
|
||||
|
||||
this._numNeeded = this._bestHeight - this._tip.height;
|
||||
|
||||
log.info('Header Service: Gathering: ' + this._numNeeded + ' ' + 'header(s) from the peer-to-peer network.');
|
||||
|
||||
this._sync();
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._sync = function() {
|
||||
|
||||
log.info('Header Service: download progress: ' + this._tip.height + '/' +
|
||||
this._bestHeight + ' (' + (this._tip.height / this._bestHeight*100.00).toFixed(2) + '%)');
|
||||
|
||||
this._p2p.getHeaders({ startHash: this._tip.hash });
|
||||
|
||||
};
|
||||
|
||||
// this gets the header that is +2 places from hash or returns 0 if there is no such
|
||||
HeaderService.prototype.getNextHash = function(tip, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// if the tip being passed in is the second to last block, then return 0 because there isn't a block
|
||||
// after the last block
|
||||
if (tip.height + 1 === self._tip.height) {
|
||||
return callback(null, 0);
|
||||
}
|
||||
|
||||
var start = self._encoding.encodeHeaderHeightKey(tip.height + 2);
|
||||
var end = self._encoding.encodeHeaderHeightKey(tip.height + 3);
|
||||
var result = 0;
|
||||
|
||||
var criteria = {
|
||||
gte: start,
|
||||
lt: end
|
||||
};
|
||||
|
||||
var stream = self._db.createReadStream(criteria);
|
||||
|
||||
var streamErr;
|
||||
|
||||
stream.on('error', function(error) {
|
||||
streamErr = error;
|
||||
});
|
||||
|
||||
stream.on('data', function(data) {
|
||||
result = self._encoding.decodeHeaderValue(data.value).hash;
|
||||
});
|
||||
|
||||
stream.on('end', function() {
|
||||
|
||||
if (streamErr) {
|
||||
return streamErr;
|
||||
}
|
||||
|
||||
callback(null, result);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype.getLastHeader = function() {
|
||||
assert(this._lastHeader, 'Last headers should be populated.');
|
||||
return this._lastHeader;
|
||||
};
|
||||
|
||||
HeaderService.prototype._getLastHeader = function(callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// redo all headers
|
||||
if (this._checkpoint === -1) {
|
||||
this._checkpoint = this._tip.height;
|
||||
}
|
||||
|
||||
if (self._tip.height >= self._checkpoint) {
|
||||
self._tip.height -= self._checkpoint;
|
||||
}
|
||||
|
||||
var removalOps = [];
|
||||
|
||||
var start = self._encoding.encodeHeaderHeightKey(self._tip.height);
|
||||
var end = self._encoding.encodeHeaderHeightKey(0xffffffff);
|
||||
|
||||
log.info('Getting last header synced at height: ' + self._tip.height);
|
||||
|
||||
var criteria = {
|
||||
gte: start,
|
||||
lte: end
|
||||
};
|
||||
|
||||
var stream = self._db.createReadStream(criteria);
|
||||
|
||||
var streamErr;
|
||||
stream.on('error', function(error) {
|
||||
streamErr = error;
|
||||
});
|
||||
|
||||
stream.on('data', function(data) {
|
||||
var header = self._encoding.decodeHeaderValue(data.value);
|
||||
|
||||
// any records with a height greater than our current tip height can be scheduled for removal
|
||||
// because they will be replaced shortly
|
||||
if (header.height > self._tip.height) {
|
||||
removalOps.push({
|
||||
type: 'del',
|
||||
key: data.key
|
||||
});
|
||||
return;
|
||||
} else if (header.height === self._tip.height) {
|
||||
self._lastHeader = header;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
stream.on('end', function() {
|
||||
|
||||
if (streamErr) {
|
||||
return streamErr;
|
||||
}
|
||||
|
||||
assert(self._lastHeader, 'The last synced header was not in the database.');
|
||||
self._tip.hash = self._lastHeader.hash;
|
||||
self._db.batch(removalOps, callback);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
HeaderService.prototype._getChainwork = function(header, prevHeader) {
|
||||
|
||||
var prevChainwork = new BN(new Buffer(prevHeader.chainwork, 'hex'));
|
||||
|
||||
return this._computeChainwork(header.bits, prevChainwork);
|
||||
};
|
||||
|
||||
HeaderService.prototype._computeChainwork = function(bits, prev) {
|
||||
|
||||
var target = consensus.fromCompact(bits);
|
||||
|
||||
if (target.isNeg() || target.cmpn(0) === 0) {
|
||||
return new BN(0);
|
||||
}
|
||||
|
||||
var proof = HeaderService.MAX_CHAINWORK.div(target.iaddn(1));
|
||||
|
||||
if (!prev) {
|
||||
return proof;
|
||||
}
|
||||
|
||||
return proof.iadd(prev);
|
||||
|
||||
};
|
||||
|
||||
module.exports = HeaderService;
|
||||
|
||||
29
lib/services/mempool/encoding.js
Normal file
29
lib/services/mempool/encoding.js
Normal file
@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
var tx = require('bcoin').tx;
|
||||
|
||||
function Encoding(servicePrefix) {
|
||||
this.servicePrefix = servicePrefix;
|
||||
}
|
||||
|
||||
Encoding.prototype.encodeMempoolTransactionKey = function(txid) {
|
||||
var buffers = [this.servicePrefix];
|
||||
var txidBuffer = new Buffer(txid, 'hex');
|
||||
buffers.push(txidBuffer);
|
||||
return Buffer.concat(buffers);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeMempoolTransactionKey = function(buffer) {
|
||||
return buffer.slice(2).toString('hex');
|
||||
};
|
||||
|
||||
Encoding.prototype.encodeMempoolTransactionValue = function(transaction) {
|
||||
return transaction.toRaw();
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeMempoolTransactionValue = function(buffer) {
|
||||
return tx.fromRaw(buffer);
|
||||
};
|
||||
|
||||
module.exports = Encoding;
|
||||
|
||||
158
lib/services/mempool/index.js
Normal file
158
lib/services/mempool/index.js
Normal file
@ -0,0 +1,158 @@
|
||||
'use strict';
|
||||
var BaseService = require('../../service');
|
||||
var util = require('util');
|
||||
var utils = require('../../utils');
|
||||
var Encoding = require('./encoding');
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
|
||||
var MempoolService = function(options) {
|
||||
BaseService.call(this, options);
|
||||
this._subscriptions = {};
|
||||
this._subscriptions.transaction = [];
|
||||
this._db = this.node.services.db;
|
||||
};
|
||||
|
||||
util.inherits(MempoolService, BaseService);
|
||||
|
||||
MempoolService.dependencies = ['db', 'block'];
|
||||
|
||||
MempoolService.prototype.getAPIMethods = function() {
|
||||
var methods = [
|
||||
['getMempoolTransaction', this, this.getMempoolTransaction, 1]
|
||||
];
|
||||
return methods;
|
||||
};
|
||||
|
||||
MempoolService.prototype.getPublishEvents = function() {
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'mempool/transaction',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'transaction'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'transaction')
|
||||
}
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
MempoolService.prototype.subscribe = function(name, emitter) {
|
||||
|
||||
this._subscriptions[name].push(emitter);
|
||||
log.info(emitter.remoteAddress, 'subscribe:', 'mempool/' + name, 'total:', this._subscriptions[name].length);
|
||||
|
||||
};
|
||||
|
||||
MempoolService.prototype.unsubscribe = function(name, emitter) {
|
||||
|
||||
var index = this._subscriptions[name].indexOf(emitter);
|
||||
|
||||
if (index > -1) {
|
||||
this._subscriptions[name].splice(index, 1);
|
||||
}
|
||||
|
||||
log.info(emitter.remoteAddress, 'unsubscribe:', 'mempool/' + name, 'total:', this._subscriptions[name].length);
|
||||
|
||||
};
|
||||
|
||||
MempoolService.prototype.start = function(callback) {
|
||||
var self = this;
|
||||
|
||||
self._db.getPrefix(self.name, function(err, prefix) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
self._encoding = new Encoding(prefix);
|
||||
self._startSubscriptions();
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
MempoolService.prototype.onReorg = function(args, callback) {
|
||||
|
||||
var oldBlockList = args[1];
|
||||
|
||||
var removalOps = [];
|
||||
|
||||
for(var i = 0; i < oldBlockList.length; i++) {
|
||||
|
||||
var block = oldBlockList[i];
|
||||
|
||||
for(var j = 0; j < block.txs.length; j++) {
|
||||
|
||||
var tx = block.txs[j];
|
||||
var key = this._encoding.encodeMempoolTransactionKey(tx.txid());
|
||||
var value = this._encoding.encodeMempoolTransactionValue(tx);
|
||||
|
||||
removalOps.push({
|
||||
type: 'put',
|
||||
key: key,
|
||||
value: value
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, removalOps);
|
||||
};
|
||||
|
||||
MempoolService.prototype._startSubscriptions = function() {
|
||||
|
||||
if (this._subscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscribed = true;
|
||||
if (!this._bus) {
|
||||
this._bus = this.node.openBus({remoteAddress: 'localhost-mempool'});
|
||||
}
|
||||
|
||||
this._bus.on('p2p/transaction', this._onTransaction.bind(this));
|
||||
this._bus.subscribe('p2p/transaction');
|
||||
};
|
||||
|
||||
MempoolService.prototype.onBlock = function(block, callback) {
|
||||
|
||||
// remove this block's txs from mempool
|
||||
var self = this;
|
||||
var ops = block.txs.map(function(tx) {
|
||||
return {
|
||||
type: 'del',
|
||||
key: self._encoding.encodeMempoolTransactionKey(tx.txid())
|
||||
};
|
||||
});
|
||||
callback(null, ops);
|
||||
|
||||
};
|
||||
|
||||
MempoolService.prototype._onTransaction = function(tx) {
|
||||
this._db.put(this._encoding.encodeMempoolTransactionKey(tx.txid()),
|
||||
this._encoding.encodeMempoolTransactionValue(tx));
|
||||
};
|
||||
|
||||
MempoolService.prototype.getMempoolTransaction = function(txid, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
self._db.get(self._encoding.encodeMempoolTransactionKey(txid), function(err, tx) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
callback(null, self._encoding.decodeMempoolTransactionValue(tx));
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
MempoolService.prototype.stop = function(callback) {
|
||||
callback();
|
||||
};
|
||||
|
||||
module.exports = MempoolService;
|
||||
59
lib/services/p2p/bcoin.js
Normal file
59
lib/services/p2p/bcoin.js
Normal file
@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
var bcoin = require('bcoin');
|
||||
var EE = require('events').EventEmitter;
|
||||
|
||||
var Bcoin = function(options) {
|
||||
this._config = this._getConfig(options);
|
||||
this.emitter = new EE();
|
||||
};
|
||||
|
||||
Bcoin.prototype.start = function(done) {
|
||||
var self = this;
|
||||
self._bcoin = bcoin.fullnode(self._config);
|
||||
|
||||
log.info('Starting Bcoin full node...');
|
||||
|
||||
self._bcoin.open().then(function() {
|
||||
self._bcoin.connect().then(function() {
|
||||
log.info('Waiting for Bcoin to sync');
|
||||
self._bcoin.startSync();
|
||||
if (self._bcoin.chain.synced){
|
||||
return done();
|
||||
}
|
||||
self._bcoin.chain.once('full', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Bcoin.prototype.stop = function() {
|
||||
this._bcoin.stopSync();
|
||||
this._bcoin.disconnect();
|
||||
this._bcoin.close();
|
||||
};
|
||||
|
||||
// --- privates
|
||||
|
||||
Bcoin.prototype._getConfig = function(options) {
|
||||
var config = {
|
||||
db: 'leveldb',
|
||||
checkpoints: true,
|
||||
network: options.network || 'main',
|
||||
listen: true,
|
||||
logConsole: true,
|
||||
logLevel: 'info',
|
||||
port: options.port,
|
||||
persistent: true,
|
||||
workers: true
|
||||
};
|
||||
if (options.prefix) {
|
||||
config.prefix = options.prefix;
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
module.exports = Bcoin;
|
||||
0
lib/services/p2p/encoding.js
Normal file
0
lib/services/p2p/encoding.js
Normal file
362
lib/services/p2p/index.js
Normal file
362
lib/services/p2p/index.js
Normal file
@ -0,0 +1,362 @@
|
||||
'use strict';
|
||||
|
||||
var p2p = require('bitcore-p2p');
|
||||
var LRU = require('lru-cache');
|
||||
var util = require('util');
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
var BaseService = require('../../service');
|
||||
var assert = require('assert');
|
||||
var Bcoin = require('./bcoin');
|
||||
var Networks = require('bitcore-lib').Networks;
|
||||
|
||||
var P2P = function(options) {
|
||||
|
||||
if (!(this instanceof P2P)) {
|
||||
return new P2P(options);
|
||||
}
|
||||
|
||||
BaseService.call(this, options);
|
||||
this._options = options;
|
||||
|
||||
this._initP2P();
|
||||
this._initPubSub();
|
||||
this._bcoin = null;
|
||||
this._currentBestHeight = null;
|
||||
this._latestBits = 0x1d00ffff;
|
||||
};
|
||||
|
||||
util.inherits(P2P, BaseService);
|
||||
|
||||
P2P.dependencies = [];
|
||||
|
||||
P2P.prototype.clearInventoryCache = function() {
|
||||
this._inv.reset();
|
||||
};
|
||||
|
||||
P2P.prototype.getAPIMethods = function() {
|
||||
var methods = [
|
||||
['clearInventoryCache', this, this.clearInventoryCache, 0],
|
||||
['getBlocks', this, this.getBlocks, 1],
|
||||
['getHeaders', this, this.getHeaders, 1],
|
||||
['getMempool', this, this.getMempool, 0],
|
||||
['sendTransaction', this, this.sendTransaction, 1]
|
||||
];
|
||||
return methods;
|
||||
};
|
||||
|
||||
P2P.prototype.getNumberOfPeers = function() {
|
||||
return this._pool.numberConnected;
|
||||
};
|
||||
|
||||
P2P.prototype.getBlocks = function(filter) {
|
||||
|
||||
var peer = this._getPeer();
|
||||
var blockFilter = this._setResourceFilter(filter, 'blocks');
|
||||
peer.sendMessage(this.messages.GetBlocks(blockFilter));
|
||||
|
||||
};
|
||||
|
||||
P2P.prototype.getHeaders = function(filter) {
|
||||
|
||||
var peer = this._getPeer();
|
||||
var headerFilter = this._setResourceFilter(filter, 'headers');
|
||||
peer.sendMessage(this.messages.GetHeaders(headerFilter));
|
||||
|
||||
};
|
||||
|
||||
P2P.prototype.getMempool = function(filter) {
|
||||
|
||||
var peer = this._getPeer();
|
||||
|
||||
this._setResourceFilter(filter, 'mempool');
|
||||
|
||||
peer.sendMessage(this.messages.MemPool());
|
||||
|
||||
};
|
||||
|
||||
P2P.prototype.getPublishEvents = function() {
|
||||
return [
|
||||
{
|
||||
name: 'p2p/transaction',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'transaction'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'transaction')
|
||||
},
|
||||
{
|
||||
name: 'p2p/block',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'block'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'block')
|
||||
},
|
||||
{
|
||||
name: 'p2p/headers',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'headers'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'headers')
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
P2P.prototype.sendTransaction = function(tx) {
|
||||
p2p.sendMessage(this.messages.Inventory(tx));
|
||||
};
|
||||
|
||||
|
||||
P2P.prototype.start = function(callback) {
|
||||
var self = this;
|
||||
self._startBcoinIfNecessary(function(){
|
||||
self._initCache();
|
||||
self._initPool();
|
||||
self._setListeners();
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
P2P.prototype._disconnectPool = function() {
|
||||
|
||||
log.info('P2P Service: disconnecting pool and peers. SIGINT issued, system shutdown initiated');
|
||||
this._pool.disconnect();
|
||||
|
||||
};
|
||||
|
||||
P2P.prototype.stop = function(callback) {
|
||||
if (this._bcoin){
|
||||
return this._bcoin.stop(callback);
|
||||
}
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
P2P.prototype.subscribe = function(name, emitter) {
|
||||
this.subscriptions[name].push(emitter);
|
||||
log.info(emitter.remoteAddress, 'subscribe:', 'p2p/' + name, 'total:', this.subscriptions[name].length);
|
||||
};
|
||||
|
||||
P2P.prototype.unsubscribe = function(name, emitter) {
|
||||
var index = this.subscriptions[name].indexOf(emitter);
|
||||
if (index > -1) {
|
||||
this.subscriptions[name].splice(index, 1);
|
||||
}
|
||||
log.info(emitter.remoteAddress, 'unsubscribe:', 'p2p/' + name, 'total:', this.subscriptions[name].length);
|
||||
};
|
||||
|
||||
|
||||
// --- privates
|
||||
|
||||
P2P.prototype._addPeer = function(peer) {
|
||||
this._peers.push(peer);
|
||||
};
|
||||
|
||||
P2P.prototype._applyMempoolFilter = function(message) {
|
||||
if (!this._mempoolFilter) {
|
||||
return message;
|
||||
}
|
||||
var txIndex = this._mempoolFilter.indexOf(message.transaction.hash);
|
||||
if (txIndex >= 0) {
|
||||
this._mempoolFilter.splice(txIndex, 1);
|
||||
return;
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
P2P.prototype._broadcast = function(subscribers, name, entity) {
|
||||
for (var i = 0; i < subscribers.length; i++) {
|
||||
subscribers[i].emit(name, entity);
|
||||
}
|
||||
};
|
||||
|
||||
P2P.prototype._connect = function() {
|
||||
var self = this;
|
||||
log.info('Connecting to p2p network.');
|
||||
self._pool.connect();
|
||||
var retryInterval = setInterval(function() {
|
||||
self._pool.connect();
|
||||
}, 5000);
|
||||
self._pool.once('peerready', function() {
|
||||
clearInterval(retryInterval);
|
||||
});
|
||||
};
|
||||
|
||||
P2P.prototype._getBestHeight = function() {
|
||||
|
||||
if (this._peers === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var maxHeight = -1;
|
||||
for(var i = 0; i < this._peers.length; i++) {
|
||||
if (this._peers[i].bestHeight > maxHeight) {
|
||||
maxHeight = this._peers[i].bestHeight;
|
||||
this._peer = this._peers[i];
|
||||
}
|
||||
}
|
||||
return maxHeight;
|
||||
};
|
||||
|
||||
// we should only choose from a list of peers that sync'ed
|
||||
P2P.prototype._getPeer = function() {
|
||||
return this._peer;
|
||||
};
|
||||
|
||||
P2P.prototype._hasPeers = function() {
|
||||
return this._options &&
|
||||
this._options.peers &&
|
||||
this._options.peers.length > 0;
|
||||
};
|
||||
|
||||
P2P.prototype._initCache = function() {
|
||||
this._inv = LRU(1000);
|
||||
};
|
||||
|
||||
P2P.prototype._initP2P = function() {
|
||||
this._maxPeers = this._options.maxPeers || 60;
|
||||
this._minPeers = this._options.minPeers || 0;
|
||||
this._configPeers = this._options.peers;
|
||||
|
||||
if (this.node.network === 'regtest') {
|
||||
Networks.enableRegtest();
|
||||
}
|
||||
this.messages = new p2p.Messages({ network: Networks.get(this.node.network) });
|
||||
this._peerHeights = [];
|
||||
this._peers = [];
|
||||
this._peerIndex = 0;
|
||||
this._mempoolFilter = [];
|
||||
};
|
||||
|
||||
P2P.prototype._initPool = function() {
|
||||
var opts = {};
|
||||
if (this._configPeers) {
|
||||
opts.addrs = this._configPeers;
|
||||
}
|
||||
opts.dnsSeed = false;
|
||||
opts.maxPeers = this._maxPeers;
|
||||
opts.network = this.node.network;
|
||||
this._pool = new p2p.Pool(opts);
|
||||
};
|
||||
|
||||
P2P.prototype._initPubSub = function() {
|
||||
this.subscriptions = {};
|
||||
this.subscriptions.block = [];
|
||||
this.subscriptions.headers = [];
|
||||
this.subscriptions.transaction = [];
|
||||
};
|
||||
|
||||
P2P.prototype._onPeerBlock = function(peer, message) {
|
||||
this._broadcast(this.subscriptions.block, 'p2p/block', message.block);
|
||||
};
|
||||
|
||||
P2P.prototype._onPeerDisconnect = function(peer, addr) {
|
||||
|
||||
if (!this.node.stopping) {
|
||||
this._connect();
|
||||
return;
|
||||
}
|
||||
|
||||
this._removePeer(peer);
|
||||
log.info('Disconnected from peer: ' + addr.ip.v4);
|
||||
};
|
||||
|
||||
P2P.prototype._onPeerHeaders = function(peer, message) {
|
||||
this._broadcast(this.subscriptions.headers, 'p2p/headers', message.headers);
|
||||
};
|
||||
|
||||
P2P.prototype._onPeerInventory = function(peer, message) {
|
||||
|
||||
var self = this;
|
||||
var newDataNeeded = [];
|
||||
message.inventory.forEach(function(inv) {
|
||||
|
||||
if (!self._inv.get(inv.hash)) {
|
||||
|
||||
self._inv.set(inv.hash, true);
|
||||
|
||||
newDataNeeded.push(inv);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (newDataNeeded.length > 0) {
|
||||
peer.sendMessage(self.messages.GetData(newDataNeeded));
|
||||
}
|
||||
};
|
||||
|
||||
P2P.prototype._onPeerReady = function(peer, addr) {
|
||||
|
||||
log.info('Connected to peer: ' + addr.ip.v4 + ', network: ' +
|
||||
peer.network.alias + ', version: ' + peer.version + ', subversion: ' +
|
||||
peer.subversion + ', status: ' + peer.status + ', port: ' +
|
||||
peer.port + ', best height: ' + peer.bestHeight);
|
||||
|
||||
this._addPeer(peer);
|
||||
var bestHeight = this._getBestHeight();
|
||||
|
||||
if (bestHeight >= 0) {
|
||||
this.emit('bestHeight', bestHeight);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
P2P.prototype._onPeerTx = function(peer, message) {
|
||||
var filteredMessage = this._applyMempoolFilter(message);
|
||||
if (filteredMessage) {
|
||||
this._broadcast(this.subscriptions.transaction, 'p2p/transaction', message.transaction);
|
||||
}
|
||||
};
|
||||
|
||||
P2P.prototype._removePeer = function(peer) {
|
||||
this._peers.splice(this._peers.indexOf(peer), 1);
|
||||
};
|
||||
|
||||
P2P.prototype._setListeners = function() {
|
||||
var self = this;
|
||||
self.node.on('stopping', self._disconnectPool.bind(self));
|
||||
self._pool.on('peerready', self._onPeerReady.bind(self));
|
||||
self._pool.on('peerdisconnect', self._onPeerDisconnect.bind(self));
|
||||
self._pool.on('peerinv', self._onPeerInventory.bind(self));
|
||||
self._pool.on('peertx', self._onPeerTx.bind(self));
|
||||
self._pool.on('peerblock', self._onPeerBlock.bind(self));
|
||||
self._pool.on('peerheaders', self._onPeerHeaders.bind(self));
|
||||
self.node.on('ready', self._connect.bind(self));
|
||||
};
|
||||
|
||||
P2P.prototype._setResourceFilter = function(filter, resource) {
|
||||
|
||||
if (resource === 'headers' || resource === 'blocks') {
|
||||
assert(filter && filter.startHash, 'A "startHash" field is required to retrieve headers or blocks');
|
||||
if (!filter.endHash) {
|
||||
filter.endHash = 0;
|
||||
}
|
||||
return { starts: [filter.startHash], stop: filter.endHash };
|
||||
}
|
||||
|
||||
if (resource === 'mempool') {
|
||||
this._mempoolFilter = filter;
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
P2P.prototype._startBcoin = function(callback) {
|
||||
var self = this;
|
||||
const network = ['livenet', 'live', 'main', 'mainnet'].indexOf(this.node.network) !== -1? 'main' : 'testnet';
|
||||
self._bcoin = new Bcoin({
|
||||
network: network,
|
||||
prefix: self.node.datadir,
|
||||
port: 48333
|
||||
});
|
||||
self._bcoin.start(callback);
|
||||
};
|
||||
|
||||
P2P.prototype._startBcoinIfNecessary = function(callback) {
|
||||
if (!this._hasPeers()) {
|
||||
log.info('Peers not explicitly configured, starting a local bcoin node.');
|
||||
this._configPeers = [{ip: {v4: '127.0.0.1'}, port: 48333}];
|
||||
return this._startBcoin(callback);
|
||||
}
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
module.exports = P2P;
|
||||
48
lib/services/timestamp/encoding.js
Normal file
48
lib/services/timestamp/encoding.js
Normal file
@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
function Encoding(servicePrefix) {
|
||||
this._servicePrefix = servicePrefix;
|
||||
this._blockPrefix = new Buffer('00', 'hex');
|
||||
this._timestampPrefix = new Buffer('01', 'hex');
|
||||
}
|
||||
|
||||
// ---- block hash -> timestamp
|
||||
Encoding.prototype.encodeBlockTimestampKey = function(hash) {
|
||||
return Buffer.concat([this._servicePrefix, this._blockPrefix, new Buffer(hash, 'hex')]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeBlockTimestampKey = function(buffer) {
|
||||
return buffer.slice(3).toString('hex');
|
||||
};
|
||||
|
||||
Encoding.prototype.encodeBlockTimestampValue = function(timestamp) {
|
||||
var timestampBuffer = new Buffer(4);
|
||||
timestampBuffer.writeUInt32BE(timestamp);
|
||||
return timestampBuffer;
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeBlockTimestampValue = function(buffer) {
|
||||
return buffer.readUInt32BE();
|
||||
};
|
||||
|
||||
|
||||
// ---- timestamp -> block hash
|
||||
Encoding.prototype.encodeTimestampBlockKey = function(timestamp) {
|
||||
var timestampBuffer = new Buffer(4);
|
||||
timestampBuffer.writeUInt32BE(timestamp);
|
||||
return Buffer.concat([this._servicePrefix, this._timestampPrefix, timestampBuffer]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeTimestampBlockKey = function(buffer) {
|
||||
return buffer.readUInt32BE(3);
|
||||
};
|
||||
|
||||
Encoding.prototype.encodeTimestampBlockValue = function(hash) {
|
||||
return new Buffer(hash, 'hex');
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeTimestampBlockValue = function(buffer) {
|
||||
return buffer.toString('hex');
|
||||
};
|
||||
|
||||
module.exports = Encoding;
|
||||
180
lib/services/timestamp/index.js
Normal file
180
lib/services/timestamp/index.js
Normal file
@ -0,0 +1,180 @@
|
||||
'use strict';
|
||||
|
||||
var BaseService = require('../../service');
|
||||
var Encoding = require('./encoding');
|
||||
var assert = require('assert');
|
||||
var _ = require('lodash');
|
||||
var LRU = require('lru-cache');
|
||||
|
||||
var inherits = require('util').inherits;
|
||||
|
||||
function TimestampService(options) {
|
||||
BaseService.call(this, options);
|
||||
this._db = this.node.services.db;
|
||||
this._lastBlockTimestamp = 0;
|
||||
this._cache = new LRU(10);
|
||||
}
|
||||
|
||||
inherits(TimestampService, BaseService);
|
||||
|
||||
TimestampService.dependencies = [ 'db' ];
|
||||
|
||||
TimestampService.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2]
|
||||
];
|
||||
};
|
||||
|
||||
TimestampService.prototype.getBlockHashesByTimestamp = function(high, low, callback) {
|
||||
|
||||
assert(_.isNumber(low) && _.isNumber(high) && low < high,
|
||||
'start time and end time must be integers representing the number of seconds since epoch.');
|
||||
|
||||
var self = this;
|
||||
var result = [];
|
||||
|
||||
var start = self._encoding.encodeTimestampBlockKey(low);
|
||||
var end = self._encoding.encodeTimestampBlockKey(high);
|
||||
|
||||
var criteria = {
|
||||
gte: start,
|
||||
lte: end
|
||||
};
|
||||
|
||||
var tsStream = self._db.createReadStream(criteria);
|
||||
|
||||
tsStream.on('data', function(data) {
|
||||
var value = self._encoding.decodeTimestampBlockValue(data.value);
|
||||
result.push(value);
|
||||
});
|
||||
|
||||
var streamErr;
|
||||
tsStream.on('error', function(err) {
|
||||
streamErr = err;
|
||||
});
|
||||
|
||||
tsStream.on('end', function() {
|
||||
|
||||
if(streamErr) {
|
||||
return callback(streamErr);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
return callback(null, result);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
TimestampService.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);
|
||||
|
||||
callback();
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
TimestampService.prototype.onBlock = function(block, callback) {
|
||||
|
||||
var operations = [];
|
||||
|
||||
var ts = block.ts;
|
||||
var hash = block.rhash();
|
||||
|
||||
if (ts <= this._lastBlockTimestamp) {
|
||||
ts = this._lastBlockTimestamp + 1;
|
||||
}
|
||||
|
||||
this._lastBlockTimestamp = ts;
|
||||
|
||||
this._cache.set(hash, ts);
|
||||
|
||||
operations = operations.concat([
|
||||
{
|
||||
type: 'put',
|
||||
key: this._encoding.encodeTimestampBlockKey(ts),
|
||||
value: this._encoding.encodeTimestampBlockValue(hash)
|
||||
},
|
||||
{
|
||||
type: 'put',
|
||||
key: this._encoding.encodeBlockTimestampKey(hash),
|
||||
value: this._encoding.encodeBlockTimestampValue(ts)
|
||||
}
|
||||
]);
|
||||
|
||||
callback(null, operations);
|
||||
};
|
||||
|
||||
TimestampService.prototype.onReorg = function(args, callback) {
|
||||
|
||||
var self = this;
|
||||
var commonAncestorHeader = args[0];
|
||||
var oldBlockList = args[1];
|
||||
|
||||
var removalOps = [];
|
||||
|
||||
// remove all the old blocks that we reorg from
|
||||
oldBlockList.forEach(function(block) {
|
||||
removalOps.concat([
|
||||
{
|
||||
type: 'del',
|
||||
key: self._encoding.encodeTimestampBlockKey(block.ts),
|
||||
},
|
||||
{
|
||||
type: 'del',
|
||||
key: self._encoding.encodeBlockTimestampKey(block.rhash()),
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
// look up the adjusted timestamp from our own database and set the lastTimestamp to it
|
||||
self.getTimestamp(commonAncestorHeader.hash, function(err, timestamp) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
self._lastBlockTimestamp = timestamp;
|
||||
callback(null, removalOps);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
TimestampService.prototype.getTimestampSync = function(hash) {
|
||||
return this._cache.get(hash);
|
||||
};
|
||||
|
||||
TimestampService.prototype.getTimestamp = function(hash, callback) {
|
||||
var self = this;
|
||||
self._db.get(self._encoding.encodeBlockTimestampKey(hash), function(err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, self._encoding.decodeBlockTimestampValue(data));
|
||||
});
|
||||
};
|
||||
|
||||
TimestampService.prototype.getHash = function(timestamp, callback) {
|
||||
var self = this;
|
||||
self._db.get(self._encoding.encodeTimestampBlockKey(timestamp), function(err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, self._encoding.decodeTimestampBlockValue(data));
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = TimestampService;
|
||||
57
lib/services/transaction/encoding.js
Normal file
57
lib/services/transaction/encoding.js
Normal file
@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
var Tx = require('bcoin').tx;
|
||||
|
||||
function Encoding(servicePrefix) {
|
||||
this.servicePrefix = servicePrefix;
|
||||
}
|
||||
|
||||
Encoding.prototype.encodeTransactionKey = function(txid) {
|
||||
return Buffer.concat([this.servicePrefix, new Buffer(txid, 'hex')]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeTransactionKey = function(buffer) {
|
||||
return buffer.slice(2).toString('hex');
|
||||
};
|
||||
|
||||
Encoding.prototype.encodeTransactionValue = function(transaction) {
|
||||
var heightBuffer = new Buffer(4);
|
||||
heightBuffer.writeUInt32BE(transaction.__height);
|
||||
|
||||
var timestampBuffer = new Buffer(4);
|
||||
timestampBuffer.writeUInt32BE(transaction.__timestamp);
|
||||
|
||||
var inputValues = transaction.__inputValues;
|
||||
var inputValuesBuffer = new Buffer(8 * inputValues.length);
|
||||
for(var i = 0; i < inputValues.length; i++) {
|
||||
inputValuesBuffer.writeDoubleBE(inputValues[i], i * 8);
|
||||
}
|
||||
|
||||
var inputValuesLengthBuffer = new Buffer(2);
|
||||
inputValuesLengthBuffer.writeUInt16BE(inputValues.length);
|
||||
|
||||
return new Buffer.concat([heightBuffer, timestampBuffer,
|
||||
inputValuesLengthBuffer, inputValuesBuffer, transaction.toRaw()]);
|
||||
};
|
||||
|
||||
Encoding.prototype.decodeTransactionValue = function(buffer) {
|
||||
var height = buffer.readUInt32BE();
|
||||
var timestamp = buffer.readUInt32BE(4);
|
||||
|
||||
var inputValuesLength = buffer.readUInt16BE(8);
|
||||
var inputValues = [];
|
||||
for(var i = 0; i < inputValuesLength; i++) {
|
||||
inputValues.push(buffer.readDoubleBE(i * 8 + 10));
|
||||
}
|
||||
|
||||
var txBuf = buffer.slice(inputValues.length * 8 + 10);
|
||||
var transaction = Tx.fromRaw(txBuf);
|
||||
|
||||
transaction.__height = height;
|
||||
transaction.__inputValues = inputValues;
|
||||
transaction.__timestamp = timestamp;
|
||||
return transaction;
|
||||
};
|
||||
|
||||
module.exports = Encoding;
|
||||
|
||||
321
lib/services/transaction/index.js
Normal file
321
lib/services/transaction/index.js
Normal file
@ -0,0 +1,321 @@
|
||||
'use strict';
|
||||
|
||||
var BaseService = require('../../service');
|
||||
var inherits = require('util').inherits;
|
||||
var Encoding = require('./encoding');
|
||||
var utils = require('../../utils');
|
||||
var _ = require('lodash');
|
||||
var log = require('../../index').log;
|
||||
var async = require('async');
|
||||
var assert = require('assert');
|
||||
|
||||
function TransactionService(options) {
|
||||
BaseService.call(this, options);
|
||||
this._db = this.node.services.db;
|
||||
this._mempool = this.node.services.mempool;
|
||||
this._block = this.node.services.block;
|
||||
this._header = this.node.services.header;
|
||||
this._p2p = this.node.services.p2p;
|
||||
this._timestamp = this.node.services.timestamp;
|
||||
}
|
||||
|
||||
inherits(TransactionService, BaseService);
|
||||
|
||||
TransactionService.dependencies = [
|
||||
'p2p',
|
||||
'db',
|
||||
'block',
|
||||
'timestamp',
|
||||
'mempool'
|
||||
];
|
||||
|
||||
// ---- start public function protorypes
|
||||
TransactionService.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
['getRawTransaction', this, this.getRawTransaction, 1],
|
||||
['getTransaction', this, this.getTransaction, 1],
|
||||
['getDetailedTransaction', this, this.getDetailedTransaction, 1],
|
||||
['getInputValues', this, this.getInputValues, 1]
|
||||
];
|
||||
};
|
||||
|
||||
TransactionService.prototype.getDetailedTransaction = function(txid, options, callback) {
|
||||
this.getTransaction(txid, options, callback);
|
||||
};
|
||||
|
||||
TransactionService.prototype.getTransaction = function(txid, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
callback = options;
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function(next) {
|
||||
self._getTransaction(txid, options, next);
|
||||
},
|
||||
self._getMempoolTransaction.bind(self),
|
||||
self.getInputValues.bind(self),
|
||||
self._setMetaInfo.bind(self)
|
||||
], callback);
|
||||
|
||||
};
|
||||
|
||||
TransactionService.prototype._setMetaInfo = function(tx, options, callback) {
|
||||
|
||||
if (!tx) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// output values
|
||||
var outputSatoshis = 0;
|
||||
|
||||
tx.outputs.forEach(function(output) {
|
||||
outputSatoshis += output.value;
|
||||
});
|
||||
|
||||
tx.outputSatoshis = outputSatoshis;
|
||||
|
||||
|
||||
//input values
|
||||
if (!tx.inputs[0].isCoinbase()) {
|
||||
|
||||
var inputSatoshis = 0;
|
||||
|
||||
tx.__inputValues.forEach(function(val) {
|
||||
|
||||
if (val >+ 0) {
|
||||
inputSatoshis += val;
|
||||
}
|
||||
});
|
||||
|
||||
var feeSatoshis = inputSatoshis - outputSatoshis;
|
||||
tx.inputSatoshis = inputSatoshis;
|
||||
tx.feeSatoshis = feeSatoshis;
|
||||
|
||||
}
|
||||
|
||||
callback(null, tx);
|
||||
|
||||
};
|
||||
|
||||
TransactionService.prototype._getMempoolTransaction = function(txid, tx, options, callback) {
|
||||
|
||||
var self = this;
|
||||
var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool;
|
||||
|
||||
if (tx || !queryMempool) {
|
||||
return callback(null, tx, options);
|
||||
}
|
||||
|
||||
self._mempool.getMempoolTransaction(txid, function(err, tx) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
return callback(null, tx, options);
|
||||
}
|
||||
|
||||
tx.confirmations = 0;
|
||||
callback(null, tx, options);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
TransactionService.prototype._getTransaction = function(txid, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var key = self._encoding.encodeTransactionKey(txid);
|
||||
|
||||
self._db.get(key, function(err, tx) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
return callback(null, txid, tx, options);
|
||||
}
|
||||
|
||||
tx = self._encoding.decodeTransactionValue(tx);
|
||||
tx.confirmations = self._header.getBestHeight() - tx.__height;
|
||||
|
||||
self._header.getBlockHeader(tx.__height, function(err, header) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (header) {
|
||||
tx.blockHash = header.hash;
|
||||
}
|
||||
|
||||
callback(null, txid, tx, options);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
TransactionService.prototype.getInputValues = function(tx, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
if (!tx) {
|
||||
return callback(null, tx, options);
|
||||
}
|
||||
|
||||
async.eachOfLimit(tx.inputs, 4, function(input, index, next) {
|
||||
|
||||
if (!tx.__inputValues) {
|
||||
tx.__inputValues = [];
|
||||
}
|
||||
|
||||
var inputSatoshis = tx.__inputValues[index];
|
||||
|
||||
if (inputSatoshis >= 0 || input.isCoinbase()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var outputIndex = input.prevout.index;
|
||||
|
||||
self._getTransaction(input.prevout.txid(), options, function(err, txid, _tx) {
|
||||
|
||||
if (err || !_tx) {
|
||||
return next(err || new Error('tx not found for tx id: ' + input.prevout.txid()));
|
||||
}
|
||||
|
||||
var output = _tx.outputs[outputIndex];
|
||||
assert(output, 'Expected an output, but did not get one for tx: ' + _tx.txid() + ' outputIndex: ' + outputIndex);
|
||||
tx.__inputValues[index] = output.value;
|
||||
next();
|
||||
|
||||
});
|
||||
|
||||
}, function(err) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var key = self._encoding.encodeTransactionKey(tx.txid());
|
||||
var value = self._encoding.encodeTransactionValue(tx);
|
||||
|
||||
self._db.put(key, value, function(err) {
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, tx, options);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
TransactionService.prototype.sendTransaction = function(tx, callback) {
|
||||
this._p2p.sendTransaction(tx, callback);
|
||||
};
|
||||
|
||||
TransactionService.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);
|
||||
callback();
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
TransactionService.prototype.stop = function(callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
// --- start private prototype functions
|
||||
TransactionService.prototype._getBlockTimestamp = function(hash) {
|
||||
return this._timestamp.getTimestampSync(hash);
|
||||
};
|
||||
|
||||
TransactionService.prototype.onBlock = function(block, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
if (self.node.stopping) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var operations = block.txs.map(function(tx) {
|
||||
return self._processTransaction(tx, { block: block });
|
||||
});
|
||||
|
||||
callback(null, operations);
|
||||
|
||||
};
|
||||
|
||||
TransactionService.prototype.onReorg = function(args, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var oldBlockList = args[1];
|
||||
|
||||
var removalOps = [];
|
||||
|
||||
for(var i = 0; i < oldBlockList.length; i++) {
|
||||
|
||||
var block = oldBlockList[i];
|
||||
|
||||
for(var j = 0; j < block.txs.length; j++) {
|
||||
|
||||
var tx = block.txs[j];
|
||||
|
||||
removalOps.push({
|
||||
type: 'del',
|
||||
key: self._encoding.encodeTransactionKey(tx.txid())
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, removalOps);
|
||||
|
||||
};
|
||||
|
||||
TransactionService.prototype._processTransaction = function(tx, opts) {
|
||||
|
||||
// this index is very simple txid -> tx, but we also need to find each
|
||||
// input's prev output value, the adjusted timestamp for the block and
|
||||
// the tx's block height
|
||||
|
||||
// input values
|
||||
tx.__inputValues = []; // these are lazy-loaded on the first access of the tx
|
||||
|
||||
// timestamp
|
||||
tx.__timestamp = this._getBlockTimestamp(opts.block.rhash());
|
||||
assert(tx.__timestamp, 'Timestamp is required when saving a transaction.');
|
||||
|
||||
// height
|
||||
tx.__height = opts.block.height;
|
||||
assert(tx.__height, 'Block height is required when saving a trasnaction.');
|
||||
|
||||
return {
|
||||
key: this._encoding.encodeTransactionKey(tx.txid()),
|
||||
value: this._encoding.encodeTransactionValue(tx)
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
module.exports = TransactionService;
|
||||
@ -8,10 +8,10 @@ var bodyParser = require('body-parser');
|
||||
var socketio = require('socket.io');
|
||||
var inherits = require('util').inherits;
|
||||
|
||||
var BaseService = require('../service');
|
||||
var BaseService = require('../../service');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var _ = bitcore.deps._;
|
||||
var index = require('../');
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
|
||||
|
||||
@ -50,6 +50,7 @@ var WebService = function(options) {
|
||||
self.server.listen(self.port);
|
||||
self.createMethodsMap();
|
||||
});
|
||||
BaseService.call(this, options);
|
||||
};
|
||||
|
||||
inherits(WebService, BaseService);
|
||||
100
lib/utils.js
100
lib/utils.js
@ -1,32 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1
|
||||
var _ = require('lodash');
|
||||
var constants = require('./constants');
|
||||
var BN = require('bn.js');
|
||||
|
||||
var utils = {};
|
||||
utils.isHash = function isHash(value) {
|
||||
return typeof value === 'string' && value.length === 64 && /^[0-9a-fA-F]+$/.test(value);
|
||||
};
|
||||
|
||||
utils.isSafeNatural = function isSafeNatural(value) {
|
||||
return typeof value === 'number' &&
|
||||
isFinite(value) &&
|
||||
Math.floor(value) === value &&
|
||||
value >= 0 &&
|
||||
value <= MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
utils.startAtZero = function startAtZero(obj, key) {
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
obj[key] = 0;
|
||||
}
|
||||
|
||||
utils.isHeight = function(blockArg) {
|
||||
return _.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg));
|
||||
};
|
||||
|
||||
//start
|
||||
utils.isAbsolutePath = require('path').isAbsolute;
|
||||
if (!utils.isAbsolutePath) {
|
||||
utils.isAbsolutePath = require('path-is-absolute');
|
||||
}
|
||||
|
||||
utils.parseParamsWithJSON = function parseParamsWithJSON(paramsArg) {
|
||||
//main
|
||||
utils.parseParamsWithJSON = function(paramsArg) {
|
||||
var params = paramsArg.map(function(paramArg) {
|
||||
var param;
|
||||
try {
|
||||
@ -39,4 +30,77 @@ utils.parseParamsWithJSON = function parseParamsWithJSON(paramsArg) {
|
||||
return params;
|
||||
};
|
||||
|
||||
utils.getTerminalKey = function(startKey) {
|
||||
if (!startKey || !Buffer.isBuffer(startKey)) {
|
||||
return;
|
||||
}
|
||||
var bn = new BN(startKey);
|
||||
var endBN = bn.iaddn(1);
|
||||
return endBN.toBuffer();
|
||||
};
|
||||
|
||||
utils.diffTime = function(time) {
|
||||
var diff = process.hrtime(time);
|
||||
return (diff[0] * 1E9 + diff[1])/(1E9 * 1.0);
|
||||
};
|
||||
|
||||
utils.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);
|
||||
}
|
||||
};
|
||||
|
||||
utils.encodeTip = function(tip, name) {
|
||||
var key = Buffer.concat([ constants.DB_PREFIX,
|
||||
new Buffer('tip-' + name, 'utf8') ]);
|
||||
|
||||
var heightBuf = new Buffer(4);
|
||||
heightBuf.writeUInt32BE(tip.height);
|
||||
|
||||
var value = Buffer.concat([ heightBuf, new Buffer(tip.hash, 'hex') ]);
|
||||
return { key: key, value: value };
|
||||
|
||||
};
|
||||
|
||||
utils.SimpleMap = function SimpleMap() {
|
||||
var object = {};
|
||||
var array = [];
|
||||
|
||||
this.size = 0;
|
||||
this.length = 0;
|
||||
|
||||
this.hasNullItems = function() {
|
||||
return array.length !== _.compact(array).length;
|
||||
};
|
||||
|
||||
this.get = function (key) {
|
||||
return array[object[key]];
|
||||
};
|
||||
|
||||
this.set = function (key, value, pos) {
|
||||
|
||||
if (pos >= 0) {
|
||||
object[key] = pos;
|
||||
array[pos] = value;
|
||||
} else {
|
||||
object[key] = array.length;
|
||||
array.push(value);
|
||||
}
|
||||
|
||||
this.size = array.length;
|
||||
this.length = array.length;
|
||||
};
|
||||
|
||||
this.getIndex = function (index) {
|
||||
return array[index];
|
||||
};
|
||||
|
||||
this.getLastIndex = function () {
|
||||
return array[array.length - 1];
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = utils;
|
||||
|
||||
3929
package-lock.json
generated
Normal file
3929
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@ -1,8 +1,11 @@
|
||||
{
|
||||
"name": "bitcore-node",
|
||||
"description": "Full node with extended capabilities using Bitcore and Bitcoin Core",
|
||||
"engines": {
|
||||
"node": ">=8.2.0"
|
||||
},
|
||||
"author": "BitPay <dev@bitpay.com>",
|
||||
"version": "3.1.3",
|
||||
"version": "5.0.0",
|
||||
"main": "./index.js",
|
||||
"repository": "git://github.com/bitpay/bitcore-node.git",
|
||||
"homepage": "https://github.com/bitpay/bitcore-node",
|
||||
@ -27,53 +30,52 @@
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"bitcore-node": "./bin/bitcore-node",
|
||||
"bitcoind": "./bin/bitcoind"
|
||||
"bitcore-node": "./bin/bitcore-node"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "./scripts/download",
|
||||
"verify": "./scripts/download --skip-bitcoin-download --verify-bitcoin-download",
|
||||
"test": "mocha -R spec --recursive",
|
||||
"regtest": "./scripts/regtest",
|
||||
"jshint": "jshint --reporter=node_modules/jshint-stylish ./lib",
|
||||
"coverage": "istanbul cover _mocha -- --recursive",
|
||||
"coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- --recursive -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
|
||||
},
|
||||
"tags": [
|
||||
"bitcoin",
|
||||
"bitcoind"
|
||||
"bitcoind",
|
||||
"bcoin",
|
||||
"bitcoin full node",
|
||||
"bitcoin index",
|
||||
"block explorer",
|
||||
"wallet backend"
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "^1.3.0",
|
||||
"async": "^2.5.0",
|
||||
"bcoin": "bcoin-org/bcoin#886008a1822ce1da7fa8395ee7db4bcc1750a28a",
|
||||
"bitcoind-rpc": "^0.6.0",
|
||||
"bitcore-lib": "^0.13.13",
|
||||
"bitcore-lib": "bitpay/bitcore-lib#transitional",
|
||||
"bitcore-p2p": "bitpay/bitcore-p2p#bcoin",
|
||||
"body-parser": "^1.13.3",
|
||||
"colors": "^1.1.2",
|
||||
"commander": "^2.8.1",
|
||||
"errno": "^0.1.4",
|
||||
"express": "^4.13.3",
|
||||
"leveldown": "",
|
||||
"levelup": "",
|
||||
"liftoff": "^2.2.0",
|
||||
"lru-cache": "^4.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"lru-cache": "^4.0.2",
|
||||
"memwatch-next": "^0.3.0",
|
||||
"mkdirp": "0.5.0",
|
||||
"path-is-absolute": "^1.0.0",
|
||||
"semver": "^5.0.1",
|
||||
"socket.io": "^1.4.5",
|
||||
"socket.io-client": "^1.4.5",
|
||||
"zmq": "^2.14.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "~1.2.1",
|
||||
"utf-8-validate": "~1.2.1"
|
||||
"socket.io-client": "^1.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"benchmark": "1.0.0",
|
||||
"bitcore-p2p": "^1.1.0",
|
||||
"chai": "^3.5.0",
|
||||
"coveralls": "^2.11.9",
|
||||
"istanbul": "^0.4.3",
|
||||
"jshint": "^2.9.2",
|
||||
"jshint-stylish": "^2.1.0",
|
||||
"mocha": "^2.4.5",
|
||||
"mocha": "",
|
||||
"proxyquire": "^1.3.1",
|
||||
"rimraf": "^2.4.2",
|
||||
"sinon": "^1.15.4"
|
||||
|
||||
@ -1,485 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// To run the tests: $ mocha -R spec regtest/bitcoind.js
|
||||
|
||||
var path = require('path');
|
||||
var index = require('..');
|
||||
var log = index.log;
|
||||
|
||||
var chai = require('chai');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var BN = bitcore.crypto.BN;
|
||||
var async = require('async');
|
||||
var rimraf = require('rimraf');
|
||||
var bitcoind;
|
||||
|
||||
/* jshint unused: false */
|
||||
var should = chai.should();
|
||||
var assert = chai.assert;
|
||||
var sinon = require('sinon');
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var transactionData = [];
|
||||
var blockHashes = [];
|
||||
var utxos;
|
||||
var client;
|
||||
var coinbasePrivateKey;
|
||||
var privateKey = bitcore.PrivateKey();
|
||||
var destKey = bitcore.PrivateKey();
|
||||
|
||||
describe('Bitcoind Functionality', function() {
|
||||
|
||||
before(function(done) {
|
||||
this.timeout(60000);
|
||||
|
||||
// Add the regtest network
|
||||
bitcore.Networks.enableRegtest();
|
||||
var regtestNetwork = bitcore.Networks.get('regtest');
|
||||
|
||||
var datadir = __dirname + '/data';
|
||||
|
||||
rimraf(datadir + '/regtest', function(err) {
|
||||
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
bitcoind = require('../').services.Bitcoin({
|
||||
spawn: {
|
||||
datadir: datadir,
|
||||
exec: path.resolve(__dirname, '../bin/bitcoind')
|
||||
},
|
||||
node: {
|
||||
network: regtestNetwork,
|
||||
getNetworkName: function() {
|
||||
return 'regtest';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bitcoind.on('error', function(err) {
|
||||
log.error('error="%s"', err.message);
|
||||
});
|
||||
|
||||
log.info('Waiting for Bitcoin Core to initialize...');
|
||||
|
||||
bitcoind.start(function() {
|
||||
log.info('Bitcoind started');
|
||||
|
||||
client = new BitcoinRPC({
|
||||
protocol: 'http',
|
||||
host: '127.0.0.1',
|
||||
port: 30331,
|
||||
user: 'bitcoin',
|
||||
pass: 'local321',
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
log.info('Generating 100 blocks...');
|
||||
|
||||
// Generate enough blocks so that the initial coinbase transactions
|
||||
// can be spent.
|
||||
|
||||
setImmediate(function() {
|
||||
client.generate(150, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
blockHashes = response.result;
|
||||
|
||||
log.info('Preparing test data...');
|
||||
|
||||
// Get all of the unspent outputs
|
||||
client.listUnspent(0, 150, function(err, response) {
|
||||
utxos = response.result;
|
||||
|
||||
async.mapSeries(utxos, function(utxo, next) {
|
||||
async.series([
|
||||
function(finished) {
|
||||
// Load all of the transactions for later testing
|
||||
client.getTransaction(utxo.txid, function(err, txresponse) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// add to the list of transactions for testing later
|
||||
transactionData.push(txresponse.result.hex);
|
||||
finished();
|
||||
});
|
||||
},
|
||||
function(finished) {
|
||||
// Get the private key for each utxo
|
||||
client.dumpPrivKey(utxo.address, function(err, privresponse) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
utxo.privateKeyWIF = privresponse.result;
|
||||
finished();
|
||||
});
|
||||
}
|
||||
], next);
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
this.timeout(60000);
|
||||
bitcoind.node.stopping = true;
|
||||
bitcoind.stop(function(err, result) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get blocks by hash', function() {
|
||||
|
||||
[0,1,2,3,5,6,7,8,9].forEach(function(i) {
|
||||
it('generated block ' + i, function(done) {
|
||||
bitcoind.getBlock(blockHashes[i], function(err, block) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(block);
|
||||
block.hash.should.equal(blockHashes[i]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get blocks as buffers', function() {
|
||||
[0,1,2,3,5,6,7,8,9].forEach(function(i) {
|
||||
it('generated block ' + i, function(done) {
|
||||
bitcoind.getRawBlock(blockHashes[i], function(err, block) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(block);
|
||||
(block instanceof Buffer).should.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get errors as error instances', function() {
|
||||
it('will wrap an rpc into a javascript error', function(done) {
|
||||
bitcoind.client.getBlock(1000000000, function(err, response) {
|
||||
var error = bitcoind._wrapRPCError(err);
|
||||
(error instanceof Error).should.equal(true);
|
||||
error.message.should.equal(err.message);
|
||||
error.code.should.equal(err.code);
|
||||
should.exist(error.stack);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get blocks by height', function() {
|
||||
|
||||
[0,1,2,3,4,5,6,7,8,9].forEach(function(i) {
|
||||
it('generated block ' + i, function(done) {
|
||||
// add the genesis block
|
||||
var height = i + 1;
|
||||
bitcoind.getBlock(i + 1, function(err, block) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(block);
|
||||
block.hash.should.equal(blockHashes[i]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('will get error with number greater than tip', function(done) {
|
||||
bitcoind.getBlock(1000000000, function(err, response) {
|
||||
should.exist(err);
|
||||
err.code.should.equal(-8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('get transactions by hash', function() {
|
||||
[0,1,2,3,4,5,6,7,8,9].forEach(function(i) {
|
||||
it('for tx ' + i, function(done) {
|
||||
var txhex = transactionData[i];
|
||||
var tx = new bitcore.Transaction();
|
||||
tx.fromString(txhex);
|
||||
bitcoind.getTransaction(tx.hash, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
assert(response.toString('hex') === txhex, 'incorrect tx data result');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('will return error if the transaction does not exist', function(done) {
|
||||
var txid = '6226c407d0e9705bdd7158e60983e37d0f5d23529086d6672b07d9238d5aa618';
|
||||
bitcoind.getTransaction(txid, function(err, response) {
|
||||
should.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get transactions as buffers', function() {
|
||||
[0,1,2,3,4,5,6,7,8,9].forEach(function(i) {
|
||||
it('for tx ' + i, function(done) {
|
||||
var txhex = transactionData[i];
|
||||
var tx = new bitcore.Transaction();
|
||||
tx.fromString(txhex);
|
||||
bitcoind.getRawTransaction(tx.hash, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
response.should.be.instanceOf(Buffer);
|
||||
assert(response.toString('hex') === txhex, 'incorrect tx data result');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('will return error if the transaction does not exist', function(done) {
|
||||
var txid = '6226c407d0e9705bdd7158e60983e37d0f5d23529086d6672b07d9238d5aa618';
|
||||
bitcoind.getRawTransaction(txid, function(err, response) {
|
||||
should.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get block header', function() {
|
||||
var expectedWork = new BN(6);
|
||||
[1,2,3,4,5,6,7,8,9].forEach(function(i) {
|
||||
it('generate block ' + i, function(done) {
|
||||
bitcoind.getBlockHeader(blockHashes[i], function(err, blockIndex) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
should.exist(blockIndex);
|
||||
should.exist(blockIndex.chainWork);
|
||||
var work = new BN(blockIndex.chainWork, 'hex');
|
||||
work.toString(16).should.equal(expectedWork.toString(16));
|
||||
expectedWork = expectedWork.add(new BN(2));
|
||||
should.exist(blockIndex.prevHash);
|
||||
blockIndex.hash.should.equal(blockHashes[i]);
|
||||
blockIndex.prevHash.should.equal(blockHashes[i - 1]);
|
||||
blockIndex.height.should.equal(i + 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it('will get null prevHash for the genesis block', function(done) {
|
||||
bitcoind.getBlockHeader(0, function(err, header) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
should.exist(header);
|
||||
should.equal(header.prevHash, undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('will get error for block not found', function(done) {
|
||||
bitcoind.getBlockHeader('notahash', function(err, header) {
|
||||
should.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get block index by height', function() {
|
||||
var expectedWork = new BN(6);
|
||||
[2,3,4,5,6,7,8,9].forEach(function(i) {
|
||||
it('generate block ' + i, function() {
|
||||
bitcoind.getBlockHeader(i, function(err, header) {
|
||||
should.exist(header);
|
||||
should.exist(header.chainWork);
|
||||
var work = new BN(header.chainWork, 'hex');
|
||||
work.toString(16).should.equal(expectedWork.toString(16));
|
||||
expectedWork = expectedWork.add(new BN(2));
|
||||
should.exist(header.prevHash);
|
||||
header.hash.should.equal(blockHashes[i - 1]);
|
||||
header.prevHash.should.equal(blockHashes[i - 2]);
|
||||
header.height.should.equal(i);
|
||||
});
|
||||
});
|
||||
});
|
||||
it('will get error with number greater than tip', function(done) {
|
||||
bitcoind.getBlockHeader(100000, function(err, header) {
|
||||
should.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('send transaction functionality', function() {
|
||||
|
||||
it('will not error and return the transaction hash', function(done) {
|
||||
|
||||
// create and sign the transaction
|
||||
var tx = bitcore.Transaction();
|
||||
tx.from(utxos[0]);
|
||||
tx.change(privateKey.toAddress());
|
||||
tx.to(destKey.toAddress(), utxos[0].amount * 1e8 - 1000);
|
||||
tx.sign(bitcore.PrivateKey.fromWIF(utxos[0].privateKeyWIF));
|
||||
|
||||
// test sending the transaction
|
||||
bitcoind.sendTransaction(tx.serialize(), function(err, hash) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
hash.should.equal(tx.hash);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('will throw an error if an unsigned transaction is sent', function(done) {
|
||||
var tx = bitcore.Transaction();
|
||||
tx.from(utxos[1]);
|
||||
tx.change(privateKey.toAddress());
|
||||
tx.to(destKey.toAddress(), utxos[1].amount * 1e8 - 1000);
|
||||
bitcoind.sendTransaction(tx.uncheckedSerialize(), function(err, hash) {
|
||||
should.exist(err);
|
||||
(err instanceof Error).should.equal(true);
|
||||
should.not.exist(hash);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('will throw an error for unexpected types (tx decode failed)', function(done) {
|
||||
var garbage = new Buffer('abcdef', 'hex');
|
||||
bitcoind.sendTransaction(garbage, function(err, hash) {
|
||||
should.exist(err);
|
||||
should.not.exist(hash);
|
||||
var num = 23;
|
||||
bitcoind.sendTransaction(num, function(err, hash) {
|
||||
should.exist(err);
|
||||
(err instanceof Error).should.equal(true);
|
||||
should.not.exist(hash);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('will emit "tx" events', function(done) {
|
||||
var tx = bitcore.Transaction();
|
||||
tx.from(utxos[2]);
|
||||
tx.change(privateKey.toAddress());
|
||||
tx.to(destKey.toAddress(), utxos[2].amount * 1e8 - 1000);
|
||||
tx.sign(bitcore.PrivateKey.fromWIF(utxos[2].privateKeyWIF));
|
||||
|
||||
var serialized = tx.serialize();
|
||||
|
||||
bitcoind.once('tx', function(buffer) {
|
||||
buffer.toString('hex').should.equal(serialized);
|
||||
done();
|
||||
});
|
||||
bitcoind.sendTransaction(serialized, function(err, hash) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
should.exist(hash);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('fee estimation', function() {
|
||||
it('will estimate fees', function(done) {
|
||||
bitcoind.estimateFee(1, function(err, fees) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
fees.should.equal(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tip updates', function() {
|
||||
it('will get an event when the tip is new', function(done) {
|
||||
this.timeout(4000);
|
||||
bitcoind.on('tip', function(height) {
|
||||
if (height === 151) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get detailed transaction', function() {
|
||||
it('should include details for coinbase tx', function(done) {
|
||||
bitcoind.getDetailedTransaction(utxos[0].txid, function(err, tx) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
should.exist(tx.height);
|
||||
tx.height.should.be.a('number');
|
||||
should.exist(tx.blockTimestamp);
|
||||
should.exist(tx.blockHash);
|
||||
tx.coinbase.should.equal(true);
|
||||
tx.version.should.equal(1);
|
||||
tx.hex.should.be.a('string');
|
||||
tx.locktime.should.equal(0);
|
||||
tx.feeSatoshis.should.equal(0);
|
||||
tx.outputSatoshis.should.equal(50 * 1e8);
|
||||
tx.inputSatoshis.should.equal(0);
|
||||
tx.inputs.length.should.equal(1);
|
||||
tx.outputs.length.should.equal(1);
|
||||
should.equal(tx.inputs[0].prevTxId, null);
|
||||
should.equal(tx.inputs[0].outputIndex, null);
|
||||
tx.inputs[0].script.should.be.a('string');
|
||||
should.equal(tx.inputs[0].scriptAsm, null);
|
||||
should.equal(tx.inputs[0].address, null);
|
||||
should.equal(tx.inputs[0].satoshis, null);
|
||||
tx.outputs[0].satoshis.should.equal(50 * 1e8);
|
||||
tx.outputs[0].script.should.be.a('string');
|
||||
tx.outputs[0].scriptAsm.should.be.a('string');
|
||||
tx.outputs[0].spentTxId.should.be.a('string');
|
||||
tx.outputs[0].spentIndex.should.equal(0);
|
||||
tx.outputs[0].spentHeight.should.be.a('number');
|
||||
tx.outputs[0].address.should.be.a('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getInfo', function() {
|
||||
it('will get information', function(done) {
|
||||
bitcoind.getInfo(function(err, info) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
info.network.should.equal('regtest');
|
||||
should.exist(info);
|
||||
should.exist(info.version);
|
||||
should.exist(info.blocks);
|
||||
should.exist(info.timeOffset);
|
||||
should.exist(info.connections);
|
||||
should.exist(info.difficulty);
|
||||
should.exist(info.testnet);
|
||||
should.exist(info.relayFee);
|
||||
should.exist(info.errors);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -1,183 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
var spawn = require('child_process').spawn;
|
||||
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var rimraf = require('rimraf');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var chai = require('chai');
|
||||
var should = chai.should();
|
||||
|
||||
var index = require('..');
|
||||
var log = index.log;
|
||||
log.debug = function() {};
|
||||
var BitcoreNode = index.Node;
|
||||
var BitcoinService = index.services.Bitcoin;
|
||||
|
||||
describe('Bitcoin Cluster', function() {
|
||||
var node;
|
||||
var daemons = [];
|
||||
var execPath = path.resolve(__dirname, '../bin/bitcoind');
|
||||
var nodesConf = [
|
||||
{
|
||||
datadir: path.resolve(__dirname, './data/node1'),
|
||||
conf: path.resolve(__dirname, './data/node1/bitcoin.conf'),
|
||||
rpcuser: 'bitcoin',
|
||||
rpcpassword: 'local321',
|
||||
rpcport: 30521,
|
||||
zmqpubrawtx: 'tcp://127.0.0.1:30611',
|
||||
zmqpubhashblock: 'tcp://127.0.0.1:30611'
|
||||
},
|
||||
{
|
||||
datadir: path.resolve(__dirname, './data/node2'),
|
||||
conf: path.resolve(__dirname, './data/node2/bitcoin.conf'),
|
||||
rpcuser: 'bitcoin',
|
||||
rpcpassword: 'local321',
|
||||
rpcport: 30522,
|
||||
zmqpubrawtx: 'tcp://127.0.0.1:30622',
|
||||
zmqpubhashblock: 'tcp://127.0.0.1:30622'
|
||||
},
|
||||
{
|
||||
datadir: path.resolve(__dirname, './data/node3'),
|
||||
conf: path.resolve(__dirname, './data/node3/bitcoin.conf'),
|
||||
rpcuser: 'bitcoin',
|
||||
rpcpassword: 'local321',
|
||||
rpcport: 30523,
|
||||
zmqpubrawtx: 'tcp://127.0.0.1:30633',
|
||||
zmqpubhashblock: 'tcp://127.0.0.1:30633'
|
||||
}
|
||||
];
|
||||
|
||||
before(function(done) {
|
||||
log.info('Starting 3 bitcoind daemons');
|
||||
this.timeout(60000);
|
||||
async.each(nodesConf, function(nodeConf, next) {
|
||||
var opts = [
|
||||
'--regtest',
|
||||
'--datadir=' + nodeConf.datadir,
|
||||
'--conf=' + nodeConf.conf
|
||||
];
|
||||
|
||||
rimraf(path.resolve(nodeConf.datadir, './regtest'), function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var process = spawn(execPath, opts, {stdio: 'inherit'});
|
||||
|
||||
var client = new BitcoinRPC({
|
||||
protocol: 'http',
|
||||
host: '127.0.0.1',
|
||||
port: nodeConf.rpcport,
|
||||
user: nodeConf.rpcuser,
|
||||
pass: nodeConf.rpcpassword
|
||||
});
|
||||
|
||||
daemons.push(process);
|
||||
|
||||
async.retry({times: 10, interval: 5000}, function(ready) {
|
||||
client.getInfo(ready);
|
||||
}, next);
|
||||
|
||||
});
|
||||
|
||||
}, done);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
this.timeout(10000);
|
||||
setTimeout(function() {
|
||||
async.each(daemons, function(process, next) {
|
||||
process.once('exit', next);
|
||||
process.kill('SIGINT');
|
||||
}, done);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('step 1: will connect to three bitcoind daemons', function(done) {
|
||||
this.timeout(20000);
|
||||
var configuration = {
|
||||
network: 'regtest',
|
||||
services: [
|
||||
{
|
||||
name: 'bitcoind',
|
||||
module: BitcoinService,
|
||||
config: {
|
||||
connect: [
|
||||
{
|
||||
rpchost: '127.0.0.1',
|
||||
rpcport: 30521,
|
||||
rpcuser: 'bitcoin',
|
||||
rpcpassword: 'local321',
|
||||
zmqpubrawtx: 'tcp://127.0.0.1:30611'
|
||||
},
|
||||
{
|
||||
rpchost: '127.0.0.1',
|
||||
rpcport: 30522,
|
||||
rpcuser: 'bitcoin',
|
||||
rpcpassword: 'local321',
|
||||
zmqpubrawtx: 'tcp://127.0.0.1:30622'
|
||||
},
|
||||
{
|
||||
rpchost: '127.0.0.1',
|
||||
rpcport: 30523,
|
||||
rpcuser: 'bitcoin',
|
||||
rpcpassword: 'local321',
|
||||
zmqpubrawtx: 'tcp://127.0.0.1:30633'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var regtest = bitcore.Networks.get('regtest');
|
||||
should.exist(regtest);
|
||||
|
||||
node = new BitcoreNode(configuration);
|
||||
|
||||
node.on('error', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
|
||||
node.on('ready', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
node.start(function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('step 2: receive block events', function(done) {
|
||||
this.timeout(10000);
|
||||
node.services.bitcoind.once('tip', function(height) {
|
||||
height.should.equal(1);
|
||||
done();
|
||||
});
|
||||
node.generateBlock(1, function(err, hashes) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
should.exist(hashes);
|
||||
});
|
||||
});
|
||||
|
||||
it('step 3: get blocks', function(done) {
|
||||
async.times(3, function(n, next) {
|
||||
node.getBlock(1, function(err, block) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
should.exist(block);
|
||||
next();
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
|
||||
});
|
||||
3
regtest/data/.gitignore
vendored
3
regtest/data/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
.lock
|
||||
blocks
|
||||
regtest
|
||||
@ -1,12 +0,0 @@
|
||||
server=1
|
||||
whitelist=127.0.0.1
|
||||
txindex=1
|
||||
addressindex=1
|
||||
timestampindex=1
|
||||
spentindex=1
|
||||
zmqpubrawtx=tcp://127.0.0.1:30332
|
||||
zmqpubhashblock=tcp://127.0.0.1:30332
|
||||
rpcallowip=127.0.0.1
|
||||
rpcport=30331
|
||||
rpcuser=bitcoin
|
||||
rpcpassword=local321
|
||||
@ -1,14 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICDTCCAXYCCQCsGf/7CM97gDANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJV
|
||||
UzELMAkGA1UECBMCR0ExDDAKBgNVBAcTA2ZvbzEhMB8GA1UEChMYSW50ZXJuZXQg
|
||||
V2lkZ2l0cyBQdHkgTHRkMB4XDTE1MDgyNjE3NTAwOFoXDTE1MDkyNTE3NTAwOFow
|
||||
SzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkdBMQwwCgYDVQQHEwNmb28xITAfBgNV
|
||||
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOB
|
||||
jQAwgYkCgYEA7SXnPpKk+0qXTTa42jzvu/C/gdGby0arY50bE2A+epI7FlI5YqKd
|
||||
hQWoNRpuehF3jH6Ij3mbeeImtTA7TaUYlgHKpn63xfJ0cRj55+6vqq09nDxf0Lm9
|
||||
IpTbgllu1l+SHtSuzFBVtGuNRSqObf8gD5XCD5lWK1vXHQ6PFSnAakMCAwEAATAN
|
||||
BgkqhkiG9w0BAQUFAAOBgQBNARLDgsw7NCBVkn57AEgwZptxeyvFWlGZCd0BmYIX
|
||||
ZFk7T1OQDwn7GlHry2IBswI0QRi076RQ4oJq+fg2O3XdFvEYV0cyypW7AxrnYTHP
|
||||
m1h2xr6Y5vhxFKP8DxpAxST27DHbR18YvTD+IGtp2UjLj646587N0MWxt8vmaU3c
|
||||
og==
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,15 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXwIBAAKBgQDtJec+kqT7SpdNNrjaPO+78L+B0ZvLRqtjnRsTYD56kjsWUjli
|
||||
op2FBag1Gm56EXeMfoiPeZt54ia1MDtNpRiWAcqmfrfF8nRxGPnn7q+qrT2cPF/Q
|
||||
ub0ilNuCWW7WX5Ie1K7MUFW0a41FKo5t/yAPlcIPmVYrW9cdDo8VKcBqQwIDAQAB
|
||||
AoGBAOzYFxyCPu2OMI/4ICQuCcwtBEa2Ph+Fo/Rn2ru+OogV9Zc0ZYWiHSnWXYkz
|
||||
rbSSL1CMqvyIGoRfHgOFeSTxxxtVyRo5LayI6Ce6V04yUFua16uo6hMX5bfGKZ9d
|
||||
uq/HCDmdQvgifxUFpTpoSencuxwVCSYstMqjGfpobc5nxN2RAkEA+23BWNyA2qcq
|
||||
yqSplgTQO0laXox9ksr1k2mJB2HtG3GNs1kapP+Z8AVtn/Mf9Va0hgbSnqpF9fll
|
||||
1xpBqfidSQJBAPF1rizAm7xP7AeRRET36YKjZCVayA/g4rp7kecrNJTTupJIuxHr
|
||||
JUlOTtXEWjIVcutSM7bP7bytPv4SAaApBysCQQDEZhqnCC+bHQO/IVrbNc1W0ljG
|
||||
DGY22VV1HfYND0CAtHXkx9CZXJPpusPEMs0e/uiq3P9/MzDNEFCt8vOiCvMJAkEA
|
||||
65oDIKGzk/R7/wpMjetEyva5AgXpjizFrmZigCjVPp61voT/G8XQ9Q1WuRjFVXc+
|
||||
UcU8tpV+iIqXG3vgYDGITwJBANb0NFFF8QsygbENtad1tw1C/hNabHk8n9hu+Z8+
|
||||
OSzEMlP7SsvddPaqusGydhTUxazoG3s4kEh5WmCWKgZGKO0=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@ -1,16 +0,0 @@
|
||||
server=1
|
||||
whitelist=127.0.0.1
|
||||
txindex=1
|
||||
addressindex=1
|
||||
timestampindex=1
|
||||
spentindex=1
|
||||
addnode=127.0.0.1:30432
|
||||
addnode=127.0.0.1:30433
|
||||
port=30431
|
||||
rpcport=30521
|
||||
zmqpubrawtx=tcp://127.0.0.1:30611
|
||||
zmqpubhashblock=tcp://127.0.0.1:30611
|
||||
rpcallowip=127.0.0.1
|
||||
rpcuser=bitcoin
|
||||
rpcpassword=local321
|
||||
keypool=3
|
||||
@ -1,16 +0,0 @@
|
||||
server=1
|
||||
whitelist=127.0.0.1
|
||||
txindex=1
|
||||
addressindex=1
|
||||
timestampindex=1
|
||||
spentindex=1
|
||||
addnode=127.0.0.1:30431
|
||||
addnode=127.0.0.1:30433
|
||||
port=30432
|
||||
rpcport=30522
|
||||
zmqpubrawtx=tcp://127.0.0.1:30622
|
||||
zmqpubhashblock=tcp://127.0.0.1:30622
|
||||
rpcallowip=127.0.0.1
|
||||
rpcuser=bitcoin
|
||||
rpcpassword=local321
|
||||
keypool=3
|
||||
@ -1,16 +0,0 @@
|
||||
server=1
|
||||
whitelist=127.0.0.1
|
||||
txindex=1
|
||||
addressindex=1
|
||||
timestampindex=1
|
||||
spentindex=1
|
||||
addnode=127.0.0.1:30431
|
||||
addnode=127.0.0.1:30432
|
||||
port=30433
|
||||
rpcport=30523
|
||||
zmqpubrawtx=tcp://127.0.0.1:30633
|
||||
zmqpubhashblock=tcp://127.0.0.1:30633
|
||||
rpcallowip=127.0.0.1
|
||||
rpcuser=bitcoin
|
||||
rpcpassword=local321
|
||||
keypool=3
|
||||
758
regtest/node.js
758
regtest/node.js
@ -1,758 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// To run the tests: $ mocha -R spec regtest/node.js
|
||||
|
||||
var path = require('path');
|
||||
var index = require('..');
|
||||
var async = require('async');
|
||||
var log = index.log;
|
||||
log.debug = function() {};
|
||||
|
||||
var chai = require('chai');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var rimraf = require('rimraf');
|
||||
var node;
|
||||
|
||||
var should = chai.should();
|
||||
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var index = require('..');
|
||||
var Transaction = bitcore.Transaction;
|
||||
var BitcoreNode = index.Node;
|
||||
var BitcoinService = index.services.Bitcoin;
|
||||
var testWIF = 'cSdkPxkAjA4HDr5VHgsebAPDEh9Gyub4HK8UJr2DFGGqKKy4K5sG';
|
||||
var testKey;
|
||||
var client;
|
||||
|
||||
var outputForIsSpentTest1;
|
||||
var unspentOutputSpentTxId;
|
||||
|
||||
describe('Node Functionality', function() {
|
||||
|
||||
var regtest;
|
||||
|
||||
before(function(done) {
|
||||
this.timeout(20000);
|
||||
|
||||
var datadir = __dirname + '/data';
|
||||
|
||||
testKey = bitcore.PrivateKey(testWIF);
|
||||
|
||||
rimraf(datadir + '/regtest', function(err) {
|
||||
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var configuration = {
|
||||
network: 'regtest',
|
||||
services: [
|
||||
{
|
||||
name: 'bitcoind',
|
||||
module: BitcoinService,
|
||||
config: {
|
||||
spawn: {
|
||||
datadir: datadir,
|
||||
exec: path.resolve(__dirname, '../bin/bitcoind')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
node = new BitcoreNode(configuration);
|
||||
|
||||
regtest = bitcore.Networks.get('regtest');
|
||||
should.exist(regtest);
|
||||
|
||||
node.on('error', function(err) {
|
||||
log.error(err);
|
||||
});
|
||||
|
||||
node.start(function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
client = new BitcoinRPC({
|
||||
protocol: 'http',
|
||||
host: '127.0.0.1',
|
||||
port: 30331,
|
||||
user: 'bitcoin',
|
||||
pass: 'local321',
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
var syncedHandler = function() {
|
||||
if (node.services.bitcoind.height === 150) {
|
||||
node.services.bitcoind.removeListener('synced', syncedHandler);
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
node.services.bitcoind.on('synced', syncedHandler);
|
||||
|
||||
client.generate(150, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
this.timeout(20000);
|
||||
node.stop(function(err, result) {
|
||||
if(err) {
|
||||
throw err;
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bus Functionality', function() {
|
||||
it('subscribes and unsubscribes to an event on the bus', function(done) {
|
||||
var bus = node.openBus();
|
||||
var blockExpected;
|
||||
var blockReceived;
|
||||
bus.subscribe('bitcoind/hashblock');
|
||||
bus.on('bitcoind/hashblock', function(data) {
|
||||
bus.unsubscribe('bitcoind/hashblock');
|
||||
if (blockExpected) {
|
||||
data.should.be.equal(blockExpected);
|
||||
done();
|
||||
} else {
|
||||
blockReceived = data;
|
||||
}
|
||||
});
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (blockReceived) {
|
||||
blockReceived.should.be.equal(response.result[0]);
|
||||
done();
|
||||
} else {
|
||||
blockExpected = response.result[0];
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Address Functionality', function() {
|
||||
var address;
|
||||
var unspentOutput;
|
||||
before(function(done) {
|
||||
this.timeout(10000);
|
||||
address = testKey.toAddress(regtest).toString();
|
||||
var startHeight = node.services.bitcoind.height;
|
||||
node.services.bitcoind.on('tip', function(height) {
|
||||
if (height === startHeight + 3) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.sendToAddress(testKey.toAddress(regtest).toString(), 10, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
client.generate(3, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should be able to get the balance of the test address', function(done) {
|
||||
node.getAddressBalance(address, false, function(err, data) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
data.balance.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can get unspent outputs for address', function(done) {
|
||||
node.getAddressUnspentOutputs(address, false, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(1);
|
||||
unspentOutput = outputForIsSpentTest1 = results[0];
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('correctly give the history for the address', function(done) {
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 10,
|
||||
queryMempool: false
|
||||
};
|
||||
node.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var items = results.items;
|
||||
items.length.should.equal(1);
|
||||
var info = items[0];
|
||||
should.exist(info.addresses[address]);
|
||||
info.addresses[address].outputIndexes.length.should.equal(1);
|
||||
info.addresses[address].outputIndexes[0].should.be.within(0, 1);
|
||||
info.addresses[address].inputIndexes.should.deep.equal([]);
|
||||
info.satoshis.should.equal(10 * 1e8);
|
||||
info.confirmations.should.equal(3);
|
||||
info.tx.blockTimestamp.should.be.a('number');
|
||||
info.tx.feeSatoshis.should.be.within(950, 4000);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('correctly give the summary for the address', function(done) {
|
||||
var options = {
|
||||
queryMempool: false
|
||||
};
|
||||
node.getAddressSummary(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.totalReceived.should.equal(1000000000);
|
||||
results.totalSpent.should.equal(0);
|
||||
results.balance.should.equal(1000000000);
|
||||
should.not.exist(results.unconfirmedBalance);
|
||||
results.appearances.should.equal(1);
|
||||
should.not.exist(results.unconfirmedAppearances);
|
||||
results.txids.length.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe('History', function() {
|
||||
|
||||
this.timeout(20000);
|
||||
|
||||
var testKey2;
|
||||
var address2;
|
||||
var testKey3;
|
||||
var address3;
|
||||
var testKey4;
|
||||
var address4;
|
||||
var testKey5;
|
||||
var address5;
|
||||
var testKey6;
|
||||
var address6;
|
||||
var tx2Amount;
|
||||
var tx2Hash;
|
||||
|
||||
before(function(done) {
|
||||
/* jshint maxstatements: 50 */
|
||||
|
||||
// Finished once all blocks have been mined
|
||||
var startHeight = node.services.bitcoind.height;
|
||||
node.services.bitcoind.on('tip', function(height) {
|
||||
if (height === startHeight + 5) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
testKey2 = bitcore.PrivateKey.fromWIF('cNfF4jXiLHQnFRsxaJyr2YSGcmtNYvxQYSakNhuDGxpkSzAwn95x');
|
||||
address2 = testKey2.toAddress(regtest).toString();
|
||||
|
||||
testKey3 = bitcore.PrivateKey.fromWIF('cVTYQbaFNetiZcvxzXcVMin89uMLC43pEBMy2etgZHbPPxH5obYt');
|
||||
address3 = testKey3.toAddress(regtest).toString();
|
||||
|
||||
testKey4 = bitcore.PrivateKey.fromWIF('cPNQmfE31H2oCUFqaHpfSqjDibkt7XoT2vydLJLDHNTvcddCesGw');
|
||||
address4 = testKey4.toAddress(regtest).toString();
|
||||
|
||||
testKey5 = bitcore.PrivateKey.fromWIF('cVrzm9gCmnzwEVMGeCxY6xLVPdG3XWW97kwkFH3H3v722nb99QBF');
|
||||
address5 = testKey5.toAddress(regtest).toString();
|
||||
|
||||
testKey6 = bitcore.PrivateKey.fromWIF('cPfMesNR2gsQEK69a6xe7qE44CZEZavgMUak5hQ74XDgsRmmGBYF');
|
||||
address6 = testKey6.toAddress(regtest).toString();
|
||||
|
||||
var tx = new Transaction();
|
||||
tx.from(unspentOutput);
|
||||
tx.to(address, 1 * 1e8);
|
||||
tx.to(address, 2 * 1e8);
|
||||
tx.to(address, 0.5 * 1e8);
|
||||
tx.to(address, 3 * 1e8);
|
||||
tx.fee(10000);
|
||||
tx.change(address);
|
||||
tx.sign(testKey);
|
||||
|
||||
unspentOutputSpentTxId = tx.id;
|
||||
|
||||
function mineBlock(next) {
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(response);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
node.sendTransaction(tx.serialize(), function(err, hash) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
client.generate(1, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
should.exist(response);
|
||||
|
||||
node.getAddressUnspentOutputs(address, false, function(err, results) {
|
||||
/* jshint maxstatements: 50 */
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(5);
|
||||
|
||||
async.series([
|
||||
function(next) {
|
||||
var tx2 = new Transaction();
|
||||
tx2Amount = results[0].satoshis - 10000;
|
||||
tx2.from(results[0]);
|
||||
tx2.to(address2, tx2Amount);
|
||||
tx2.change(address);
|
||||
tx2.sign(testKey);
|
||||
tx2Hash = tx2.hash;
|
||||
node.sendTransaction(tx2.serialize(), function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
mineBlock(next);
|
||||
});
|
||||
}, function(next) {
|
||||
var tx3 = new Transaction();
|
||||
tx3.from(results[1]);
|
||||
tx3.to(address3, results[1].satoshis - 10000);
|
||||
tx3.change(address);
|
||||
tx3.sign(testKey);
|
||||
node.sendTransaction(tx3.serialize(), function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
mineBlock(next);
|
||||
});
|
||||
}, function(next) {
|
||||
var tx4 = new Transaction();
|
||||
tx4.from(results[2]);
|
||||
tx4.to(address4, results[2].satoshis - 10000);
|
||||
tx4.change(address);
|
||||
tx4.sign(testKey);
|
||||
node.sendTransaction(tx4.serialize(), function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
mineBlock(next);
|
||||
});
|
||||
}, function(next) {
|
||||
var tx5 = new Transaction();
|
||||
tx5.from(results[3]);
|
||||
tx5.from(results[4]);
|
||||
tx5.to(address5, results[3].satoshis - 10000);
|
||||
tx5.to(address6, results[4].satoshis - 10000);
|
||||
tx5.change(address);
|
||||
tx5.sign(testKey);
|
||||
node.sendTransaction(tx5.serialize(), function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
mineBlock(next);
|
||||
});
|
||||
}
|
||||
], function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('five addresses', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {};
|
||||
node.getAddressHistory(addresses, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.totalCount.should.equal(4);
|
||||
var history = results.items;
|
||||
history.length.should.equal(4);
|
||||
history[0].tx.height.should.equal(159);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].tx.height.should.equal(158);
|
||||
should.exist(history[1].addresses[address4]);
|
||||
history[2].tx.height.should.equal(157);
|
||||
should.exist(history[2].addresses[address3]);
|
||||
history[3].tx.height.should.equal(156);
|
||||
should.exist(history[3].addresses[address2]);
|
||||
history[3].satoshis.should.equal(tx2Amount);
|
||||
history[3].tx.hash.should.equal(tx2Hash);
|
||||
history[3].confirmations.should.equal(4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('five addresses (limited by height)', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {
|
||||
start: 158,
|
||||
end: 157
|
||||
};
|
||||
node.getAddressHistory(addresses, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.totalCount.should.equal(2);
|
||||
var history = results.items;
|
||||
history.length.should.equal(2);
|
||||
history[0].tx.height.should.equal(158);
|
||||
history[0].confirmations.should.equal(2);
|
||||
history[1].tx.height.should.equal(157);
|
||||
should.exist(history[1].addresses[address3]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('five addresses (limited by height 155 to 154)', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {
|
||||
start: 157,
|
||||
end: 156
|
||||
};
|
||||
node.getAddressHistory(addresses, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.totalCount.should.equal(2);
|
||||
var history = results.items;
|
||||
history.length.should.equal(2);
|
||||
history[0].tx.height.should.equal(157);
|
||||
history[1].tx.height.should.equal(156);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('five addresses (paginated by index)', function(done) {
|
||||
var addresses = [
|
||||
address2,
|
||||
address3,
|
||||
address4,
|
||||
address5,
|
||||
address6
|
||||
];
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 3
|
||||
};
|
||||
node.getAddressHistory(addresses, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.totalCount.should.equal(4);
|
||||
var history = results.items;
|
||||
history.length.should.equal(3);
|
||||
history[0].tx.height.should.equal(159);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].tx.height.should.equal(158);
|
||||
should.exist(history[1].addresses[address4]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('one address with sending and receiving', function(done) {
|
||||
var addresses = [
|
||||
address
|
||||
];
|
||||
var options = {};
|
||||
node.getAddressHistory(addresses, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.totalCount.should.equal(6);
|
||||
var history = results.items;
|
||||
history.length.should.equal(6);
|
||||
history[0].tx.height.should.equal(159);
|
||||
history[0].addresses[address].inputIndexes.should.deep.equal([0, 1]);
|
||||
history[0].addresses[address].outputIndexes.should.deep.equal([2]);
|
||||
history[0].confirmations.should.equal(1);
|
||||
history[1].tx.height.should.equal(158);
|
||||
history[2].tx.height.should.equal(157);
|
||||
history[3].tx.height.should.equal(156);
|
||||
history[4].tx.height.should.equal(155);
|
||||
history[4].satoshis.should.equal(-10000);
|
||||
history[4].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
|
||||
history[4].addresses[address].inputIndexes.should.deep.equal([0]);
|
||||
history[5].tx.height.should.equal(152);
|
||||
history[5].satoshis.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('summary for an address (sending and receiving)', function(done) {
|
||||
node.getAddressSummary(address, {}, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.totalReceived.should.equal(2000000000);
|
||||
results.totalSpent.should.equal(1999990000);
|
||||
results.balance.should.equal(10000);
|
||||
results.unconfirmedBalance.should.equal(0);
|
||||
results.appearances.should.equal(6);
|
||||
results.unconfirmedAppearances.should.equal(0);
|
||||
results.txids.length.should.equal(6);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('total transaction count (sending and receiving)', function(done) {
|
||||
var addresses = [
|
||||
address
|
||||
];
|
||||
var options = {};
|
||||
node.getAddressHistory(addresses, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.totalCount.should.equal(6);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination', function() {
|
||||
it('from 0 to 1', function(done) {
|
||||
var options = {
|
||||
from: 0,
|
||||
to: 1
|
||||
};
|
||||
node.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var history = results.items;
|
||||
history.length.should.equal(1);
|
||||
history[0].tx.height.should.equal(159);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 1 to 2', function(done) {
|
||||
var options = {
|
||||
from: 1,
|
||||
to: 2
|
||||
};
|
||||
node.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var history = results.items;
|
||||
history.length.should.equal(1);
|
||||
history[0].tx.height.should.equal(158);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 2 to 3', function(done) {
|
||||
var options = {
|
||||
from: 2,
|
||||
to: 3
|
||||
};
|
||||
node.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var history = results.items;
|
||||
history.length.should.equal(1);
|
||||
history[0].tx.height.should.equal(157);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 3 to 4', function(done) {
|
||||
var options = {
|
||||
from: 3,
|
||||
to: 4
|
||||
};
|
||||
node.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var history = results.items;
|
||||
history.length.should.equal(1);
|
||||
history[0].tx.height.should.equal(156);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 4 to 5', function(done) {
|
||||
var options = {
|
||||
from: 4,
|
||||
to: 5
|
||||
};
|
||||
node.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var history = results.items;
|
||||
history.length.should.equal(1);
|
||||
history[0].tx.height.should.equal(155);
|
||||
history[0].satoshis.should.equal(-10000);
|
||||
history[0].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
|
||||
history[0].addresses[address].inputIndexes.should.deep.equal([0]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('from 5 to 6', function(done) {
|
||||
var options = {
|
||||
from: 5,
|
||||
to: 6
|
||||
};
|
||||
node.getAddressHistory(address, options, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var history = results.items;
|
||||
history.length.should.equal(1);
|
||||
history[0].tx.height.should.equal(152);
|
||||
history[0].satoshis.should.equal(10 * 1e8);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mempool Index', function() {
|
||||
var unspentOutput;
|
||||
before(function(done) {
|
||||
node.getAddressUnspentOutputs(address, false, function(err, results) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
results.length.should.equal(1);
|
||||
unspentOutput = results[0];
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('will update the mempool index after new tx', function(done) {
|
||||
var memAddress = bitcore.PrivateKey().toAddress(node.network).toString();
|
||||
var tx = new Transaction();
|
||||
tx.from(unspentOutput);
|
||||
tx.to(memAddress, unspentOutput.satoshis - 1000);
|
||||
tx.fee(1000);
|
||||
tx.sign(testKey);
|
||||
|
||||
node.services.bitcoind.sendTransaction(tx.serialize(), function(err, hash) {
|
||||
node.getAddressTxids(memAddress, {}, function(err, txids) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
txids.length.should.equal(1);
|
||||
txids[0].should.equal(hash);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Orphaned Transactions', function() {
|
||||
this.timeout(8000);
|
||||
var orphanedTransaction;
|
||||
|
||||
before(function(done) {
|
||||
var count;
|
||||
var invalidatedBlockHash;
|
||||
|
||||
async.series([
|
||||
function(next) {
|
||||
client.sendToAddress(testKey.toAddress(regtest).toString(), 10, function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
client.generate(1, next);
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
client.getBlockCount(function(err, response) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
count = response.result;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
client.getBlockHash(count, function(err, response) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
invalidatedBlockHash = response.result;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
client.getBlock(invalidatedBlockHash, function(err, response) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
orphanedTransaction = response.result.tx[1];
|
||||
should.exist(orphanedTransaction);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
client.invalidateBlock(invalidatedBlockHash, next);
|
||||
}
|
||||
], function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('will not show confirmation count for orphaned transaction', function(done) {
|
||||
// This test verifies that in the situation that the transaction is not in the mempool and
|
||||
// is included in an orphaned block transaction index that the confirmation count will be unconfirmed.
|
||||
node.getDetailedTransaction(orphanedTransaction, function(err, data) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
should.exist(data);
|
||||
should.exist(data.height);
|
||||
data.height.should.equal(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
217
regtest/p2p.js
217
regtest/p2p.js
@ -1,217 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// To run the tests: $ mocha -R spec regtest/p2p.js
|
||||
|
||||
var path = require('path');
|
||||
var index = require('..');
|
||||
var log = index.log;
|
||||
|
||||
var p2p = require('bitcore-p2p');
|
||||
var Peer = p2p.Peer;
|
||||
var Messages = p2p.Messages;
|
||||
var chai = require('chai');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var Transaction = bitcore.Transaction;
|
||||
var BN = bitcore.crypto.BN;
|
||||
var async = require('async');
|
||||
var rimraf = require('rimraf');
|
||||
var bitcoind;
|
||||
|
||||
/* jshint unused: false */
|
||||
var should = chai.should();
|
||||
var assert = chai.assert;
|
||||
var sinon = require('sinon');
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var transactionData = [];
|
||||
var blockHashes = [];
|
||||
var txs = [];
|
||||
var client;
|
||||
var messages;
|
||||
var peer;
|
||||
var coinbasePrivateKey;
|
||||
var privateKey = bitcore.PrivateKey();
|
||||
var destKey = bitcore.PrivateKey();
|
||||
var BufferUtil = bitcore.util.buffer;
|
||||
var blocks;
|
||||
|
||||
describe('P2P Functionality', function() {
|
||||
|
||||
before(function(done) {
|
||||
this.timeout(100000);
|
||||
|
||||
// enable regtest
|
||||
bitcore.Networks.enableRegtest();
|
||||
var regtestNetwork = bitcore.Networks.get('regtest');
|
||||
var datadir = __dirname + '/data';
|
||||
|
||||
rimraf(datadir + '/regtest', function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
bitcoind = require('../').services.Bitcoin({
|
||||
spawn: {
|
||||
datadir: datadir,
|
||||
exec: path.resolve(__dirname, '../bin/bitcoind')
|
||||
},
|
||||
node: {
|
||||
network: bitcore.Networks.testnet
|
||||
}
|
||||
});
|
||||
|
||||
bitcoind.on('error', function(err) {
|
||||
log.error('error="%s"', err.message);
|
||||
});
|
||||
|
||||
log.info('Waiting for Bitcoin Core to initialize...');
|
||||
|
||||
bitcoind.start(function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
log.info('Bitcoind started');
|
||||
|
||||
client = new BitcoinRPC({
|
||||
protocol: 'http',
|
||||
host: '127.0.0.1',
|
||||
port: 30331,
|
||||
user: 'bitcoin',
|
||||
pass: 'local321',
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
peer = new Peer({
|
||||
host: '127.0.0.1',
|
||||
port: '18444',
|
||||
network: regtestNetwork
|
||||
});
|
||||
|
||||
messages = new Messages({
|
||||
network: regtestNetwork
|
||||
});
|
||||
|
||||
blocks = 500;
|
||||
|
||||
log.info('Generating ' + blocks + ' blocks...');
|
||||
|
||||
// Generate enough blocks so that the initial coinbase transactions
|
||||
// can be spent.
|
||||
|
||||
setImmediate(function() {
|
||||
client.generate(blocks, function(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
blockHashes = response.result;
|
||||
|
||||
log.info('Preparing test data...');
|
||||
|
||||
// Get all of the unspent outputs
|
||||
client.listUnspent(0, blocks, function(err, response) {
|
||||
var utxos = response.result;
|
||||
|
||||
async.mapSeries(utxos, function(utxo, next) {
|
||||
async.series([
|
||||
function(finished) {
|
||||
// Load all of the transactions for later testing
|
||||
client.getTransaction(utxo.txid, function(err, txresponse) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// add to the list of transactions for testing later
|
||||
transactionData.push(txresponse.result.hex);
|
||||
finished();
|
||||
});
|
||||
},
|
||||
function(finished) {
|
||||
// Get the private key for each utxo
|
||||
client.dumpPrivKey(utxo.address, function(err, privresponse) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
utxo.privateKeyWIF = privresponse.result;
|
||||
var tx = bitcore.Transaction();
|
||||
tx.from(utxo);
|
||||
tx.change(privateKey.toAddress());
|
||||
tx.to(destKey.toAddress(), utxo.amount * 1e8 - 1000);
|
||||
tx.sign(bitcore.PrivateKey.fromWIF(utxo.privateKeyWIF));
|
||||
txs.push(tx);
|
||||
finished();
|
||||
});
|
||||
}
|
||||
], next);
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
peer.on('ready', function() {
|
||||
log.info('Peer ready');
|
||||
done();
|
||||
});
|
||||
log.info('Connecting to peer');
|
||||
peer.connect();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
this.timeout(20000);
|
||||
peer.on('disconnect', function() {
|
||||
log.info('Peer disconnected');
|
||||
bitcoind.node.stopping = true;
|
||||
bitcoind.stop(function(err, result) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
peer.disconnect();
|
||||
});
|
||||
|
||||
it('will be able to handle many inventory messages and be able to send getdata messages and received the txs', function(done) {
|
||||
this.timeout(100000);
|
||||
|
||||
var usedTxs = {};
|
||||
|
||||
bitcoind.on('tx', function(buffer) {
|
||||
var txFromResult = new Transaction().fromBuffer(buffer);
|
||||
var tx = usedTxs[txFromResult.id];
|
||||
should.exist(tx);
|
||||
buffer.toString('hex').should.equal(tx.serialize());
|
||||
delete usedTxs[tx.id];
|
||||
if (Object.keys(usedTxs).length === 0) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
peer.on('getdata', function(message) {
|
||||
var hash = message.inventory[0].hash;
|
||||
var reversedHash = BufferUtil.reverse(hash).toString('hex');
|
||||
var tx = usedTxs[reversedHash];
|
||||
if (reversedHash === tx.id) {
|
||||
var txMessage = messages.Transaction(tx);
|
||||
peer.sendMessage(txMessage);
|
||||
}
|
||||
});
|
||||
async.whilst(
|
||||
function() {
|
||||
return txs.length > 0;
|
||||
},
|
||||
function(callback) {
|
||||
var tx = txs.pop();
|
||||
usedTxs[tx.id] = tx;
|
||||
var message = messages.Inventory.forTransaction(tx.hash);
|
||||
peer.sendMessage(message);
|
||||
callback();
|
||||
},
|
||||
function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
119
scripts/download
119
scripts/download
@ -1,119 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
|
||||
platform=`uname -a | awk '{print tolower($1)}'`
|
||||
arch=`uname -m`
|
||||
version="0.12.1"
|
||||
url="https://github.com/bitpay/bitcoin/releases/download"
|
||||
tag="v0.12.1-bitcore-4"
|
||||
|
||||
if [ "${platform}" == "linux" ]; then
|
||||
if [ "${arch}" == "x86_64" ]; then
|
||||
tarball_name="bitcoin-${version}-linux64.tar.gz"
|
||||
elif [ "${arch}" == "x86_32" ]; then
|
||||
tarball_name="bitcoin-${version}-linux32.tar.gz"
|
||||
fi
|
||||
elif [ "${platform}" == "darwin" ]; then
|
||||
tarball_name="bitcoin-${version}-osx64.tar.gz"
|
||||
else
|
||||
echo "Bitcoin binary distribution not available for platform and architecture"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
binary_url="${url}/${tag}/${tarball_name}"
|
||||
shasums_url="${url}/${tag}/SHA256SUMS.asc"
|
||||
|
||||
download_bitcoind() {
|
||||
|
||||
cd "${root_dir}/bin"
|
||||
|
||||
echo "Downloading bitcoin: ${binary_url}"
|
||||
|
||||
is_curl=true
|
||||
if hash curl 2>/dev/null; then
|
||||
curl --fail -I $binary_url >/dev/null 2>&1
|
||||
else
|
||||
is_curl=false
|
||||
wget --server-response --spider $binary_url >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if test $? -eq 0; then
|
||||
if [ "${is_curl}" = true ]; then
|
||||
curl -L $binary_url > $tarball_name
|
||||
curl -L $shasums_url > SHA256SUMS.asc
|
||||
else
|
||||
wget $binary_url
|
||||
wget $shasums_url
|
||||
fi
|
||||
if test -e "${tarball_name}"; then
|
||||
echo "Unpacking bitcoin distribution"
|
||||
tar -xvzf $tarball_name
|
||||
if test $? -eq 0; then
|
||||
ln -sf "bitcoin-${version}/bin/bitcoind"
|
||||
return;
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "Bitcoin binary distribution could not be downloaded"
|
||||
exit -1
|
||||
}
|
||||
|
||||
verify_download() {
|
||||
echo "Verifying signatures of bitcoin download"
|
||||
gpg --verify "${root_dir}/bin/SHA256SUMS.asc"
|
||||
|
||||
if hash shasum 2>/dev/null; then
|
||||
shasum_cmd="shasum -a 256"
|
||||
else
|
||||
shasum_cmd="sha256sum"
|
||||
fi
|
||||
|
||||
download_sha=$(${shasum_cmd} "${root_dir}/bin/${tarball_name}" | awk '{print $1}')
|
||||
expected_sha=$(cat "${root_dir}/bin/SHA256SUMS.asc" | grep "${tarball_name}" | awk '{print $1}')
|
||||
echo "Checksum (download): ${download_sha}"
|
||||
echo "Checksum (verified): ${expected_sha}"
|
||||
if [ "${download_sha}" != "${expected_sha}" ]; then
|
||||
echo -e "\033[1;31mChecksums did NOT match!\033[0m\n"
|
||||
exit 1
|
||||
else
|
||||
echo -e "\033[1;32mChecksums matched!\033[0m\n"
|
||||
fi
|
||||
}
|
||||
|
||||
download=1
|
||||
verify=0
|
||||
|
||||
if [ "${SKIP_BITCOIN_DOWNLOAD}" = 1 ]; then
|
||||
download=0;
|
||||
fi
|
||||
|
||||
if [ "${VERIFY_BITCOIN_DOWNLOAD}" = 1 ]; then
|
||||
verify=1;
|
||||
fi
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
param="$1"
|
||||
value="$2"
|
||||
|
||||
case $param in
|
||||
--skip-bitcoin-download)
|
||||
download=0
|
||||
;;
|
||||
--verify-bitcoin-download)
|
||||
verify=1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ "${download}" = 1 ]; then
|
||||
download_bitcoind
|
||||
fi
|
||||
|
||||
if [ "${verify}" = 1 ]; then
|
||||
verify_download
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
_mocha -R spec regtest/p2p.js
|
||||
_mocha -R spec regtest/bitcoind.js
|
||||
_mocha -R spec regtest/cluster.js
|
||||
_mocha -R spec regtest/node.js
|
||||
@ -1,104 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var sinon = require('sinon');
|
||||
var Service = require('../lib/service');
|
||||
var BitcoreNode = require('../lib/node');
|
||||
var util = require('util');
|
||||
var should = require('chai').should();
|
||||
var index = require('../lib');
|
||||
var log = index.log;
|
||||
|
||||
var TestService = function(options) {
|
||||
this.node = options.node;
|
||||
};
|
||||
util.inherits(TestService, Service);
|
||||
TestService.dependencies = [];
|
||||
|
||||
TestService.prototype.start = function(callback) {
|
||||
callback();
|
||||
};
|
||||
TestService.prototype.stop = function(callback) {
|
||||
callback();
|
||||
};
|
||||
TestService.prototype.close = function(callback) {
|
||||
callback();
|
||||
};
|
||||
TestService.prototype.getPublishEvents = function() {
|
||||
return [
|
||||
{
|
||||
name: 'test/testEvent',
|
||||
scope: this,
|
||||
subscribe: this.subscribe.bind(this, 'test/testEvent'),
|
||||
unsubscribe: this.unsubscribe.bind(this, 'test/testEvent')
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
TestService.prototype.subscribe = function(name, emitter, params) {
|
||||
emitter.emit(name, params);
|
||||
};
|
||||
|
||||
TestService.prototype.unsubscribe = function(name, emitter) {
|
||||
emitter.emit('unsubscribe');
|
||||
};
|
||||
|
||||
|
||||
describe('Bus Functionality', function() {
|
||||
var sandbox = sinon.sandbox.create();
|
||||
beforeEach(function() {
|
||||
sandbox.stub(log, 'info');
|
||||
});
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should subscribe to testEvent', function(done) {
|
||||
var node = new BitcoreNode({
|
||||
datadir: './',
|
||||
network: 'testnet',
|
||||
port: 8888,
|
||||
services: [
|
||||
{
|
||||
name: 'testService',
|
||||
config: {},
|
||||
module: TestService
|
||||
}
|
||||
]
|
||||
});
|
||||
node.start(function() {
|
||||
var bus = node.openBus();
|
||||
var params = 'somedata';
|
||||
bus.on('test/testEvent', function(data) {
|
||||
data.should.be.equal(params);
|
||||
done();
|
||||
});
|
||||
bus.subscribe('test/testEvent', params);
|
||||
});
|
||||
});
|
||||
|
||||
it('should unsubscribe from a testEvent', function(done) {
|
||||
var node = new BitcoreNode({
|
||||
datadir: './',
|
||||
network: 'testnet',
|
||||
port: 8888,
|
||||
services: [
|
||||
{
|
||||
name: 'testService',
|
||||
config: {},
|
||||
module: TestService
|
||||
}
|
||||
]
|
||||
});
|
||||
node.start(function() {
|
||||
var bus = node.openBus();
|
||||
var params = 'somedata';
|
||||
bus.on('unsubscribe', function() {
|
||||
done();
|
||||
});
|
||||
bus.subscribe('test/testEvent');
|
||||
bus.unsubscribe('test/testEvent');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
131
test/bus.unit.js
131
test/bus.unit.js
@ -1,131 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var Bus = require('../lib/bus');
|
||||
|
||||
describe('Bus', function() {
|
||||
|
||||
describe('#subscribe', function() {
|
||||
it('will call db and services subscribe function with the correct arguments', function() {
|
||||
var subscribeDb = sinon.spy();
|
||||
var subscribeService = sinon.spy();
|
||||
var node = {
|
||||
services: {
|
||||
db: {
|
||||
getPublishEvents: sinon.stub().returns([
|
||||
{
|
||||
name: 'dbtest',
|
||||
scope: this,
|
||||
subscribe: subscribeDb
|
||||
}
|
||||
])
|
||||
},
|
||||
service1: {
|
||||
getPublishEvents: sinon.stub().returns([
|
||||
{
|
||||
name: 'test',
|
||||
scope: this,
|
||||
subscribe: subscribeService,
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
};
|
||||
var bus = new Bus({node: node});
|
||||
bus.subscribe('dbtest', 'a', 'b', 'c');
|
||||
bus.subscribe('test', 'a', 'b', 'c');
|
||||
subscribeService.callCount.should.equal(1);
|
||||
subscribeDb.callCount.should.equal(1);
|
||||
subscribeDb.args[0][0].should.equal(bus);
|
||||
subscribeDb.args[0][1].should.equal('a');
|
||||
subscribeDb.args[0][2].should.equal('b');
|
||||
subscribeDb.args[0][3].should.equal('c');
|
||||
subscribeService.args[0][0].should.equal(bus);
|
||||
subscribeService.args[0][1].should.equal('a');
|
||||
subscribeService.args[0][2].should.equal('b');
|
||||
subscribeService.args[0][3].should.equal('c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#unsubscribe', function() {
|
||||
it('will call db and services unsubscribe function with the correct arguments', function() {
|
||||
var unsubscribeDb = sinon.spy();
|
||||
var unsubscribeService = sinon.spy();
|
||||
var node = {
|
||||
services: {
|
||||
db: {
|
||||
getPublishEvents: sinon.stub().returns([
|
||||
{
|
||||
name: 'dbtest',
|
||||
scope: this,
|
||||
unsubscribe: unsubscribeDb
|
||||
}
|
||||
])
|
||||
},
|
||||
service1: {
|
||||
getPublishEvents: sinon.stub().returns([
|
||||
{
|
||||
name: 'test',
|
||||
scope: this,
|
||||
unsubscribe: unsubscribeService,
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
};
|
||||
var bus = new Bus({node: node});
|
||||
bus.unsubscribe('dbtest', 'a', 'b', 'c');
|
||||
bus.unsubscribe('test', 'a', 'b', 'c');
|
||||
unsubscribeService.callCount.should.equal(1);
|
||||
unsubscribeDb.callCount.should.equal(1);
|
||||
unsubscribeDb.args[0][0].should.equal(bus);
|
||||
unsubscribeDb.args[0][1].should.equal('a');
|
||||
unsubscribeDb.args[0][2].should.equal('b');
|
||||
unsubscribeDb.args[0][3].should.equal('c');
|
||||
unsubscribeService.args[0][0].should.equal(bus);
|
||||
unsubscribeService.args[0][1].should.equal('a');
|
||||
unsubscribeService.args[0][2].should.equal('b');
|
||||
unsubscribeService.args[0][3].should.equal('c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#close', function() {
|
||||
it('will unsubscribe from all events', function() {
|
||||
var unsubscribeDb = sinon.spy();
|
||||
var unsubscribeService = sinon.spy();
|
||||
var node = {
|
||||
services: {
|
||||
db: {
|
||||
getPublishEvents: sinon.stub().returns([
|
||||
{
|
||||
name: 'dbtest',
|
||||
scope: this,
|
||||
unsubscribe: unsubscribeDb
|
||||
}
|
||||
])
|
||||
},
|
||||
service1: {
|
||||
getPublishEvents: sinon.stub().returns([
|
||||
{
|
||||
name: 'test',
|
||||
scope: this,
|
||||
unsubscribe: unsubscribeService
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
};
|
||||
var bus = new Bus({node: node});
|
||||
bus.close();
|
||||
|
||||
unsubscribeDb.callCount.should.equal(1);
|
||||
unsubscribeService.callCount.should.equal(1);
|
||||
unsubscribeDb.args[0].length.should.equal(1);
|
||||
unsubscribeDb.args[0][0].should.equal(bus);
|
||||
unsubscribeService.args[0].length.should.equal(1);
|
||||
unsubscribeService.args[0][0].should.equal(bus);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -1,16 +0,0 @@
|
||||
#testnet=1
|
||||
#irc=0
|
||||
#upnp=0
|
||||
server=1
|
||||
|
||||
whitelist=127.0.0.1
|
||||
|
||||
# listen on different ports
|
||||
port=20000
|
||||
|
||||
rpcallowip=127.0.0.1
|
||||
|
||||
rpcuser=bitcoin
|
||||
rpcpassword=local321
|
||||
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
[
|
||||
{
|
||||
"comment": "sends to tx[1]",
|
||||
"hex":"0100000001dee8f4266e83072e0ad258125cc5a42ac25d2d2c73e6e2e873413b3939af1605000000006b483045022100ae987d056f81d2c982b71b0406f2374c1958b24bd289d77371347e275d2a62c002205148b17173be18af4e1e73ce2b0fd600734ea77087754bdba5dc7d645b01880a01210226ab3b46f85bf32f63778c680e16ef8b3fcb51d638a7980d651bfaeae6c17752ffffffff0170820300000000001976a9142baf68e3681df183375a4f4c10306de9a5c6cc7788ac00000000"
|
||||
},
|
||||
{
|
||||
"comment": "spends from tx[0] (valid)",
|
||||
"hex":"0100000001f77c71cf8c272d22471f054cae7fb48561ebcf004b8ec8f9f65cd87af82a2944000000006a47304402203c2bc91a170facdc5ef4b5b94c413bc7a10f65e09b326d205f070b17aa94d67102205b684111af2a20171eb65db73e6c73f9e77e6e6f739e050bc052ed6ecc9feb4a01210365d8756a4f3fc738105cfab8d80a85189bdb4db5af83374e645b79e2aadd976effffffff01605b0300000000001976a9149e84d1295471958e5ffccd8d36a57bd5d220f8ed88ac00000000"
|
||||
},
|
||||
{
|
||||
"comment": "spends from tx[0] (missing signature)",
|
||||
"hex":"0100000001f77c71cf8c272d22471f054cae7fb48561ebcf004b8ec8f9f65cd87af82a29440000000000ffffffff01605b0300000000001976a9149e84d1295471958e5ffccd8d36a57bd5d220f8ed88ac00000000"
|
||||
}
|
||||
]
|
||||
@ -1,23 +0,0 @@
|
||||
#testnet=1
|
||||
#irc=0
|
||||
upnp=0
|
||||
server=1
|
||||
|
||||
whitelist=127.0.0.1
|
||||
txindex=1
|
||||
addressindex=1
|
||||
timestampindex=1
|
||||
spentindex=1
|
||||
dbcache=8192
|
||||
checkblocks=144
|
||||
maxuploadtarget=1024
|
||||
zmqpubrawtx=tcp://127.0.0.1:28332
|
||||
zmqpubhashblock=tcp://127.0.0.1:28332
|
||||
|
||||
port=20000
|
||||
rpcport=50001
|
||||
|
||||
rpcallowip=127.0.0.1
|
||||
|
||||
rpcuser=bitcoin
|
||||
rpcpassword=local321
|
||||
@ -1,12 +0,0 @@
|
||||
server=1
|
||||
whitelist=127.0.0.1
|
||||
txindex=1
|
||||
addressindex=1
|
||||
timestampindex=1
|
||||
spentindex=1
|
||||
zmqpubrawtx=tcp://127.0.0.1:28332
|
||||
zmqpubhashblock=tcp://127.0.0.1:28332
|
||||
rpcallowip=127.0.0.1
|
||||
rpcuser=bitcoin
|
||||
rpcpassword=local321
|
||||
uacomment=bitcore
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,3 +0,0 @@
|
||||
[
|
||||
"010000000ec9e1d96d51f7d0a5b726184cda744207e51a596f13b564109de6ffc0653055cf000000006a4730440220263d48a2f4c3a2aa6032f96253c853531131171d8ae3d30586a45de7ba7c4006022077db4b39926877939baf59e8d357effe7674e7b12ad986c0a4cd99f2b7acafca012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff0c16800642bdf90cbbd340be2e801bee9b907db6d59dc4c7cb269358c1e2593a000000006a4730440220085e39cb3a948559c1b1c88ba635eeef37a767322c1e4d96556e998302acb5dc0220421c03de78121692c538a9bc85a58fbe796384541fe87b6e91c39404318c390d012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff559fde3733e950db9e3faea1a27872047a5c8bc14e8e6ac4a7b4f7b5f90c42c2000000006b483045022100a46a109a5acfc34b13c591ab69392e2dc2c3ea12d0900c5f7a539ea888e57ae0022015372bad56d63c6d08a5e66e0c9a63b2fc8dce245aa765e37dac06acb84c18d501210207e2a01d4a334c2d7c9ebacc5ea8a0d4b86fd54599b1aebe125d9e664be012c2ffffffff92760c236f6f18751c4ecab1dfcfebac7358a431b31bcd72b0a09d336233bdce000000006a47304402200857fa82e5b287c6ed5624cbc4fcd068a756a7a9ef785a73ce4e51b15a8aa34b022042cbc6f478b711539c6345d0b05d4bc9a9b5c34b2e4d25f234cf6784ff2eed19012103cab1f64f3d5f20a3f4a070694e6f93f5248b502b3842e369d81f05d07dec01e3ffffffff158669557e8a0b71288df37626601e636c5a8b3797f7f3357590313e8efe790a000000006b48304502210085e62cb95066540730b74aeae133277e511b5bf79de8c0ad60e784638e681ddf022043b456285569e0da133f527412218893f05a781d6f77f8cddd12eb90bfdc5937012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffc1f9f57f1eda20f3b45669b3f0d1eae73b410ddf2b4fc1cfe10051a6051eff68000000006a47304402200fbe15c73446309040f4264567d0e8cc46691cf5d0626c443fc2dde716211e5402207f84e68e273755d140f346e029213dbc42b65cfb1e2ac41f72402c7ff45feffc012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff39fa27e16c540a9bb796e65d4ac624fc33878e522461d6955764bf7b83c03ce8000000006a4730440220131e6aed76da389f21dfd7cd271fad73c5c533b1c18fbfabd6799a0d0e7dc80602205c38855bea0f1dfbbb608bc1b56c75b91a39980de121ffd9f880b522d412d821012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff375fb7ae26eb637ccc007a669b9f50ed10fa53127504b80e0fd2be34c50becd7000000006b483045022100f0a9e585aa3113eae4bfb204f38f96e60dc613c04313aae931b677e4d8d7081d022014664874859f3d47447c6c0f257c28c74e8fdaedd5f781d752f3a4b651d3d179012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff6f9d6a3c847e6112bb920424ca0d8d6e0956b128acb206e8fb58b2d2f2d7d46b000000006a4730440220736b198484cf5226616a539146e037a93cc75963885eefe58fc29a7be8123c750220619a456c0fe7437ec67c642d88e890344fc1c51a7b3cfc0ae283d61d0f176c5e012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff3cccbd8090d60fcf28064333cf2f51ef0f838ba5e26a5e0f38807ee16d38a649000000006b483045022100e1ed25e9365e596d4fc3cbf278490d8ea765c4266c55f19311cf5da33f8d00750220361888a1738ebba827c0c947690b5f2a5f20e9f1be8956c3a34a4ba03f9e60f5012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff7f4d60a2e961490aa465a7df461bf09b151bdc0c162f3bef0c1cbed6160d02c7000000006a47304402204e79b15a1db9a356f00dc9f2d559e31561cad1343ba5809a65b52bd868e3963e022055b9326ed5de9aa9970ec67a2ebf1a9dbf9ee513b64bd13837c87320bb4d6947012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffe63b9370ba49a129e071750cbb300128015fdd90d7399f9c4e44934eabbaa2f7000000006b483045022100b9ceb2e376c0334d45bf08bfeb06dc250e7cb01d3a08e2fb3506388683552417022024c7c5bda385b904cca691fb6e1ad8c5eba5858a88a2112cb824dca72793b7a7012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffffc78b96fddededb6cbc1dff9de51f2743fd42e91de2506794b121928af4729528000000006a47304402201f05caddee5a0ff590b27c4ce25be1cbbeb45dc39679a1b8b0e10b1a378d84bc02203e31b01e14d891e9809c43a4df54494c626c5e47eaeeeb99ab4e02bd73c3d6cd012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff30093916240e981a77cb869924fa0c38a894c24b1a6e7d26b117bb9caa7d5bbe000000006a4730440220483f297379daacee14babbf929708a861a991373bca3ed4eef240e2c156a162602205f1e93e375a897c6a9ddc3dc616ccf14137096ebd7888040e1053a769d21b945012103e9100732bb534bea2f6d3b971914ec8403557306600c01c5ce63ec185737c732ffffffff022897d411000000001976a91415354ee1828ed12f243f80dcb92c3a8205285f0188ac3be68c02000000001976a9143ecf8ff79932c3de33829a001236985872d10be188ac00000000"
|
||||
]
|
||||
@ -6,6 +6,7 @@ var should = chai.should();
|
||||
var Logger = require('../lib/logger');
|
||||
|
||||
describe('Logger', function() {
|
||||
process.env.BITCORE_ENV = 'debug';
|
||||
var sandbox = sinon.sandbox.create();
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
@ -31,10 +32,10 @@ describe('Logger', function() {
|
||||
it('will log with formatting', function() {
|
||||
var logger = new Logger({formatting: true});
|
||||
|
||||
sandbox.stub(console, 'info');
|
||||
sandbox.stub(console, 'log');
|
||||
logger.info('Test info log');
|
||||
console.info.callCount.should.equal(1);
|
||||
console.info.restore();
|
||||
console.log.callCount.should.equal(1);
|
||||
console.log.restore();
|
||||
|
||||
sandbox.stub(console, 'error');
|
||||
logger.error(new Error('Test error log'));
|
||||
@ -46,20 +47,20 @@ describe('Logger', function() {
|
||||
console.log.callCount.should.equal(1);
|
||||
console.log.restore();
|
||||
|
||||
sandbox.stub(console, 'warn');
|
||||
sandbox.stub(console, 'log');
|
||||
logger.warn('Test warn log');
|
||||
console.warn.callCount.should.equal(1);
|
||||
console.warn.restore();
|
||||
console.log.callCount.should.equal(1);
|
||||
console.log.restore();
|
||||
});
|
||||
|
||||
it('will log without formatting', function() {
|
||||
var logger = new Logger({formatting: false});
|
||||
|
||||
sandbox.stub(console, 'info');
|
||||
sandbox.stub(console, 'log');
|
||||
logger.info('Test info log');
|
||||
console.info.callCount.should.equal(1);
|
||||
should.not.exist(console.info.args[0][0].match(/^\[/));
|
||||
console.info.restore();
|
||||
console.log.callCount.should.equal(1);
|
||||
should.not.exist(console.log.args[0][0].match(/^\[/));
|
||||
console.log.restore();
|
||||
|
||||
sandbox.stub(console, 'error');
|
||||
logger.error(new Error('Test error log'));
|
||||
@ -73,11 +74,11 @@ describe('Logger', function() {
|
||||
should.equal(console.log.args[0][0].match(/^\[/), null);
|
||||
console.log.restore();
|
||||
|
||||
sandbox.stub(console, 'warn');
|
||||
sandbox.stub(console, 'log');
|
||||
logger.warn('Test warn log');
|
||||
console.warn.callCount.should.equal(1);
|
||||
should.equal(console.warn.args[0][0].match(/^\[/), null);
|
||||
console.warn.restore();
|
||||
console.log.callCount.should.equal(1);
|
||||
should.equal(console.log.args[0][0].match(/^\[/), null);
|
||||
console.log.restore();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -2,15 +2,13 @@
|
||||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var Networks = bitcore.Networks;
|
||||
var proxyquire = require('proxyquire');
|
||||
var util = require('util');
|
||||
var BaseService = require('../lib/service');
|
||||
var index = require('../lib');
|
||||
var log = index.log;
|
||||
|
||||
describe('Bitcore Node', function() {
|
||||
describe('Node', function() {
|
||||
|
||||
var baseConfig = {};
|
||||
|
||||
@ -22,10 +20,6 @@ describe('Bitcore Node', function() {
|
||||
Node.prototype._initialize = sinon.spy();
|
||||
});
|
||||
|
||||
after(function() {
|
||||
Networks.disableRegtest();
|
||||
});
|
||||
|
||||
describe('@constructor', function() {
|
||||
var TestService;
|
||||
before(function() {
|
||||
@ -34,12 +28,13 @@ describe('Bitcore Node', function() {
|
||||
});
|
||||
it('will set properties', function() {
|
||||
var config = {
|
||||
network: 'testnet',
|
||||
services: [
|
||||
{
|
||||
name: 'test1',
|
||||
module: TestService
|
||||
}
|
||||
],
|
||||
]
|
||||
};
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype.start = sinon.spy();
|
||||
@ -47,12 +42,12 @@ describe('Bitcore Node', function() {
|
||||
node._unloadedServices.length.should.equal(1);
|
||||
node._unloadedServices[0].name.should.equal('test1');
|
||||
node._unloadedServices[0].module.should.equal(TestService);
|
||||
node.network.should.equal(Networks.defaultNetwork);
|
||||
node.network.should.equal('testnet');
|
||||
var node2 = TestNode(config);
|
||||
node2._unloadedServices.length.should.equal(1);
|
||||
node2._unloadedServices[0].name.should.equal('test1');
|
||||
node2._unloadedServices[0].module.should.equal(TestService);
|
||||
node2.network.should.equal(Networks.defaultNetwork);
|
||||
node2.network.should.equal('testnet');
|
||||
});
|
||||
it('will set network to testnet', function() {
|
||||
var config = {
|
||||
@ -67,7 +62,7 @@ describe('Bitcore Node', function() {
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype.start = sinon.spy();
|
||||
var node = new TestNode(config);
|
||||
node.network.should.equal(Networks.testnet);
|
||||
node.network.should.equal('testnet');
|
||||
});
|
||||
it('will set network to regtest', function() {
|
||||
var config = {
|
||||
@ -82,9 +77,7 @@ describe('Bitcore Node', function() {
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode.prototype.start = sinon.spy();
|
||||
var node = new TestNode(config);
|
||||
var regtest = Networks.get('regtest');
|
||||
should.exist(regtest);
|
||||
node.network.should.equal(regtest);
|
||||
node.network.should.equal('regtest');
|
||||
});
|
||||
it('will be able to disable log formatting', function() {
|
||||
var config = {
|
||||
@ -97,11 +90,12 @@ describe('Bitcore Node', function() {
|
||||
],
|
||||
formatLogs: false
|
||||
};
|
||||
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
var node = new TestNode(config);
|
||||
node.log.formatting.should.equal(false);
|
||||
|
||||
var TestNode = proxyquire('../lib/node', {});
|
||||
TestNode = proxyquire('../lib/node', {});
|
||||
config.formatLogs = true;
|
||||
var node2 = new TestNode(config);
|
||||
node2.log.formatting.should.equal(true);
|
||||
@ -189,7 +183,7 @@ describe('Bitcore Node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getServiceOrder', function() {
|
||||
describe('#_getServiceOrder', function() {
|
||||
it('should return the services in the correct order', function() {
|
||||
var node = new Node(baseConfig);
|
||||
node._unloadedServices = [
|
||||
@ -218,7 +212,7 @@ describe('Bitcore Node', function() {
|
||||
}
|
||||
}
|
||||
];
|
||||
var order = node.getServiceOrder();
|
||||
var order = node._getServiceOrder(node._unloadedServices);
|
||||
order[0].name.should.equal('daemon');
|
||||
order[1].name.should.equal('p2p');
|
||||
order[2].name.should.equal('db');
|
||||
@ -336,7 +330,7 @@ describe('Bitcore Node', function() {
|
||||
];
|
||||
};
|
||||
|
||||
node.getServiceOrder = sinon.stub().returns([
|
||||
node._getServiceOrder = sinon.stub().returns([
|
||||
{
|
||||
name: 'test1',
|
||||
module: TestService,
|
||||
@ -379,7 +373,7 @@ describe('Bitcore Node', function() {
|
||||
];
|
||||
};
|
||||
|
||||
node.getServiceOrder = sinon.stub().returns([
|
||||
node._getServiceOrder = sinon.stub().returns([
|
||||
{
|
||||
name: 'test',
|
||||
module: TestService,
|
||||
@ -399,6 +393,7 @@ describe('Bitcore Node', function() {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('will handle service with getAPIMethods undefined', function(done) {
|
||||
var node = new Node(baseConfig);
|
||||
|
||||
@ -407,7 +402,7 @@ describe('Bitcore Node', function() {
|
||||
TestService.prototype.start = sinon.stub().callsArg(0);
|
||||
TestService.prototype.getData = function() {};
|
||||
|
||||
node.getServiceOrder = sinon.stub().returns([
|
||||
node._getServiceOrder = sinon.stub().returns([
|
||||
{
|
||||
name: 'test',
|
||||
module: TestService,
|
||||
@ -423,30 +418,6 @@ describe('Bitcore Node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getNetworkName', function() {
|
||||
afterEach(function() {
|
||||
bitcore.Networks.disableRegtest();
|
||||
});
|
||||
it('it will return the network name for livenet', function() {
|
||||
var node = new Node(baseConfig);
|
||||
node.getNetworkName().should.equal('livenet');
|
||||
});
|
||||
it('it will return the network name for testnet', function() {
|
||||
var baseConfig = {
|
||||
network: 'testnet'
|
||||
};
|
||||
var node = new Node(baseConfig);
|
||||
node.getNetworkName().should.equal('testnet');
|
||||
});
|
||||
it('it will return the network for regtest', function() {
|
||||
var baseConfig = {
|
||||
network: 'regtest'
|
||||
};
|
||||
var node = new Node(baseConfig);
|
||||
node.getNetworkName().should.equal('regtest');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
var sandbox = sinon.sandbox.create();
|
||||
beforeEach(function() {
|
||||
@ -471,7 +442,7 @@ describe('Bitcore Node', function() {
|
||||
};
|
||||
node.test2 = {};
|
||||
node.test2.stop = sinon.stub().callsArg(0);
|
||||
node.getServiceOrder = sinon.stub().returns([
|
||||
node._getServiceOrder = sinon.stub().returns([
|
||||
{
|
||||
name: 'test1',
|
||||
module: TestService
|
||||
|
||||
30
test/regtest/comms.txt
Normal file
30
test/regtest/comms.txt
Normal file
@ -0,0 +1,30 @@
|
||||
[2017-08-16T13:44:43.245Z] info: Connecting to p2p network.
|
||||
client sending: magic:: 0b110907 command:: 76657273696f6e0000000000 length:: 65000000 checksum:: 735475bc message:: 7111010001000000000000004b4c945900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e2b8235df4c158a0f2f626974636f72653a312e312e322f0000000001
|
||||
server sending: magic:: 0b110907 command:: 76657273696f6e0000000000 length:: 67000000 checksum:: 46b5c9ae
|
||||
client sending: magic:: 0b110907 command:: 76657261636b000000000000 length:: 00000000 checksum:: 5df6e0e2 message::
|
||||
server sending: magic:: 0b110907 command:: 76657261636b000000000000 length:: 00000000 checksum:: 5df6e0e2
|
||||
[2017-08-16T13:44:43.261Z] info: Connected to peer: 192.168.3.5, network: regtest, version: 70015, subversion: /Satoshi:0.14.99/, status: ready, port: 18333, best height: 1178711
|
||||
[2017-08-16T13:44:43.262Z] info: Header Service: Gathering: 2001 header(s) from the peer-to-peer network.
|
||||
[2017-08-16T13:44:43.262Z] info: Header Service: download progress: 1176710/1178711 (99.83%)
|
||||
client sending: magic:: 0b110907 command:: 676574686561646572730000 length:: 45000000 checksum:: 857caf8b message:: 7111010001145ed5b8587723d506f208c0aaf9c4d628bcba4bacd1d30f90270000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
server sending: magic:: command:: length:: checksum::
|
||||
server sending: magic:: 0b110907 command:: 616c65727400000000000000 length:: a8000000 checksum:: 1bf9aaea
|
||||
server sending: magic:: 0b110907 command:: 70696e670000000000000000 length:: 08000000 checksum:: 7c640b03
|
||||
client sending: magic:: 0b110907 command:: 706f6e670000000000000000 length:: 08000000 checksum:: 7c640b03 message:: e79ac440be90a476
|
||||
server sending: magic:: 0b110907 command:: 676574686561646572730000 length:: 25040000 checksum:: 552dc886
|
||||
server sending: magic:: command:: length:: checksum::
|
||||
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: d3780200 checksum:: 29213586
|
||||
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: d3780200 checksum:: 29213586
|
||||
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: d3780200 checksum:: 29213586
|
||||
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: d3780200 checksum:: 29213586
|
||||
server sending: magic:: command:: length:: checksum::
|
||||
[2017-08-16T13:44:43.411Z] info: Header Service: download progress: 1178710/1178711 (100.00%)
|
||||
client sending: magic:: 0b110907 command:: 676574686561646572730000 length:: 45000000 checksum:: cd33b9da message:: 71110100019f1309c60de611c5cdec7e0b24fb00da0d16fb706f1ae21a500f0000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: 52000000 checksum:: a4022af1
|
||||
server sending: magic:: 0b110907 command:: 686561646572730000000000 length:: 52000000 checksum:: a4022af1
|
||||
server sending: magic:: command:: length:: checksum::
|
||||
[2017-08-16T13:44:43.419Z] info: localhost-header subscribe: p2p/block total: 1
|
||||
[2017-08-16T13:44:43.419Z] info: Header Service: emitting headers to block service.
|
||||
[2017-08-16T13:44:43.419Z] info: Block Service: Gathering: 0 block(s) from the peer-to-peer network.
|
||||
[2017-08-16T13:44:43.419Z] info: Block Service: The best block hash is: 00000000000004842ea914123b8010541a41174a11ba62b244d0aec19840467c at height: 1178711
|
||||
|
||||
10
test/regtest/data/blocks.json
Normal file
10
test/regtest/data/blocks.json
Normal file
@ -0,0 +1,10 @@
|
||||
[
|
||||
"0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0520bf1be2e869376908f7c637214f9f0f9f4fd86c819a8c98a3c5f8d70a65f805ba9559ffff7f20000000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
|
||||
"00000020bd5725c4bc8fb66032523ca18eb2c809ca1935ab35baf5a1bbaf60ef9a616b2b373d5a12492652b1d22c28ce5c6eb6b22f03c69db1eae9a368e1b544147583eb06ba9559ffff7f20030000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03520101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
|
||||
"00000020af97cfd581a5f0da7925c346c371f6c58131a6b00a95060562f90607aa111e5f97c7b866d2503ee4982569a486f6e25023e72bef588bdef60a06fc740e50001906ba9559ffff7f20020000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03530101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
|
||||
"000000201c9d9eb8064b9508b3cd10de5927cc47347a6899b66946ecc22d52b04c2f3c5f99435c3a200a2419695565432e3115a5ff0d57ed1975b9efe0c7ad1a709a6a4e07ba9559ffff7f20000000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03540101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
|
||||
"00000020de37a9c9fbb1dc3ea5ca9e148e5310d639649814b45a424afe1d58a18db5d8327b529f7d58238330d55b82228ea14abcab965319de7e59c6534758c91c137fcb07ba9559ffff7f20020000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03550101ffffffff0200f2052a01000000232103e91a98edf4e457b5d5aa1e50e35ce6afb67bf7a9ff98b5c36dabbe994b080205ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
|
||||
"00000020e3aa93afbba267bec962a7a38bd064478e5c082e9a73e434b20544950ad8395b3b4a71816a4ce7973d0bcc6a583441fca260f71a175bd2887a9e3e40e4badcd355bb9559ffff7f20000000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03560101ffffffff0200f2052a010000002321023886024ea5984e57b35c3b339f5aee097819ac55235e4fd5822a6ad0a4de1b55ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000",
|
||||
"000000201a3c951a20b5d603144ce060c86e95fed1869524e66acfc46bdf08d96f6642094916aa5b965b0016fb8ba8b58e99f6b3edbe1a844aa7948adaccf7f28f08f914b9cb9559ffff7f20030000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03570101ffffffff0200f2052a01000000232102a8ef631320be3e6203329acba88fe0a663c19d59fee8240592dc2a32553a4159ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000"
|
||||
|
||||
]
|
||||
4
test/regtest/data/blocks_reorg.json
Normal file
4
test/regtest/data/blocks_reorg.json
Normal file
@ -0,0 +1,4 @@
|
||||
[
|
||||
"000000201a3c951a20b5d603144ce060c86e95fed1869524e66acfc46bdf08d96f664209b4b1c32ec485f4ad27c5402a1b16a0b1135364b7c9b0dcf4276f9fa3fd215d1b08cc9559ffff7f20000000000102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03570101ffffffff0200f2052a01000000232102a5566542d1f0f202541d98755628a41dcd4416b50db820e2b04d5ecb0bd02b73ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900000000"
|
||||
]
|
||||
|
||||
276
test/regtest/reorg.js
Normal file
276
test/regtest/reorg.js
Normal file
@ -0,0 +1,276 @@
|
||||
'use strict';
|
||||
|
||||
var expect = require('chai').expect;
|
||||
var net = require('net');
|
||||
var spawn = require('child_process').spawn;
|
||||
var path = require('path');
|
||||
var rimraf = require('rimraf');
|
||||
var mkdirp = require('mkdirp');
|
||||
var fs = require('fs');
|
||||
var p2p = require('bitcore-p2p');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var Networks = bitcore.Networks;
|
||||
var Header = bitcore.BlockHeader;
|
||||
var Block = bitcore.Block;
|
||||
var BcoinBlock = require('bcoin').block;
|
||||
var http = require('http');
|
||||
|
||||
Networks.enableRegtest();
|
||||
var messages = new p2p.Messages({ network: Networks.get('regtest'), Block: BcoinBlock });
|
||||
var server;
|
||||
var rawBlocks = require('./data/blocks.json');
|
||||
var rawReorgBlocks = require('./data/blocks_reorg.json')[0];
|
||||
|
||||
var reorgBlock = BcoinBlock.fromRaw(rawReorgBlocks, 'hex');
|
||||
|
||||
var blocks = rawBlocks.map(function(rawBlock) {
|
||||
return new Block(new Buffer(rawBlock, 'hex'));
|
||||
});
|
||||
|
||||
var headers = blocks.map(function(block) {
|
||||
return block.header;
|
||||
});
|
||||
|
||||
var debug = true;
|
||||
var bitcoreDataDir = '/tmp/bitcore';
|
||||
|
||||
var bitcore = {
|
||||
configFile: {
|
||||
file: bitcoreDataDir + '/bitcore-node.json',
|
||||
conf: {
|
||||
network: 'regtest',
|
||||
port: 53001,
|
||||
datadir: bitcoreDataDir,
|
||||
services: [
|
||||
'p2p',
|
||||
'db',
|
||||
'header',
|
||||
'block',
|
||||
'address',
|
||||
'transaction',
|
||||
'mempool',
|
||||
'web',
|
||||
'insight-api',
|
||||
'fee',
|
||||
'timestamp'
|
||||
],
|
||||
servicesConfig: {
|
||||
'p2p': {
|
||||
'peers': [
|
||||
{ 'ip': { 'v4': '127.0.0.1' }, port: 18444 }
|
||||
]
|
||||
},
|
||||
'insight-api': {
|
||||
'routePrefix': 'api'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
httpOpts: {
|
||||
protocol: 'http:',
|
||||
hostname: 'localhost',
|
||||
port: 53001,
|
||||
},
|
||||
opts: { cwd: bitcoreDataDir },
|
||||
datadir: bitcoreDataDir,
|
||||
exec: path.resolve(__dirname, '../../bin/bitcore-node'),
|
||||
args: ['start'],
|
||||
process: null
|
||||
};
|
||||
|
||||
|
||||
var blockIndex = 0;
|
||||
var tcpSocket;
|
||||
|
||||
var startFakeNode = function() {
|
||||
server = net.createServer(function(socket) {
|
||||
|
||||
tcpSocket = socket;
|
||||
socket.on('end', function() {
|
||||
console.log('bitcore-node has ended the connection');
|
||||
});
|
||||
|
||||
socket.on('data', function(data) {
|
||||
|
||||
var command = data.slice(4, 16).toString('hex');
|
||||
var message;
|
||||
|
||||
if (command === '76657273696f6e0000000000') { //version
|
||||
message = messages.Version();
|
||||
}
|
||||
|
||||
if (command === '76657261636b000000000000') { //verack
|
||||
message = messages.VerAck();
|
||||
}
|
||||
|
||||
if (command === '676574686561646572730000') { //getheaders
|
||||
message = messages.Headers(headers, { BlockHeader: Header });
|
||||
}
|
||||
|
||||
if (command === '676574626c6f636b73000000') { //getblocks
|
||||
var block = blocks[blockIndex];
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
var blockHash = block.hash;
|
||||
var inv = p2p.Inventory.forBlock(blockHash);
|
||||
message = messages.Inventory([inv]);
|
||||
}
|
||||
|
||||
if (command === '676574646174610000000000') { //getdata
|
||||
var raw = rawBlocks[blockIndex++];
|
||||
var blk = BcoinBlock.fromRaw(raw, 'hex');
|
||||
message = messages.Block(blk, { Block: BcoinBlock });
|
||||
}
|
||||
|
||||
if (message) {
|
||||
socket.write(message.toBuffer());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
socket.pipe(socket);
|
||||
});
|
||||
|
||||
server.listen(18444, '127.0.0.1');
|
||||
};
|
||||
|
||||
|
||||
var shutdownFakeNode = function() {
|
||||
server.close();
|
||||
};
|
||||
|
||||
var shutdownBitcore = function(callback) {
|
||||
if (bitcore.process) {
|
||||
bitcore.process.kill();
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
var startBitcore = function(callback) {
|
||||
|
||||
rimraf(bitcoreDataDir, function(err) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
mkdirp(bitcoreDataDir, function(err) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fs.writeFileSync(bitcore.configFile.file, JSON.stringify(bitcore.configFile.conf));
|
||||
|
||||
var args = bitcore.args;
|
||||
bitcore.process = spawn(bitcore.exec, args, bitcore.opts);
|
||||
|
||||
bitcore.process.stdout.on('data', function(data) {
|
||||
|
||||
if (debug) {
|
||||
process.stdout.write(data.toString());
|
||||
}
|
||||
|
||||
});
|
||||
bitcore.process.stderr.on('data', function(data) {
|
||||
|
||||
if (debug) {
|
||||
process.stderr.write(data.toString());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
|
||||
describe('Reorg', function() {
|
||||
// 1. spin up bitcore-node and have it connect to our custom tcp socket
|
||||
// 2. feed it a few headers
|
||||
// 3. feed it a few blocks
|
||||
// 4. feed it a block that reorgs
|
||||
|
||||
this.timeout(60000);
|
||||
|
||||
before(function(done) {
|
||||
startFakeNode();
|
||||
startBitcore(done);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
shutdownFakeNode();
|
||||
shutdownBitcore(done);
|
||||
});
|
||||
|
||||
it('should reorg correctly when already synced', function(done) {
|
||||
|
||||
// at this point we have a fully synced chain at height 7....
|
||||
// we now want to send a new block number 7 whose prev hash is block 6 (it should be block 7)
|
||||
// we then should reorg back to block 6 then back up to the new block 7
|
||||
|
||||
setTimeout(function() {
|
||||
|
||||
console.log('From Test: reorging to block: ' + reorgBlock.rhash());
|
||||
|
||||
// send the reorg block
|
||||
rawBlocks.push(rawReorgBlocks);
|
||||
var blockHash = reorgBlock.rhash();
|
||||
var inv = p2p.Inventory.forBlock(blockHash);
|
||||
var msg = messages.Inventory([inv]);
|
||||
tcpSocket.write(msg.toBuffer());
|
||||
|
||||
// wait 2 secs until the reorg happens, if it takes any longer the test ought to fail anyway
|
||||
setTimeout(function() {
|
||||
var error;
|
||||
var request = http.request('http://localhost:53001/api/block/' + reorgBlock.rhash(), function(res) {
|
||||
|
||||
if (res.statusCode !== 200 && res.statusCode !== 201) {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
return done('Error from bitcore-node webserver: ' + res.statusCode);
|
||||
}
|
||||
|
||||
var resError;
|
||||
var resData = '';
|
||||
|
||||
res.on('error', function(e) {
|
||||
resError = e;
|
||||
});
|
||||
|
||||
res.on('data', function(data) {
|
||||
resData += data;
|
||||
});
|
||||
|
||||
res.on('end', function() {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
var data = JSON.parse(resData);
|
||||
expect(data.height).to.equal(7);
|
||||
expect(data.hash).to.equal(reorgBlock.rhash());
|
||||
done(resError, resData);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
request.on('error', function(e) {
|
||||
error = e;
|
||||
done(error);
|
||||
});
|
||||
|
||||
request.write('');
|
||||
request.end();
|
||||
}, 2000);
|
||||
}, 2000);
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var path = require('path');
|
||||
var defaultBaseConfig = require('../../lib/scaffold/default-base-config');
|
||||
|
||||
describe('#defaultBaseConfig', function() {
|
||||
it('will return expected configuration', function() {
|
||||
var cwd = process.cwd();
|
||||
var home = process.env.HOME;
|
||||
var info = defaultBaseConfig();
|
||||
info.path.should.equal(cwd);
|
||||
info.config.network.should.equal('livenet');
|
||||
info.config.port.should.equal(3001);
|
||||
info.config.services.should.deep.equal(['bitcoind', 'web']);
|
||||
var bitcoind = info.config.servicesConfig.bitcoind;
|
||||
bitcoind.spawn.datadir.should.equal(home + '/.bitcoin');
|
||||
bitcoind.spawn.exec.should.equal(path.resolve(__dirname, '../../bin/bitcoind'));
|
||||
});
|
||||
it('be able to specify a network', function() {
|
||||
var info = defaultBaseConfig({network: 'testnet'});
|
||||
info.config.network.should.equal('testnet');
|
||||
});
|
||||
it('be able to specify a datadir', function() {
|
||||
var info = defaultBaseConfig({datadir: './data2', network: 'testnet'});
|
||||
info.config.servicesConfig.bitcoind.spawn.datadir.should.equal('./data2');
|
||||
});
|
||||
});
|
||||
@ -1,106 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var proxyquire = require('proxyquire');
|
||||
|
||||
describe('#defaultConfig', function() {
|
||||
var expectedExecPath = path.resolve(__dirname, '../../bin/bitcoind');
|
||||
|
||||
it('will return expected configuration', function() {
|
||||
var config = JSON.stringify({
|
||||
network: 'livenet',
|
||||
port: 3001,
|
||||
services: [
|
||||
'bitcoind',
|
||||
'web'
|
||||
],
|
||||
servicesConfig: {
|
||||
bitcoind: {
|
||||
spawn: {
|
||||
datadir: process.env.HOME + '/.bitcore/data',
|
||||
exec: expectedExecPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null, 2);
|
||||
var defaultConfig = proxyquire('../../lib/scaffold/default-config', {
|
||||
fs: {
|
||||
existsSync: sinon.stub().returns(false),
|
||||
writeFileSync: function(path, data) {
|
||||
path.should.equal(process.env.HOME + '/.bitcore/bitcore-node.json');
|
||||
data.should.equal(config);
|
||||
},
|
||||
readFileSync: function() {
|
||||
return config;
|
||||
}
|
||||
},
|
||||
mkdirp: {
|
||||
sync: sinon.stub()
|
||||
}
|
||||
});
|
||||
var home = process.env.HOME;
|
||||
var info = defaultConfig();
|
||||
info.path.should.equal(home + '/.bitcore');
|
||||
info.config.network.should.equal('livenet');
|
||||
info.config.port.should.equal(3001);
|
||||
info.config.services.should.deep.equal(['bitcoind', 'web']);
|
||||
var bitcoind = info.config.servicesConfig.bitcoind;
|
||||
should.exist(bitcoind);
|
||||
bitcoind.spawn.datadir.should.equal(home + '/.bitcore/data');
|
||||
bitcoind.spawn.exec.should.equal(expectedExecPath);
|
||||
});
|
||||
it('will include additional services', function() {
|
||||
var config = JSON.stringify({
|
||||
network: 'livenet',
|
||||
port: 3001,
|
||||
services: [
|
||||
'bitcoind',
|
||||
'web',
|
||||
'insight-api',
|
||||
'insight-ui'
|
||||
],
|
||||
servicesConfig: {
|
||||
bitcoind: {
|
||||
spawn: {
|
||||
datadir: process.env.HOME + '/.bitcore/data',
|
||||
exec: expectedExecPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null, 2);
|
||||
var defaultConfig = proxyquire('../../lib/scaffold/default-config', {
|
||||
fs: {
|
||||
existsSync: sinon.stub().returns(false),
|
||||
writeFileSync: function(path, data) {
|
||||
path.should.equal(process.env.HOME + '/.bitcore/bitcore-node.json');
|
||||
data.should.equal(config);
|
||||
},
|
||||
readFileSync: function() {
|
||||
return config;
|
||||
}
|
||||
},
|
||||
mkdirp: {
|
||||
sync: sinon.stub()
|
||||
}
|
||||
});
|
||||
var home = process.env.HOME;
|
||||
var info = defaultConfig({
|
||||
additionalServices: ['insight-api', 'insight-ui']
|
||||
});
|
||||
info.path.should.equal(home + '/.bitcore');
|
||||
info.config.network.should.equal('livenet');
|
||||
info.config.port.should.equal(3001);
|
||||
info.config.services.should.deep.equal([
|
||||
'bitcoind',
|
||||
'web',
|
||||
'insight-api',
|
||||
'insight-ui'
|
||||
]);
|
||||
var bitcoind = info.config.servicesConfig.bitcoind;
|
||||
should.exist(bitcoind);
|
||||
bitcoind.spawn.datadir.should.equal(home + '/.bitcore/data');
|
||||
bitcoind.spawn.exec.should.equal(expectedExecPath);
|
||||
});
|
||||
});
|
||||
@ -1,79 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var mkdirp = require('mkdirp');
|
||||
var rimraf = require('rimraf');
|
||||
|
||||
var findConfig = require('../../lib/scaffold/find-config');
|
||||
|
||||
describe('#findConfig', function() {
|
||||
|
||||
var testDir = path.resolve(__dirname, '../temporary-test-data');
|
||||
var expectedConfig = {
|
||||
name: 'My Node'
|
||||
};
|
||||
|
||||
before(function(done) {
|
||||
// setup testing directories
|
||||
mkdirp(testDir + '/p2/p1/p0', function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
fs.writeFile(
|
||||
testDir + '/p2/bitcore-node.json',
|
||||
JSON.stringify(expectedConfig),
|
||||
function() {
|
||||
mkdirp(testDir + '/e0', function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
done();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
// cleanup testing directories
|
||||
rimraf(testDir, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('will find a configuration file', function() {
|
||||
|
||||
it('in the current directory', function() {
|
||||
var config = findConfig(path.resolve(testDir, 'p2'));
|
||||
config.path.should.equal(path.resolve(testDir, 'p2'));
|
||||
config.config.should.deep.equal(expectedConfig);
|
||||
});
|
||||
|
||||
it('in a parent directory', function() {
|
||||
var config = findConfig(path.resolve(testDir, 'p2/p1'));
|
||||
config.path.should.equal(path.resolve(testDir, 'p2'));
|
||||
config.config.should.deep.equal(expectedConfig);
|
||||
});
|
||||
|
||||
it('recursively find in parent directories', function() {
|
||||
var config = findConfig(path.resolve(testDir, 'p2/p1/p0'));
|
||||
config.path.should.equal(path.resolve(testDir, 'p2'));
|
||||
config.config.should.deep.equal(expectedConfig);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('will return false if missing a configuration', function() {
|
||||
var config = findConfig(path.resolve(testDir, 'e0'));
|
||||
config.should.equal(false);
|
||||
});
|
||||
|
||||
});
|
||||
@ -1,136 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var sinon = require('sinon');
|
||||
var proxyquire = require('proxyquire');
|
||||
var BitcoinService = require('../../lib/services/bitcoind');
|
||||
var index = require('../../lib');
|
||||
var log = index.log;
|
||||
|
||||
describe('#start', function() {
|
||||
|
||||
var sandbox = sinon.sandbox.create();
|
||||
beforeEach(function() {
|
||||
sandbox.stub(log, 'error');
|
||||
});
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('will dynamically create a node from a configuration', function() {
|
||||
|
||||
it('require each bitcore-node service with default config', function(done) {
|
||||
var node;
|
||||
var TestNode = function(options) {
|
||||
options.services[0].should.deep.equal({
|
||||
name: 'bitcoind',
|
||||
module: BitcoinService,
|
||||
config: {
|
||||
spawn: {
|
||||
datadir: './data'
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
TestNode.prototype.start = sinon.stub().callsArg(0);
|
||||
TestNode.prototype.on = sinon.stub();
|
||||
TestNode.prototype.chain = {
|
||||
on: sinon.stub()
|
||||
};
|
||||
|
||||
var starttest = proxyquire('../../lib/scaffold/start', {
|
||||
'../node': TestNode
|
||||
});
|
||||
|
||||
starttest.registerExitHandlers = sinon.stub();
|
||||
|
||||
node = starttest({
|
||||
path: __dirname,
|
||||
config: {
|
||||
services: [
|
||||
'bitcoind'
|
||||
],
|
||||
servicesConfig: {
|
||||
bitcoind: {
|
||||
spawn: {
|
||||
datadir: './data'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
node.should.be.instanceof(TestNode);
|
||||
done();
|
||||
});
|
||||
it('shutdown with an error from start', function(done) {
|
||||
var TestNode = proxyquire('../../lib/node', {});
|
||||
TestNode.prototype.start = function(callback) {
|
||||
setImmediate(function() {
|
||||
callback(new Error('error'));
|
||||
});
|
||||
};
|
||||
var starttest = proxyquire('../../lib/scaffold/start', {
|
||||
'../node': TestNode
|
||||
});
|
||||
starttest.cleanShutdown = sinon.stub();
|
||||
starttest.registerExitHandlers = sinon.stub();
|
||||
|
||||
starttest({
|
||||
path: __dirname,
|
||||
config: {
|
||||
services: [],
|
||||
servicesConfig: {}
|
||||
}
|
||||
});
|
||||
setImmediate(function() {
|
||||
starttest.cleanShutdown.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('require each bitcore-node service with explicit config', function(done) {
|
||||
var node;
|
||||
var TestNode = function(options) {
|
||||
options.services[0].should.deep.equal({
|
||||
name: 'bitcoind',
|
||||
module: BitcoinService,
|
||||
config: {
|
||||
param: 'test',
|
||||
spawn: {
|
||||
datadir: './data'
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
TestNode.prototype.start = sinon.stub().callsArg(0);
|
||||
TestNode.prototype.on = sinon.stub();
|
||||
TestNode.prototype.chain = {
|
||||
on: sinon.stub()
|
||||
};
|
||||
|
||||
var starttest = proxyquire('../../lib/scaffold/start', {
|
||||
'../node': TestNode
|
||||
});
|
||||
starttest.registerExitHandlers = sinon.stub();
|
||||
|
||||
node = starttest({
|
||||
path: __dirname,
|
||||
config: {
|
||||
services: [
|
||||
'bitcoind'
|
||||
],
|
||||
servicesConfig: {
|
||||
'bitcoind': {
|
||||
param: 'test',
|
||||
spawn: {
|
||||
datadir: './data'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
node.should.be.instanceof(TestNode);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,274 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var path = require('path');
|
||||
var sinon = require('sinon');
|
||||
var proxyquire = require('proxyquire');
|
||||
var start = require('../../lib/scaffold/start');
|
||||
|
||||
describe('#start', function() {
|
||||
describe('#checkConfigVersion2', function() {
|
||||
var sandbox = sinon.sandbox.create();
|
||||
beforeEach(function() {
|
||||
sandbox.stub(console, 'warn');
|
||||
});
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
it('will give true with "datadir" at root', function() {
|
||||
var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2;
|
||||
var v2 = checkConfigVersion2({datadir: '/home/user/.bitcore/data', services: []});
|
||||
v2.should.equal(true);
|
||||
});
|
||||
it('will give true with "address" service enabled', function() {
|
||||
var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2;
|
||||
var v2 = checkConfigVersion2({services: ['address']});
|
||||
v2.should.equal(true);
|
||||
});
|
||||
it('will give true with "db" service enabled', function() {
|
||||
var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2;
|
||||
var v2 = checkConfigVersion2({services: ['db']});
|
||||
v2.should.equal(true);
|
||||
});
|
||||
it('will give false without "datadir" at root and "address", "db" services disabled', function() {
|
||||
var checkConfigVersion2 = proxyquire('../../lib/scaffold/start', {}).checkConfigVersion2;
|
||||
var v2 = checkConfigVersion2({services: []});
|
||||
v2.should.equal(false);
|
||||
});
|
||||
});
|
||||
describe('#setupServices', function() {
|
||||
var cwd = process.cwd();
|
||||
var setupServices = proxyquire('../../lib/scaffold/start', {}).setupServices;
|
||||
it('will require an internal module', function() {
|
||||
function InternalService() {}
|
||||
InternalService.dependencies = [];
|
||||
InternalService.prototype.start = sinon.stub();
|
||||
InternalService.prototype.stop = sinon.stub();
|
||||
var expectedPath = path.resolve(__dirname, '../../lib/services/internal');
|
||||
var testRequire = function(p) {
|
||||
p.should.equal(expectedPath);
|
||||
return InternalService;
|
||||
};
|
||||
var config = {
|
||||
services: ['internal'],
|
||||
servicesConfig: {
|
||||
internal: {
|
||||
param: 'value'
|
||||
}
|
||||
}
|
||||
};
|
||||
var services = setupServices(testRequire, cwd, config);
|
||||
services[0].name.should.equal('internal');
|
||||
services[0].config.should.deep.equal({param: 'value'});
|
||||
services[0].module.should.equal(InternalService);
|
||||
});
|
||||
it('will require a local module', function() {
|
||||
function LocalService() {}
|
||||
LocalService.dependencies = [];
|
||||
LocalService.prototype.start = sinon.stub();
|
||||
LocalService.prototype.stop = sinon.stub();
|
||||
var notfoundPath = path.resolve(__dirname, '../../lib/services/local');
|
||||
var testRequire = function(p) {
|
||||
if (p === notfoundPath) {
|
||||
throw new Error();
|
||||
} else if (p === 'local') {
|
||||
return LocalService;
|
||||
} else if (p === 'local/package.json') {
|
||||
return {
|
||||
name: 'local'
|
||||
};
|
||||
}
|
||||
};
|
||||
var config = {
|
||||
services: ['local']
|
||||
};
|
||||
var services = setupServices(testRequire, cwd, config);
|
||||
services[0].name.should.equal('local');
|
||||
services[0].module.should.equal(LocalService);
|
||||
});
|
||||
it('will require a local module with "bitcoreNode" in package.json', function() {
|
||||
function LocalService() {}
|
||||
LocalService.dependencies = [];
|
||||
LocalService.prototype.start = sinon.stub();
|
||||
LocalService.prototype.stop = sinon.stub();
|
||||
var notfoundPath = path.resolve(__dirname, '../../lib/services/local');
|
||||
var testRequire = function(p) {
|
||||
if (p === notfoundPath) {
|
||||
throw new Error();
|
||||
} else if (p === 'local/package.json') {
|
||||
return {
|
||||
name: 'local',
|
||||
bitcoreNode: 'lib/bitcoreNode.js'
|
||||
};
|
||||
} else if (p === 'local/lib/bitcoreNode.js') {
|
||||
return LocalService;
|
||||
}
|
||||
};
|
||||
var config = {
|
||||
services: ['local']
|
||||
};
|
||||
var services = setupServices(testRequire, cwd, config);
|
||||
services[0].name.should.equal('local');
|
||||
services[0].module.should.equal(LocalService);
|
||||
});
|
||||
it('will throw error if module is incompatible', function() {
|
||||
var internal = {};
|
||||
var testRequire = function() {
|
||||
return internal;
|
||||
};
|
||||
var config = {
|
||||
services: ['bitcoind']
|
||||
};
|
||||
(function() {
|
||||
setupServices(testRequire, cwd, config);
|
||||
}).should.throw('Could not load service');
|
||||
});
|
||||
});
|
||||
describe('#cleanShutdown', function() {
|
||||
it('will call node stop and process exit', function() {
|
||||
var log = {
|
||||
info: sinon.stub(),
|
||||
error: sinon.stub()
|
||||
};
|
||||
var cleanShutdown = proxyquire('../../lib/scaffold/start', {
|
||||
'../': {
|
||||
log: log
|
||||
}
|
||||
}).cleanShutdown;
|
||||
var node = {
|
||||
stop: sinon.stub().callsArg(0)
|
||||
};
|
||||
var _process = {
|
||||
exit: sinon.stub()
|
||||
};
|
||||
cleanShutdown(_process, node);
|
||||
setImmediate(function() {
|
||||
node.stop.callCount.should.equal(1);
|
||||
_process.exit.callCount.should.equal(1);
|
||||
_process.exit.args[0][0].should.equal(0);
|
||||
});
|
||||
});
|
||||
it('will log error during shutdown and exit with status 1', function() {
|
||||
var log = {
|
||||
info: sinon.stub(),
|
||||
error: sinon.stub()
|
||||
};
|
||||
var cleanShutdown = proxyquire('../../lib/scaffold/start', {
|
||||
'../': {
|
||||
log: log
|
||||
}
|
||||
}).cleanShutdown;
|
||||
var node = {
|
||||
stop: sinon.stub().callsArgWith(0, new Error('test'))
|
||||
};
|
||||
var _process = {
|
||||
exit: sinon.stub()
|
||||
};
|
||||
cleanShutdown(_process, node);
|
||||
setImmediate(function() {
|
||||
node.stop.callCount.should.equal(1);
|
||||
log.error.callCount.should.equal(1);
|
||||
_process.exit.callCount.should.equal(1);
|
||||
_process.exit.args[0][0].should.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#registerExitHandlers', function() {
|
||||
var log = {
|
||||
info: sinon.stub(),
|
||||
error: sinon.stub()
|
||||
};
|
||||
var registerExitHandlers = proxyquire('../../lib/scaffold/start', {
|
||||
'../': {
|
||||
log: log
|
||||
}
|
||||
}).registerExitHandlers;
|
||||
it('log, stop and exit with an `uncaughtException`', function(done) {
|
||||
var proc = new EventEmitter();
|
||||
proc.exit = sinon.stub();
|
||||
var node = {
|
||||
stop: sinon.stub().callsArg(0)
|
||||
};
|
||||
registerExitHandlers(proc, node);
|
||||
proc.emit('uncaughtException', new Error('test'));
|
||||
setImmediate(function() {
|
||||
node.stop.callCount.should.equal(1);
|
||||
proc.exit.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('stop and exit on `SIGINT`', function(done) {
|
||||
var proc = new EventEmitter();
|
||||
proc.exit = sinon.stub();
|
||||
var node = {
|
||||
stop: sinon.stub().callsArg(0)
|
||||
};
|
||||
registerExitHandlers(proc, node);
|
||||
proc.emit('SIGINT');
|
||||
setImmediate(function() {
|
||||
node.stop.callCount.should.equal(1);
|
||||
proc.exit.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#registerExitHandlers', function() {
|
||||
var stub;
|
||||
var registerExitHandlers = require('../../lib/scaffold/start').registerExitHandlers;
|
||||
|
||||
before(function() {
|
||||
stub = sinon.stub(process, 'on');
|
||||
});
|
||||
|
||||
after(function() {
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
it('should setup two listeners on process when registering exit handlers', function() {
|
||||
registerExitHandlers(process, {});
|
||||
stub.callCount.should.equal(2);
|
||||
});
|
||||
|
||||
describe('#exitHandler', function() {
|
||||
var sandbox;
|
||||
var cleanShutdown;
|
||||
var exitHandler;
|
||||
var logStub;
|
||||
|
||||
before(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
var start = require('../../lib/scaffold/start');
|
||||
var log = require('../../lib').log;
|
||||
logStub = sandbox.stub(log, 'error');
|
||||
cleanShutdown = sandbox.stub(start, 'cleanShutdown', function() {});
|
||||
exitHandler = require('../../lib/scaffold/start').exitHandler;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should replace the listener for SIGINT after the first SIGINT is handled', function() {
|
||||
var options = { sigint: true };
|
||||
var node = {};
|
||||
exitHandler(options, process, node);
|
||||
cleanShutdown.callCount.should.equal(1);
|
||||
exitHandler(options, process, node);
|
||||
cleanShutdown.callCount.should.equal(1);
|
||||
});
|
||||
|
||||
it('should log all errors and stops the services nonetheless', function() {
|
||||
var options = { sigint: true };
|
||||
var stop = sinon.stub();
|
||||
var node = {
|
||||
stop: stop
|
||||
};
|
||||
exitHandler(options, process, node, new Error('some error'));
|
||||
logStub.callCount.should.equal(2);
|
||||
stop.callCount.should.equal(1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
84
test/services/address/encoding.unit.js
Normal file
84
test/services/address/encoding.unit.js
Normal file
@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
var bitcore = require('bitcore-lib');
|
||||
var should = require('chai').should();
|
||||
var Encoding = require('../../../lib/services/address/encoding');
|
||||
|
||||
describe('Address service encoding', function() {
|
||||
|
||||
var servicePrefix = new Buffer('0000', 'hex');
|
||||
var encoding = new Encoding(servicePrefix);
|
||||
var txid = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
|
||||
var address = '1EZBqbJSHFKSkVPNKzc5v26HA6nAHiTXq6';
|
||||
var height = 1;
|
||||
var addressSizeBuf = new Buffer(1);
|
||||
var prefix0 = new Buffer('00', 'hex');
|
||||
var prefix1 = new Buffer('01', 'hex');
|
||||
var ts = Math.floor(new Date('2017-02-28').getTime() / 1000);
|
||||
var tsBuf = new Buffer(4);
|
||||
tsBuf.writeUInt32BE(ts);
|
||||
addressSizeBuf.writeUInt8(address.length);
|
||||
var addressIndexKeyBuf = Buffer.concat([
|
||||
servicePrefix,
|
||||
prefix0,
|
||||
addressSizeBuf,
|
||||
new Buffer(address),
|
||||
new Buffer('00000001', 'hex'),
|
||||
new Buffer(txid, 'hex'),
|
||||
new Buffer('00000000', 'hex'),
|
||||
new Buffer('00', 'hex'),
|
||||
tsBuf
|
||||
]);
|
||||
var outputIndex = 5;
|
||||
var utxoKeyBuf = Buffer.concat([
|
||||
servicePrefix,
|
||||
prefix1,
|
||||
addressSizeBuf,
|
||||
new Buffer(address),
|
||||
new Buffer(txid, 'hex'),
|
||||
new Buffer('00000005', 'hex')]);
|
||||
var txHex = '0100000001cc3ffe0638792c8b39328bb490caaefe2cf418f2ce0144956e0c22515f29724d010000006a473044022030ce9fa68d1a32abf0cd4adecf90fb998375b64fe887c6987278452b068ae74c022036a7d00d1c8af19e298e04f14294c807ebda51a20389ad751b4ff3c032cf8990012103acfcb348abb526526a9f63214639d79183871311c05b2eebc727adfdd016514fffffffff02f6ae7d04000000001976a9144455183e407ee4d3423858c8a3275918aedcd18e88aca99b9b08010000001976a9140beceae2c29bfde08d2b6d80b33067451c5887be88ac00000000';
|
||||
var tx = new bitcore.Transaction(txHex);
|
||||
var sats = tx.outputs[0].satoshis;
|
||||
var satsBuf = new Buffer(8);
|
||||
satsBuf.writeDoubleBE(sats);
|
||||
var utxoValueBuf = Buffer.concat([new Buffer('00000001', 'hex'), satsBuf, tsBuf, tx.outputs[0]._scriptBuffer]);
|
||||
|
||||
it('should encode address key' , function() {
|
||||
encoding.encodeAddressIndexKey(address, height, txid, 0, 0, ts).should.deep.equal(addressIndexKeyBuf);
|
||||
});
|
||||
|
||||
it('should decode address key', function() {
|
||||
var addressIndexKey = encoding.decodeAddressIndexKey(addressIndexKeyBuf);
|
||||
addressIndexKey.address.should.equal(address);
|
||||
addressIndexKey.txid.should.equal(txid);
|
||||
addressIndexKey.height.should.equal(height);
|
||||
});
|
||||
|
||||
it('should encode utxo key', function() {
|
||||
encoding.encodeUtxoIndexKey(address, txid, outputIndex).should.deep.equal(utxoKeyBuf);
|
||||
});
|
||||
|
||||
it('should decode utxo key', function() {
|
||||
var utxoKey = encoding.decodeUtxoIndexKey(utxoKeyBuf);
|
||||
utxoKey.address.should.equal(address);
|
||||
utxoKey.txid.should.equal(txid);
|
||||
utxoKey.outputIndex.should.equal(outputIndex);
|
||||
});
|
||||
it('should encode utxo value', function() {
|
||||
encoding.encodeUtxoIndexValue(
|
||||
height,
|
||||
tx.outputs[0].satoshis,
|
||||
ts,
|
||||
tx.outputs[0]._scriptBuffer).should.deep.equal(utxoValueBuf);
|
||||
});
|
||||
|
||||
it('should decode utxo value', function() {
|
||||
var utxoValue = encoding.decodeUtxoIndexValue(utxoValueBuf);
|
||||
utxoValue.height.should.equal(height);
|
||||
utxoValue.satoshis.should.equal(sats);
|
||||
utxoValue.script.should.deep.equal(tx.outputs[0]._scriptBuffer);
|
||||
utxoValue.timestamp.should.equal(ts);
|
||||
});
|
||||
});
|
||||
|
||||
224
test/services/address/index.unit.js
Normal file
224
test/services/address/index.unit.js
Normal file
@ -0,0 +1,224 @@
|
||||
'use strict';
|
||||
|
||||
var sinon = require('sinon');
|
||||
var AddressService = require('../../../lib/services/address');
|
||||
var Tx = require('bcoin').tx;
|
||||
var expect = require('chai').expect;
|
||||
var Encoding = require('../../../lib/services/address/encoding');
|
||||
var Readable = require('stream').Readable;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var bcoin = require('bcoin');
|
||||
|
||||
describe('Address Service', function() {
|
||||
|
||||
var tx = Tx.fromRaw( '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000', 'hex');
|
||||
var blocks = require('../../regtest/data/blocks.json');
|
||||
var addressService;
|
||||
var sandbox;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
addressService = new AddressService({
|
||||
node: {
|
||||
getNetworkName: function() { return 'regtest'; },
|
||||
services: []
|
||||
}
|
||||
});
|
||||
addressService._encoding = new Encoding(new Buffer('0000', 'hex'));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
|
||||
it('should get prefix for database', function(done) {
|
||||
var getPrefix = sandbox.stub().callsArgWith(1, null, new Buffer('ffee', 'hex'));
|
||||
addressService._db = { getPrefix: getPrefix };
|
||||
addressService.start(function() {
|
||||
expect(getPrefix.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
it('should stop the service', function(done) {
|
||||
addressService.stop(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#getAddressHistory', function() {
|
||||
it('should get the address history', function(done) {
|
||||
|
||||
sandbox.stub(addressService, '_getAddressHistory').callsArgWith(2, null, {});
|
||||
addressService.getAddressHistory(['a', 'b', 'c'], { from: 12, to: 14 }, function(err, res) {
|
||||
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
expect(res).to.be.deep.equal({
|
||||
totalItems: 3,
|
||||
from: 12,
|
||||
to: 14,
|
||||
items: [ {}, {}, {} ]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_getAddresHistory', function() {
|
||||
it('should get the address history', function(done) {
|
||||
var encoding = new Encoding(new Buffer('0001', 'hex'));
|
||||
addressService._encoding = encoding;
|
||||
var address = 'a';
|
||||
var opts = { from: 12, to: 14 };
|
||||
var txid = '1c6ea4a55a3edaac0a05e93b52908f607376a8fdc5387c492042f8baa6c05085';
|
||||
var data = [ null, encoding.encodeAddressIndexKey(address, 123, txid, 1, 1) ];
|
||||
var getTransaction = sandbox.stub().callsArgWith(2, null, {});
|
||||
addressService._tx = { getTransaction: getTransaction };
|
||||
|
||||
var txidStream = new Readable();
|
||||
|
||||
txidStream._read = function() {
|
||||
txidStream.push(data.pop());
|
||||
}
|
||||
|
||||
var createReadStream = sandbox.stub().returns(txidStream);
|
||||
addressService._db = { createKeyStream: createReadStream };
|
||||
|
||||
addressService._getAddressHistory(address, opts, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
expect(getTransaction.calledOnce).to.be.true;
|
||||
expect(res).to.deep.equal([{}]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#AddressSummary', function() {
|
||||
|
||||
it('should get the address summary', function(done) {
|
||||
var encoding = new Encoding(new Buffer('0001', 'hex'));
|
||||
addressService._encoding = encoding;
|
||||
var address = 'a';
|
||||
var txid = tx.txid();
|
||||
var data = [ null, encoding.encodeAddressIndexKey(address, 123, txid, 1, 0) ];
|
||||
var inputValues = [120, 0, 120, 120];
|
||||
tx.__inputValues = inputValues;
|
||||
var getTransaction = sandbox.stub().callsArgWith(2, null, tx);
|
||||
addressService._tx = { getTransaction: getTransaction };
|
||||
addressService._header = { getBestHeight: function() { return 150; } };
|
||||
|
||||
var txidStream = new Readable();
|
||||
|
||||
txidStream._read = function() {
|
||||
txidStream.push(data.pop());
|
||||
}
|
||||
|
||||
var createReadStream = sandbox.stub().returns(txidStream);
|
||||
addressService._db = { createKeyStream: createReadStream };
|
||||
|
||||
addressService.getAddressSummary(address, {}, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
expect(getTransaction.calledOnce).to.be.true;
|
||||
expect(res).to.deep.equal({ addrStr: 'a',
|
||||
balance: 0.01139033,
|
||||
balanceSat: 1139033,
|
||||
totalReceived: 0.01139033,
|
||||
totalReceivedSat: 1139033,
|
||||
totalSent: 0,
|
||||
totalSentSat: 0,
|
||||
unconfirmedBalance: 0,
|
||||
unconfirmedBalanceSat: 0,
|
||||
unconfirmedTxApperances: 0,
|
||||
txApperances: 1,
|
||||
transactions: [ '25e28f9fb0ada5353b7d98d85af5524b2f8df5b0b0e2d188f05968bceca603eb' ]
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
describe('#getAddressUnspentOutputs', function() {
|
||||
it('should get address utxos', function(done) {
|
||||
|
||||
var encoding = new Encoding(new Buffer('0001', 'hex'));
|
||||
addressService._encoding = encoding;
|
||||
|
||||
var address = 'a';
|
||||
var txid = tx.txid();
|
||||
var ts = Math.floor(new Date('2019-01-01').getTime() / 1000);
|
||||
|
||||
var data = {
|
||||
key: encoding.encodeUtxoIndexKey(address, txid, 1),
|
||||
value: encoding.encodeUtxoIndexValue(123, 120000, ts, tx.outputs[1].script.raw)
|
||||
};
|
||||
|
||||
addressService._header = { getBestHeight: function() { return 150; } };
|
||||
|
||||
var txidStream = new EventEmitter();
|
||||
|
||||
var createReadStream = sandbox.stub().returns(txidStream);
|
||||
addressService._db = { createReadStream: createReadStream };
|
||||
|
||||
addressService.getAddressUnspentOutputs(address, {}, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
expect(res[0]).to.deep.equal({
|
||||
address: "a",
|
||||
amount: 0.0012,
|
||||
confirmations: 27,
|
||||
confirmationsFromCache: true,
|
||||
satoshis: 120000,
|
||||
scriptPubKey: "76a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac",
|
||||
ts: 1546300800,
|
||||
txid: "25e28f9fb0ada5353b7d98d85af5524b2f8df5b0b0e2d188f05968bceca603eb",
|
||||
vout: 1
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
txidStream.emit('data', data);
|
||||
txidStream.emit('end');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#onReorg', function() {
|
||||
|
||||
it('should reorg', function(done ) {
|
||||
|
||||
var commonAncestorHeader = bcoin.block.fromRaw(blocks[5], 'hex').toHeaders().toJSON();
|
||||
var oldBlocks = [bcoin.block.fromRaw(blocks[6], 'hex')];
|
||||
|
||||
addressService.onReorg([commonAncestorHeader, oldBlocks], function(err, ops) {
|
||||
|
||||
expect(ops.length).to.equal(1);
|
||||
expect(ops[0].type).to.equal('del');
|
||||
|
||||
done();
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
49
test/services/block/encoding.unit.js
Normal file
49
test/services/block/encoding.unit.js
Normal file
@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var Block = require('bcoin').block;
|
||||
|
||||
var Encoding = require('../../../lib/services/block/encoding');
|
||||
|
||||
describe('Block service encoding', function() {
|
||||
|
||||
var servicePrefix = new Buffer('0000', 'hex');
|
||||
|
||||
var encoding = new Encoding(servicePrefix);
|
||||
var hash = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
|
||||
var height = 1;
|
||||
var block = Block.fromRaw('0100000095194b8567fe2e8bbda931afd01a7acd399b9325cb54683e64129bcd00000000660802c98f18fd34fd16d61c63cf447568370124ac5f3be626c2e1c3c9f0052d19a76949ffff001d33f3c25d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d014dffffffff0100f2052a01000000434104e70a02f5af48a1989bf630d92523c9d14c45c75f7d1b998e962bff6ff9995fc5bdb44f1793b37495d80324acba7c8f537caaf8432b8d47987313060cc82d8a93ac00000000', 'hex');
|
||||
|
||||
describe('Block', function() {
|
||||
|
||||
it('should encode block key' , function() {
|
||||
encoding.encodeBlockKey(hash).should.deep.equal(Buffer.concat([
|
||||
servicePrefix,
|
||||
new Buffer(hash, 'hex')
|
||||
]));
|
||||
});
|
||||
|
||||
it('should decode block key' , function() {
|
||||
var buf = Buffer.concat([
|
||||
servicePrefix,
|
||||
new Buffer(hash, 'hex')
|
||||
]);
|
||||
|
||||
var actual = encoding.decodeBlockKey(buf);
|
||||
actual.should.deep.equal(hash);
|
||||
});
|
||||
|
||||
it('should encode block value', function() {
|
||||
encoding.encodeBlockValue(block).should.deep.equal(
|
||||
block.toRaw());
|
||||
});
|
||||
|
||||
it('shound decode block value', function() {
|
||||
var ret = encoding.decodeBlockValue(block.toRaw());
|
||||
ret.should.deep.equal(ret);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
196
test/services/block/index.unit.js
Normal file
196
test/services/block/index.unit.js
Normal file
@ -0,0 +1,196 @@
|
||||
'use strict';
|
||||
|
||||
var expect = require('chai').expect;
|
||||
var BlockService = require('../../../lib/services/block');
|
||||
var sinon = require('sinon');
|
||||
var bcoin = require('bcoin');
|
||||
var Block = bcoin.block;
|
||||
var Encoding = require('../../../lib/services/block/encoding');
|
||||
|
||||
describe('Block Service', function() {
|
||||
|
||||
var blockService;
|
||||
var blocks = require('../../regtest/data/blocks.json');
|
||||
var block1 = Block.fromRaw('010000006a39821735ec18a366d95b391a7ff10dee181a198f1789b0550e0d00000000002b0c80fa52b669022c344c3e09e6bb9698ab90707bb4bb412af3fbf31cfd2163a601514c5a0c011c572aef0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c022003ffffffff0100f2052a01000000434104c5b694d72e601091fd733c6b18b94795c13e2db6b1474747e7be914b407854cad37cee3058f85373b9f9dbb0014e541c45851d5f85e83a1fd7c45e54423718f3ac00000000', 'hex');
|
||||
var block2 = Block.fromRaw('01000000fb3c5deea3902d5e6e0222435688795152ae0f737715b0bed6a88b00000000008ec0f92d33b05617cb3c3b4372aa0c2ae3aeb8aa7f34fe587db8e55b578cfac6b601514c5a0c011c98a831000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c027f01ffffffff0100f2052a0100000043410495fee5189566db550919ad2b4e5f9111dbdc2cb60b5c71ea4c0fdad59a961c42eb289e5b9fdc4cb3f3fec6dd866172720bae3e3b881fc203fcaf98bf902c53f1ac00000000', 'hex');
|
||||
|
||||
var sandbox;
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
blockService = new BlockService({
|
||||
node: {
|
||||
getNetworkName: function() { return 'regtest'; },
|
||||
services: []
|
||||
}
|
||||
});
|
||||
blockService._encoding = new Encoding(new Buffer('0000', 'hex'));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#_detectReorg', function() {
|
||||
it('should detect reorg', function() {
|
||||
var block = Block.fromRaw(blocks[6], 'hex');
|
||||
blockService._tip = { hash: bcoin.util.revHex(Block.fromRaw(blocks[5], 'hex').prevBlock) };
|
||||
expect(blockService._detectReorg(block)).to.be.true;
|
||||
});
|
||||
|
||||
it('should not detect reorg', function() {
|
||||
var block = Block.fromRaw(blocks[6], 'hex');
|
||||
blockService._tip = { hash: bcoin.util.revHex(Block.fromRaw(blocks[6], 'hex').prevBlock) };
|
||||
expect(blockService._detectReorg(block)).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_findCommonAncestor', function() {
|
||||
|
||||
it('should find the common ancestor between the current chain and the new chain', function(done) {
|
||||
|
||||
blockService._tip = { hash: block2.rhash(), height: 70901 };
|
||||
|
||||
var encodedData = blockService._encoding.encodeBlockValue(block2);
|
||||
|
||||
var get = sandbox.stub().callsArgWith(1, null, encodedData);
|
||||
|
||||
var headers = { get: sandbox.stub().returns({ prevHash: block1.rhash() }) };
|
||||
blockService._db = { get: get };
|
||||
|
||||
blockService._findCommonAncestor('aa', headers, function(err, common, oldBlocks) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
expect(common).to.equal('aa');
|
||||
expect(oldBlocks).to.deep.equal([]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getBestBlockHash', function() {
|
||||
it('should get best block hash', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getBlock', function() {
|
||||
it('should get block', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getBlockHashesByTimestamp', function() {
|
||||
it('should get block hashes by timestamp', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getBlockHeader', function() {
|
||||
it('should get block header', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getBlockOverview', function() {
|
||||
it('should get block overview', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getRawBlock', function() {
|
||||
});
|
||||
|
||||
describe('#_onBlock', function() {
|
||||
|
||||
it('should process blocks', function() {
|
||||
var processBlock = sandbox.stub(blockService, '_processBlock');
|
||||
blockService._tip = { hash: block1.rhash(), height: 1 };
|
||||
blockService._onBlock(block2);
|
||||
expect(processBlock.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should not process blocks', function() {
|
||||
var processBlock = sandbox.stub(blockService, '_processBlock');
|
||||
blockService._tip = { hash: block2.rhash(), height: 1 };
|
||||
blockService._onBlock(block1);
|
||||
expect(processBlock.calledOnce).to.be.false;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_setListeners', function() {
|
||||
|
||||
it('should set listeners for headers, reorg', function() {
|
||||
var on = sandbox.stub();
|
||||
var once = sandbox.stub();
|
||||
blockService._header = { on: on, once: once };
|
||||
blockService._setListeners();
|
||||
expect(on.calledOnce).to.be.true;
|
||||
expect(once.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_setTip', function() {
|
||||
|
||||
it('should set the tip if given a block', function() {
|
||||
blockService._db = {};
|
||||
blockService._tip = { height: 99, hash: '00' };
|
||||
blockService._setTip({ height: 100, hash: 'aa' });
|
||||
expect(blockService._tip).to.deep.equal({ height: 100, hash: 'aa' });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_startSubscriptions', function() {
|
||||
it('should start the subscriptions if not already subscribed', function() {
|
||||
var on = sinon.stub();
|
||||
var subscribe = sinon.stub();
|
||||
var openBus = sinon.stub().returns({ on: on, subscribe: subscribe });
|
||||
blockService.node = { openBus: openBus };
|
||||
blockService._startSubscriptions();
|
||||
expect(blockService._subscribed).to.be.true;
|
||||
expect(openBus.calledOnce).to.be.true;
|
||||
expect(on.calledOnce).to.be.true;
|
||||
expect(subscribe.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_startSync', function() {
|
||||
|
||||
it('should start the sync of blocks if type set', function() {
|
||||
blockService._header = { getLastHeader: sinon.stub.returns({ height: 100 }) };
|
||||
blockService._tip = { height: 98 };
|
||||
var sync = sandbox.stub(blockService, '_sync');
|
||||
blockService._startSync();
|
||||
expect(sync.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
|
||||
it('should get the prefix', function(done) {
|
||||
var getPrefix = sandbox.stub().callsArgWith(1, null, blockService._encoding);
|
||||
var getServiceTip = sandbox.stub().callsArgWith(1, null, { height: 1, hash: 'aa' });
|
||||
var setListeners = sandbox.stub(blockService, '_setListeners');
|
||||
var startSub = sandbox.stub(blockService, '_startSubscriptions');
|
||||
var setTip = sandbox.stub(blockService, '_setTip');
|
||||
blockService._db = { getPrefix: getPrefix, getServiceTip: getServiceTip };
|
||||
blockService.start(function() {
|
||||
expect(blockService._encoding).to.be.an.instanceof(Encoding);
|
||||
expect(getServiceTip.calledOnce).to.be.true;
|
||||
expect(getPrefix.calledOnce).to.be.true;
|
||||
expect(startSub.calledOnce).to.be.true;
|
||||
expect(setTip.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
|
||||
it('should call stop', function(done) {
|
||||
blockService.stop(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
324
test/services/db/index.unit.js
Normal file
324
test/services/db/index.unit.js
Normal file
@ -0,0 +1,324 @@
|
||||
'use strict';
|
||||
|
||||
var chai = require('chai');
|
||||
var should = chai.should();
|
||||
var assert = chai.assert;
|
||||
var expect = chai.expect;
|
||||
var DBService = require('../../../lib/services/db');
|
||||
var sinon = require('sinon');
|
||||
var Levelup = require('levelup');
|
||||
var Tx = require('bcoin').tx;
|
||||
|
||||
describe('DB', function() {
|
||||
|
||||
var dbService;
|
||||
var tx = Tx.fromRaw( '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000', 'hex');
|
||||
|
||||
var sandbox;
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
dbService = new DBService({
|
||||
node: {
|
||||
services: [],
|
||||
datadir: '/tmp',
|
||||
network: 'regtest',
|
||||
on: sinon.stub()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
it('should start the db service by creating a db dir, ' +
|
||||
' if necessary, and setting the store', function(done) {
|
||||
dbService.start(function() {
|
||||
dbService._store.should.be.instanceOf(Levelup);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
it('should stop if store not open', function(done) {
|
||||
dbService.stop(function() {
|
||||
var close = sandbox.stub().callsArg(0);
|
||||
dbService._store = { close: close };
|
||||
dbService._stopping.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should stop if store open', function(done) {
|
||||
dbService.stop(function() {
|
||||
var close = sandbox.stub().callsArg(0);
|
||||
dbService._store = { close: close, isOpen: sinon.stub().returns(true) };
|
||||
dbService._stopping.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_onError', function() {
|
||||
it('should stop the db', function() {
|
||||
var stop = sandbox.stub();
|
||||
dbService.node = { stop: stop };
|
||||
dbService._onError(new Error('some error'));
|
||||
stop.should.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_setDataPath', function() {
|
||||
|
||||
it('should set the data path', function() {
|
||||
dbService._setDataPath();
|
||||
dbService.dataPath.should.equal('/tmp/regtest/bitcorenode.db');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_setVersion', function() {
|
||||
it('should set the version', function(done) {
|
||||
var put = sandbox.stub(dbService, 'put').callsArgWith(2, null);
|
||||
dbService._setVersion(function(err) {
|
||||
put.should.be.calledOnce;
|
||||
put.args[0][0].toString('hex').should.deep.equal('ffff76657273696f6e');
|
||||
put.args[0][1].toString('hex').should.deep.equal('00000001');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#get', function() {
|
||||
it('should get a value from the db', function(done) {
|
||||
var get = sandbox.stub().callsArgWith(2, null, 'data');
|
||||
dbService._store = { get: get };
|
||||
dbService.get('key', function(err, value) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
value.should.equal('data');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not get a value while the node is shutting down', function(done) {
|
||||
dbService._stopping = true;
|
||||
dbService.get('key', function(err, value) {
|
||||
err.message.should.equal('Shutdown sequence underway, not able to complete the query');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#put', function() {
|
||||
it('should put a value in the db', function(done) {
|
||||
var put = sandbox.stub().callsArgWith(2, null);
|
||||
dbService._store = { put: put };
|
||||
dbService.put(new Buffer('key'), new Buffer('value'), function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
put.should.be.calledOnce;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow an operation while the node is shutting down', function(done) {
|
||||
dbService._stopping = true;
|
||||
dbService.put(new Buffer('key'), new Buffer('value'), function(err) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#batch', function() {
|
||||
|
||||
it('should save a batch of operations', function(done) {
|
||||
|
||||
var batch = sandbox.stub().callsArgWith(1, null);
|
||||
dbService._store = { batch: batch };
|
||||
|
||||
dbService.batch([], function(err) {
|
||||
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
batch.callCount.should.equal(1);
|
||||
done();
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call batch whilst shutting down', function(done) {
|
||||
|
||||
dbService._stopping = true;
|
||||
|
||||
var batch = sandbox.stub().callsArgWith(1, null);
|
||||
dbService._store = { batch: batch };
|
||||
|
||||
dbService.batch(batch, function(err) {
|
||||
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
batch.callCount.should.equal(0);
|
||||
done();
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createReadStream', function() {
|
||||
|
||||
it('should get a read stream', function() {
|
||||
|
||||
var on = sandbox.stub();
|
||||
var stream = { on: on };
|
||||
var createReadStream = sandbox.stub().returns(stream);
|
||||
dbService._store = { createReadStream: createReadStream };
|
||||
dbService.createReadStream([]).should.deep.equal(stream);
|
||||
createReadStream.callCount.should.equal(1);
|
||||
|
||||
});
|
||||
|
||||
it('should not get a read stream if the node is stopping', function() {
|
||||
|
||||
dbService._stopping = true;
|
||||
|
||||
var on = sandbox.stub();
|
||||
var stream = { on: on };
|
||||
var createReadStream = sandbox.stub().returns(stream);
|
||||
dbService._store = { createReadStream: createReadStream };
|
||||
var stream = dbService.createReadStream([]);
|
||||
expect(stream).to.be.undefined;
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#createKeyStream', function() {
|
||||
|
||||
it('should get a key stream', function() {
|
||||
|
||||
var on = sandbox.stub();
|
||||
var stream = { on: on };
|
||||
var createKeyStream = sandbox.stub().returns(stream);
|
||||
dbService._store = { createKeyStream: createKeyStream };
|
||||
dbService.createKeyStream([]).should.deep.equal(stream);
|
||||
createKeyStream.callCount.should.equal(1);
|
||||
|
||||
});
|
||||
|
||||
it('should not get a key stream if the node is stopping', function() {
|
||||
|
||||
dbService._stopping = true;
|
||||
|
||||
var on = sandbox.stub();
|
||||
var stream = { on: on };
|
||||
var createKeyStream = sandbox.stub().returns(stream);
|
||||
dbService._store = { createKeyStream: createKeyStream };
|
||||
var stream = dbService.createKeyStream([]);
|
||||
expect(stream).to.be.undefined;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#close', function() {
|
||||
it('should close the store if there is a store and it is open', function(done) {
|
||||
|
||||
var close = sandbox.stub().callsArgWith(0, null);
|
||||
dbService._store = { isOpen: sinon.stub().returns(true), close: close };
|
||||
|
||||
dbService.close(function(err) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
close.callCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getServiceTip', function() {
|
||||
it('should get service tip for previously saved', function(done) {
|
||||
|
||||
var tipBuf = Buffer.concat([ new Buffer('deadbeef', 'hex'), new Buffer(tx.txid(), 'hex') ]);
|
||||
var get = sandbox.stub(dbService, 'get').callsArgWith(1, null, tipBuf);
|
||||
dbService.getServiceTip('test', function(err, tip) {
|
||||
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
get.callCount.should.equal(1);
|
||||
tip.height.should.equal(0xdeadbeef);
|
||||
tip.hash.should.equal(tx.txid());
|
||||
done();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('should get service tip for not previously saved', function(done) {
|
||||
|
||||
var get = sandbox.stub(dbService, 'get').callsArgWith(1, null, null);
|
||||
dbService.getServiceTip('test', function(err, tip) {
|
||||
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
get.callCount.should.equal(1);
|
||||
tip.height.should.equal(0);
|
||||
tip.hash.should.equal('0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206');
|
||||
done();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getPrefix', function() {
|
||||
|
||||
it('should get the db prefix for a service when one already exists', function(done) {
|
||||
var get = sandbox.stub(dbService, 'get').callsArgWith(1, null, new Buffer('0000', 'hex'));
|
||||
dbService.getPrefix('test', function(err, prefix) {
|
||||
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
get.callCount.should.equal(1);
|
||||
prefix.toString('hex').should.equal('0000');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the db prefix for a service when one does not already exist', function(done) {
|
||||
var put = sandbox.stub(dbService, 'put').callsArgWith(2, null);
|
||||
var get = sandbox.stub(dbService, 'get');
|
||||
get.onCall(0).callsArgWith(1, null, null);
|
||||
get.onCall(1).callsArgWith(1, null, new Buffer('eeee', 'hex'));
|
||||
dbService.getPrefix('test', function(err, prefix) {
|
||||
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
get.callCount.should.equal(2);
|
||||
put.callCount.should.equal(2);
|
||||
put.args[1][1].toString('hex').should.equal('eeef');
|
||||
prefix.toString('hex').should.equal('eeee');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
54
test/services/fee/index.unit.js
Normal file
54
test/services/fee/index.unit.js
Normal file
@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
var sinon = require('sinon');
|
||||
var FeeService = require('../../../lib/services/fee');
|
||||
var expect = require('chai').expect;
|
||||
|
||||
describe('#Fee Service', function() {
|
||||
|
||||
var feeService;
|
||||
var sandbox;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
feeService = new FeeService({
|
||||
rpc: {
|
||||
user: 'bitcoin',
|
||||
pass: 'local321',
|
||||
host: 'localhost',
|
||||
protocol: 'http',
|
||||
port: 8332
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
/*
|
||||
Running in regtest mode or unsync'd will return -1
|
||||
*/
|
||||
|
||||
it('Has an estimateFee method', function() {
|
||||
var method = feeService.getAPIMethods()[0][0];
|
||||
expect(method).to.equal('estimateFee');
|
||||
});
|
||||
|
||||
it('Can estimate fees', function(done) {
|
||||
var estimateFee = sinon.stub().callsArgWith(1, null, { result: 0.1 });
|
||||
feeService._client = { estimateFee: estimateFee };
|
||||
feeService.estimateFee(4, function(err, fee) {
|
||||
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
expect(fee).to.equal(0.1);
|
||||
done();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
88
test/services/header/encoding.unit.js
Normal file
88
test/services/header/encoding.unit.js
Normal file
@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
|
||||
var Encoding = require('../../../lib/services/header/encoding');
|
||||
|
||||
describe('Header service encoding', function() {
|
||||
|
||||
var servicePrefix = new Buffer('0000', 'hex');
|
||||
|
||||
var hashPrefix = new Buffer('00', 'hex');
|
||||
var heightPrefix = new Buffer('01', 'hex');
|
||||
var encoding = new Encoding(servicePrefix);
|
||||
var hash = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
|
||||
var hashBuf = new Buffer(hash, 'hex');
|
||||
var header = {
|
||||
hash: hash,
|
||||
prevHash: '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03ade',
|
||||
version: 0x2000012,
|
||||
merkleRoot: '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03adf',
|
||||
timestamp: 1E9,
|
||||
bits: 400000,
|
||||
nonce: 123456,
|
||||
height: 123,
|
||||
chainwork: '0000000000000000000000000000000000000000000000000000000200020002'
|
||||
};
|
||||
var versionBuf = new Buffer(4);
|
||||
var prevHashBuf = new Buffer(header.prevHash, 'hex');
|
||||
var merkleRootBuf = new Buffer(header.merkleRoot, 'hex');
|
||||
var tsBuf = new Buffer(4);
|
||||
var bitsBuf = new Buffer(4);
|
||||
var nonceBuf = new Buffer(4);
|
||||
var heightBuf = new Buffer(4);
|
||||
var chainBuf = new Buffer('0000000000000000000000000000000000000000000000000000000200020002', 'hex');
|
||||
heightBuf.writeUInt32BE(header.height);
|
||||
|
||||
it('should encode header hash key' , function() {
|
||||
encoding.encodeHeaderHashKey(hash).should.deep.equal(Buffer.concat([servicePrefix, hashPrefix, hashBuf]));
|
||||
});
|
||||
|
||||
it('should decode header hash key', function() {
|
||||
encoding.decodeHeaderHashKey(Buffer.concat([servicePrefix, hashPrefix, hashBuf]))
|
||||
.should.deep.equal(hash);
|
||||
});
|
||||
|
||||
it('should encode header height key' , function() {
|
||||
encoding.encodeHeaderHeightKey(header.height).should.deep.equal(Buffer.concat([servicePrefix, heightPrefix, heightBuf]));
|
||||
});
|
||||
|
||||
it('should decode header height key', function() {
|
||||
encoding.decodeHeaderHeightKey(Buffer.concat([servicePrefix, heightPrefix, heightBuf]))
|
||||
.should.deep.equal(header.height);
|
||||
});
|
||||
it('should encode header value', function() {
|
||||
var prevHashBuf = new Buffer(header.prevHash, 'hex');
|
||||
versionBuf.writeInt32BE(header.version); // signed
|
||||
tsBuf.writeUInt32BE(header.timestamp);
|
||||
bitsBuf.writeUInt32BE(header.bits);
|
||||
nonceBuf.writeUInt32BE(header.nonce);
|
||||
heightBuf.writeUInt32BE(header.height);
|
||||
encoding.encodeHeaderValue(header).should.deep.equal(Buffer.concat([
|
||||
hashBuf,
|
||||
versionBuf,
|
||||
prevHashBuf,
|
||||
merkleRootBuf,
|
||||
tsBuf,
|
||||
bitsBuf,
|
||||
nonceBuf,
|
||||
heightBuf,
|
||||
chainBuf
|
||||
]));
|
||||
});
|
||||
|
||||
it('should decode header value', function() {
|
||||
encoding.decodeHeaderValue(Buffer.concat([
|
||||
hashBuf,
|
||||
versionBuf,
|
||||
prevHashBuf,
|
||||
merkleRootBuf,
|
||||
tsBuf,
|
||||
bitsBuf,
|
||||
nonceBuf,
|
||||
heightBuf,
|
||||
chainBuf
|
||||
])).should.deep.equal(header);
|
||||
});
|
||||
});
|
||||
|
||||
162
test/services/header/index.unit.js
Normal file
162
test/services/header/index.unit.js
Normal file
@ -0,0 +1,162 @@
|
||||
'use strict';
|
||||
|
||||
var sinon = require('sinon');
|
||||
var HeaderService = require('../../../lib/services/header');
|
||||
var chai = require('chai');
|
||||
var assert = chai.assert;
|
||||
var expect = chai.expect;
|
||||
var Encoding = require('../../../lib/services/header/encoding');
|
||||
var utils = require('../../../lib/utils');
|
||||
var Block = require('bitcore-lib').Block;
|
||||
var BN = require('bn.js');
|
||||
var Emitter = require('events').EventEmitter;
|
||||
|
||||
describe('Header Service', function() {
|
||||
|
||||
var headerService;
|
||||
var sandbox;
|
||||
var prevHeader = new Block(new Buffer('01000000b25c0849b469983b4a5b90a49e4c0e4ba3853122ed141b5bd92d14000000000021a8aaa4995e4ce3b885677730b153741feda66a08492287a45c6a131671ba5a72ff504c5a0c011c456e4d060201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c028208ffffffff0100f2052a010000004341041994d910507ec4b2135dd32a4723caf00f8567f356ffbd5e703786d856b49a89d6597c280d8981238fbde81fa3767161bc3e994c17be41b42235a61c24c73459ac0000000001000000013b517d1aebd89b4034e0cf9b25ecbe82ef162ce71284e92a1f1adebf44ea1409000000008b483045022100c7ebc62e89740ddab42a64435c996e1c91a063f9f2cc004b4f023f7a1be5234402207608837faebec16049461d4ef7de807ce217040fd2a823a29da16ec07e463d440141048f108c0da4b5be3308e2e0b521d02d341de85b36a29285b47f00bc33e57a89cf4b6e76aa4a48ddc9a5e882620779e0f1b19dc98d478052fbd544167c745be1d8ffffffff010026e85a050000001976a914f760ef90462b0a4bde26d597c1f29324f5cd0fc488ac00000000', 'hex')).header.toObject();
|
||||
var preObjectHeader = new Block(new Buffer('010000006a39821735ec18a366d95b391a7ff10dee181a198f1789b0550e0d00000000002b0c80fa52b669022c344c3e09e6bb9698ab90707bb4bb412af3fbf31cfd2163a601514c5a0c011c572aef0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c022003ffffffff0100f2052a01000000434104c5b694d72e601091fd733c6b18b94795c13e2db6b1474747e7be914b407854cad37cee3058f85373b9f9dbb0014e541c45851d5f85e83a1fd7c45e54423718f3ac00000000', 'hex')).header;
|
||||
var header = preObjectHeader.toObject();
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
headerService = new HeaderService({
|
||||
node: {
|
||||
getNetworkName: function() { return 'regtest'; },
|
||||
services: []
|
||||
}
|
||||
});
|
||||
headerService._encoding = new Encoding(new Buffer('0000', 'hex'));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
|
||||
|
||||
it('should get prefix for database', function(done) {
|
||||
|
||||
var getServiceTip = sandbox.stub().callsArgWith(1, null, { height: 123, hash: 'a' });
|
||||
var setListeners = sandbox.stub(headerService, '_setListeners');
|
||||
var getPrefix = sandbox.stub().callsArgWith(1, null, new Buffer('ffee', 'hex'));
|
||||
var getLastHeader = sandbox.stub(headerService, '_getLastHeader').callsArgWith(0, null);
|
||||
var openBus = sandbox.stub();
|
||||
headerService.node = { openBus: openBus };
|
||||
var _startHeaderSubscription = sandbox.stub(headerService, '_startHeaderSubscription');
|
||||
|
||||
headerService._db = { getPrefix: getPrefix, getServiceTip: getServiceTip, batch: sinon.stub() };
|
||||
|
||||
headerService.start(function() {
|
||||
expect(_startHeaderSubscription.calledOnce).to.be.true;
|
||||
expect(getLastHeader.calledOnce).to.be.true;
|
||||
expect(setListeners.calledOnce).to.be.true;
|
||||
expect(headerService._tip).to.be.deep.equal({ height: 123, hash: 'a' });
|
||||
expect(headerService._encoding).to.be.instanceOf(Encoding);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
it('should stop the service', function(done) {
|
||||
headerService.stop(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAllHeaders', function() {
|
||||
|
||||
it('should get all the headers', function(done) {
|
||||
headerService._tip = { height: 123 };
|
||||
|
||||
var fakeStream = new Emitter();
|
||||
var createReadStream = sandbox.stub().returns(fakeStream);
|
||||
headerService._db = { createReadStream: createReadStream };
|
||||
|
||||
headerService.getAllHeaders(function(err, headers) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
expect(headers.length).to.deep.equal(1);
|
||||
expect(headers.get(header.hash).hash).to.equal('00000000008ba8d6beb01577730fae52517988564322026e5e2d90a3ee5d3cfb');
|
||||
done();
|
||||
});
|
||||
|
||||
header.chainwork = '00';
|
||||
fakeStream.emit('data', { value: headerService._encoding.encodeHeaderValue(header) });
|
||||
fakeStream.emit('end');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_startSync', function() {
|
||||
|
||||
it('should start the sync process', function() {
|
||||
headerService._bestHeight = 123;
|
||||
headerService._tip = { height: 120 };
|
||||
var getHeaders = sandbox.stub();
|
||||
headerService._p2p = { getHeaders: getHeaders };
|
||||
headerService._startSync();
|
||||
expect(getHeaders.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_sync', function() {
|
||||
|
||||
it('should sync header', function() {
|
||||
headerService._numNeeded = 1000;
|
||||
headerService._tip = { height: 121, hash: 'a' };
|
||||
var getHeaders = sandbox.stub();
|
||||
headerService._p2p = { getHeaders: getHeaders };
|
||||
headerService._sync();
|
||||
expect(getHeaders.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_onHeaders', function() {
|
||||
|
||||
it('should handle new headers received', function() {
|
||||
|
||||
var headers = [preObjectHeader];
|
||||
var onHeader = sandbox.stub(headerService, '_onHeader');
|
||||
var saveHeaders = sandbox.stub(headerService, '_saveHeaders');
|
||||
headerService._tip = { height: 123, hash: 'aa' };
|
||||
headerService._onHeaders(headers);
|
||||
expect(onHeader.calledOnce).to.be.true;
|
||||
expect(saveHeaders.calledOnce).to.be.true;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_getChainwork', function() {
|
||||
|
||||
it('should get chainwork', function() {
|
||||
prevHeader.chainwork = '000000000000000000000000000000000000000000000000000d4b2e8ee30c08';
|
||||
var actual = headerService._getChainwork(header, prevHeader);
|
||||
prevHeader.chainwork = '000000000000000000000000000000000000000000000000000d4b2e8ee30c08';
|
||||
expect(actual.toString(16, 64)).to.equal('000000000000000000000000000000000000000000000000000d4c22c66d0d72');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_computeChainwork', function() {
|
||||
|
||||
it('should calculate chain work correctly', function() {
|
||||
var expected = new BN(new Buffer('000000000000000000000000000000000000000000677c7b8122f9902c79f4e0', 'hex'));
|
||||
var prev = new BN(new Buffer('000000000000000000000000000000000000000000677bd68118a98f8779ea90', 'hex'));
|
||||
|
||||
var actual = headerService._computeChainwork(0x18018d30, prev);
|
||||
assert(actual.eq(expected), 'not equal: actual: ' + actual + ' expected: ' + expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
41
test/services/mempool/encoding.unit.js
Normal file
41
test/services/mempool/encoding.unit.js
Normal file
@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var tx = require('bcoin').tx;
|
||||
|
||||
var Encoding = require('../../../lib/services/mempool/encoding');
|
||||
|
||||
describe('Block service encoding', function() {
|
||||
|
||||
var servicePrefix = new Buffer('0000', 'hex');
|
||||
|
||||
var encoding = new Encoding(servicePrefix);
|
||||
var hash = '25e28f9fb0ada5353b7d98d85af5524b2f8df5b0b0e2d188f05968bceca603eb';
|
||||
var txString = '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000'
|
||||
|
||||
describe('Mempool', function() {
|
||||
|
||||
it('should encode mempool transaction key', function() {
|
||||
encoding.encodeMempoolTransactionKey(hash).should.deep.equal(Buffer.concat([ servicePrefix, new Buffer(hash, 'hex') ]));
|
||||
});
|
||||
|
||||
it('should decode mempool transaction key', function() {
|
||||
encoding.decodeMempoolTransactionKey(Buffer.concat([ servicePrefix, new Buffer(hash, 'hex') ])).should.deep.equal(hash);
|
||||
});
|
||||
|
||||
it('should encode mempool transaction value', function() {
|
||||
var mytx = tx.fromRaw(txString, 'hex');
|
||||
mytx.__inputValues = [1012955, 447698, 446664, 391348];
|
||||
encoding.encodeMempoolTransactionValue(mytx).should.deep.equal(new Buffer(txString, 'hex'));
|
||||
});
|
||||
|
||||
it('should decode mempool transaction value', function() {
|
||||
var mytx = encoding.decodeMempoolTransactionValue(new Buffer(txString, 'hex'));
|
||||
mytx.should.deep.equal(tx.fromRaw(txString, 'hex'));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
95
test/services/mempool/index.unit.js
Normal file
95
test/services/mempool/index.unit.js
Normal file
@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
|
||||
var expect = require('chai').expect;
|
||||
var MempoolService = require('../../../lib/services/mempool');
|
||||
var sinon = require('sinon');
|
||||
var Encoding = require('../../../lib/services/mempool/encoding');
|
||||
var bcoin = require('bcoin');
|
||||
var Tx = bcoin.tx;
|
||||
var Block = bcoin.block;
|
||||
|
||||
describe('Mempool Service', function() {
|
||||
|
||||
var mempoolService;
|
||||
var sandbox;
|
||||
var block = Block.fromRaw('010000006a39821735ec18a366d95b391a7ff10dee181a198f1789b0550e0d00000000002b0c80fa52b669022c344c3e09e6bb9698ab90707bb4bb412af3fbf31cfd2163a601514c5a0c011c572aef0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c022003ffffffff0100f2052a01000000434104c5b694d72e601091fd733c6b18b94795c13e2db6b1474747e7be914b407854cad37cee3058f85373b9f9dbb0014e541c45851d5f85e83a1fd7c45e54423718f3ac00000000', 'hex');
|
||||
var tx = Tx.fromRaw( '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000', 'hex');
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
mempoolService = new MempoolService({
|
||||
node: {
|
||||
getNetworkName: function() { return 'regtest'; },
|
||||
services: []
|
||||
}
|
||||
});
|
||||
mempoolService._encoding = new Encoding(new Buffer('0000', 'hex'));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
|
||||
it('should get the db prefix', function(done) {
|
||||
var getPrefix = sandbox.stub().callsArgWith(1, null, new Buffer('0001', 'hex'));
|
||||
var startSubs = sandbox.stub(mempoolService, '_startSubscriptions');
|
||||
mempoolService._db = { getPrefix: getPrefix };
|
||||
|
||||
mempoolService.start(function() {
|
||||
expect(getPrefix.calledOnce).to.be.true;
|
||||
expect(startSubs.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
it('should stop the service', function(done) {
|
||||
mempoolService.stop(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getMempoolTransaction', function() {
|
||||
|
||||
it('should get a mempool transaction', function(done) {
|
||||
var get = sandbox.stub().callsArgWith(1, null, tx.toJSON());
|
||||
var key = sandbox.stub();
|
||||
var val = sandbox.stub().returns(tx.toJSON());
|
||||
mempoolService._encoding = { encodeMempoolTransactionKey: key, decodeMempoolTransactionValue: val };
|
||||
mempoolService._db = { get: get };
|
||||
mempoolService.getMempoolTransaction(tx.hash, function(err, mytx) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
expect(mytx).to.deep.equal(tx.toJSON());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#_onTransaction', function() {
|
||||
|
||||
it('should add the transaction to the database', function() {
|
||||
var put = sandbox.stub();
|
||||
mempoolService._db = { put: put };
|
||||
mempoolService._onTransaction(tx);
|
||||
expect(put.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_onBlock', function() {
|
||||
it('should remove block\'s txs from database', function(done) {
|
||||
mempoolService.onBlock(block, function(err, ops) {
|
||||
expect(ops[0].type).to.deep.equal('del');
|
||||
expect(ops[0].key.toString('hex')).to.deep.equal('00006321fd1cf3fbf32a41bbb47b7090ab9896bbe6093e4c342c0269b652fa800c2b');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
33
test/services/p2p/index.unit.js
Normal file
33
test/services/p2p/index.unit.js
Normal file
@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
var expect = require('chai').expect;
|
||||
var sinon = require('sinon');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var P2PService = require('../../../lib/services/p2p');
|
||||
|
||||
describe('P2P Service', function() {
|
||||
var p2p;
|
||||
var testEmitter;
|
||||
|
||||
before(function(done) {
|
||||
p2p = new P2PService({
|
||||
node: {
|
||||
name: 'p2p',
|
||||
on: sinon.stub()
|
||||
}
|
||||
});
|
||||
sinon.stub(p2p, '_initPool');
|
||||
p2p._pool = new EventEmitter();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should get the mempool from the network', function() {
|
||||
var sendMessage = sinon.stub();
|
||||
var peer = { sendMessage: sendMessage };
|
||||
var getPeer = sinon.stub(p2p, '_getPeer').returns(peer);
|
||||
p2p.getMempool();
|
||||
expect(getPeer.calledOnce).to.be.true;
|
||||
expect(sendMessage.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
49
test/services/timestamp/encoding.unit.js
Normal file
49
test/services/timestamp/encoding.unit.js
Normal file
@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
var should = require('chai').should();
|
||||
|
||||
var Encoding = require('../../../lib/services/timestamp/encoding');
|
||||
|
||||
describe('Timestamp service encoding', function() {
|
||||
|
||||
var servicePrefix = new Buffer('0000', 'hex');
|
||||
var blockPrefix = new Buffer('00', 'hex');
|
||||
var timestampPrefix = new Buffer('01', 'hex');
|
||||
var encoding = new Encoding(servicePrefix);
|
||||
var blockhash = '00000000000000000115b92b1ff4377441049bff75c6c48b626eb99e8b744297';
|
||||
var timestamp = 5;
|
||||
var timestampBuf = new Buffer(4);
|
||||
timestampBuf.writeUInt32BE(timestamp);
|
||||
|
||||
it('should encode block timestamp key' , function() {
|
||||
encoding.encodeBlockTimestampKey(blockhash).should.deep.equal(Buffer.concat([servicePrefix, blockPrefix, new Buffer(blockhash, 'hex')]));
|
||||
});
|
||||
|
||||
it('should decode block timestamp key', function() {
|
||||
var blockTimestampKey = encoding.decodeBlockTimestampKey(Buffer.concat([servicePrefix, blockPrefix, new Buffer(blockhash, 'hex')]));
|
||||
blockTimestampKey.should.equal(blockhash);
|
||||
});
|
||||
|
||||
it('should encode block timestamp value', function() {
|
||||
encoding.encodeBlockTimestampValue(timestamp).should.deep.equal(timestampBuf);
|
||||
});
|
||||
|
||||
it('should decode block timestamp value', function() {
|
||||
encoding.decodeBlockTimestampValue(timestampBuf).should.equal(timestamp);
|
||||
});
|
||||
|
||||
it('should encode timestamp block key', function() {
|
||||
encoding.encodeTimestampBlockKey(timestamp).should.deep.equal(Buffer.concat([servicePrefix, timestampPrefix, timestampBuf]));
|
||||
});
|
||||
|
||||
it('should decode timestamp block key', function() {
|
||||
encoding.decodeTimestampBlockKey(Buffer.concat([servicePrefix, timestampPrefix, timestampBuf])).should.equal(timestamp);
|
||||
});
|
||||
|
||||
it('should encode timestamp block value', function() {
|
||||
encoding.encodeTimestampBlockValue(blockhash).should.deep.equal(new Buffer(blockhash, 'hex'));
|
||||
});
|
||||
|
||||
it('should decode timestamp block value', function() {
|
||||
encoding.decodeTimestampBlockValue(new Buffer(blockhash, 'hex')).should.equal(blockhash);
|
||||
});
|
||||
});
|
||||
42
test/services/transaction/encoding.unit.js
Normal file
42
test/services/transaction/encoding.unit.js
Normal file
@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var Tx = require('bcoin').tx;
|
||||
|
||||
var Encoding = require('../../../lib/services/transaction/encoding');
|
||||
|
||||
describe('Transaction service encoding', function() {
|
||||
|
||||
var servicePrefix = new Buffer('0000', 'hex');
|
||||
var encoding = new Encoding(servicePrefix);
|
||||
var txid = '91b58f19b6eecba94ed0f6e463e8e334ec0bcda7880e2985c82a8f32e4d03add';
|
||||
var txHex = '0100000001cc3ffe0638792c8b39328bb490caaefe2cf418f2ce0144956e0c22515f29724d010000006a473044022030ce9fa68d1a32abf0cd4adecf90fb998375b64fe887c6987278452b068ae74c022036a7d00d1c8af19e298e04f14294c807ebda51a20389ad751b4ff3c032cf8990012103acfcb348abb526526a9f63214639d79183871311c05b2eebc727adfdd016514fffffffff02f6ae7d04000000001976a9144455183e407ee4d3423858c8a3275918aedcd18e88aca99b9b08010000001976a9140beceae2c29bfde08d2b6d80b33067451c5887be88ac00000000';
|
||||
var tx = Tx.fromRaw(txHex, 'hex');
|
||||
var txEncoded = Buffer.concat([new Buffer('00000002', 'hex'), new Buffer('00000001', 'hex'), new Buffer('0002', 'hex'), new Buffer('40000000000000004008000000000000', 'hex'), tx.toRaw()]);
|
||||
|
||||
it('should encode transaction key' , function() {
|
||||
var txBuf = new Buffer(txid, 'hex');
|
||||
encoding.encodeTransactionKey(txid).should.deep.equal(Buffer.concat([servicePrefix, txBuf]));
|
||||
});
|
||||
|
||||
it('should decode transaction key', function() {
|
||||
encoding.decodeTransactionKey(Buffer.concat([servicePrefix, new Buffer(txid, 'hex')]))
|
||||
.should.equal(txid);
|
||||
});
|
||||
|
||||
it('should encode transaction value', function() {
|
||||
tx.__height = 2;
|
||||
tx.__timestamp = 1;
|
||||
tx.__inputValues = [ 2, 3 ];
|
||||
|
||||
encoding.encodeTransactionValue(tx).should.deep.equal(txEncoded);
|
||||
});
|
||||
|
||||
it('should decode transaction value', function() {
|
||||
var tx = encoding.decodeTransactionValue(txEncoded);
|
||||
tx.__height.should.equal(2);
|
||||
tx.__timestamp.should.equal(1);
|
||||
tx.__inputValues.should.deep.equal([2,3]);
|
||||
tx.toRaw().toString('hex').should.equal(txHex);
|
||||
});
|
||||
});
|
||||
126
test/services/transaction/index.unit.js
Normal file
126
test/services/transaction/index.unit.js
Normal file
@ -0,0 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('chai').should();
|
||||
var bcoin = require('bcoin');
|
||||
var Tx = bcoin.tx;
|
||||
var Block = bcoin.block;
|
||||
var sinon = require('sinon');
|
||||
var TxService = require('../../../lib/services/transaction');
|
||||
var Encoding = require('../../../lib/services/transaction/encoding');
|
||||
|
||||
describe('Transaction Service', function() {
|
||||
var block = Block.fromRaw('010000006a39821735ec18a366d95b391a7ff10dee181a198f1789b0550e0d00000000002b0c80fa52b669022c344c3e09e6bb9698ab90707bb4bb412af3fbf31cfd2163a601514c5a0c011c572aef0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08045a0c011c022003ffffffff0100f2052a01000000434104c5b694d72e601091fd733c6b18b94795c13e2db6b1474747e7be914b407854cad37cee3058f85373b9f9dbb0014e541c45851d5f85e83a1fd7c45e54423718f3ac00000000', 'hex');
|
||||
var tx = Tx.fromRaw( '0100000004de9b4bb17f627096a9ee0b4528e4eae17df5b5c69edc29704c2e84a7371db29f010000006b483045022100f5b1a0d33b7be291c3953c25f8ae39d98601aa7099a8674daf638a08b86c7173022006ce372da5ad088a1cc6e5c49c2760a1b6f085eb1b51b502211b6bc9508661f9012102ec5e3731e54475dd2902326f43602a03ae3d62753324139163f81f20e787514cffffffff7a1d4e5fc2b8177ec738cd723a16cf2bf493791e55573445fc0df630fe5e2d64010000006b483045022100cf97f6cb8f126703e9768545dfb20ffb10ba78ae3d101aa46775f5a239b075fc02203150c4a89a11eaf5e404f4f96b62efa4455e9525765a025525c7105a7e47b6db012102c01e11b1d331f999bbdb83e8831de503cd52a01e3834a95ccafd615c67703d77ffffffff9e52447116415ca0d0567418a1a4ef8f27be3ff5a96bf87c922f3723d7db5d7c000000006b483045022100f6c117e536701be41a6b0b544d7c3b1091301e4e64a6265b6eb167b15d16959d022076916de4b115e700964194ce36a24cb9105f86482f4abbc63110c3f537cd5770012102ddf84cc7bee2d6a82ac09628a8ad4a26cd449fc528b81e7e6cc615707b8169dfffffffff5815d9750eb3572e30d6fd9df7afb4dbd76e042f3aa4988ac763b3fdf8397f80010000006a473044022028f4402b736066d93d2a32b28ccd3b7a21d84bb58fcd07fe392a611db94cdec5022018902ee0bf2c3c840c1b81ead4e6c87c88c48b2005bf5eea796464e561a620a8012102b6cdd1a6cd129ef796faeedb0b840fcd0ca00c57e16e38e46ee7028d59812ae7ffffffff0220a10700000000001976a914c342bcd1a7784d9842f7386b8b3b8a3d4171a06e88ac59611100000000001976a91449f8c749a9960dc29b5cbe7d2397cea7d26611bb88ac00000000', 'hex');
|
||||
var txService;
|
||||
var sandbox;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
txService = new TxService({
|
||||
node: {
|
||||
getNetworkName: function() { return 'regtest'; },
|
||||
services: []
|
||||
}
|
||||
});
|
||||
txService._encoding = new Encoding(new Buffer('0000', 'hex'));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#start', function() {
|
||||
it('should get the prefix and the service tip', function(done) {
|
||||
var getPrefix = sandbox.stub().callsArgWith(1, null, new Buffer('ffee', 'hex'));
|
||||
txService._db = { getPrefix: getPrefix };
|
||||
txService.start(function() {
|
||||
getPrefix.calledOnce.should.be.true;
|
||||
txService._encoding.should.be.instanceOf(Encoding);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop', function() {
|
||||
it('should stop the service', function(done) {
|
||||
txService.stop(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendTransaction', function() {
|
||||
it('should send a raw transaction', function(done) {
|
||||
var sendTransaction = sandbox.stub().callsArg(0);
|
||||
txService._p2p = { sendTransaction: sendTransaction };
|
||||
txService.sendTransaction(function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_getBlockTimestamp', function() {
|
||||
it('should get the block\'s timestamp', function() {
|
||||
var getTimestamp = sandbox.stub().returns(1);
|
||||
txService._timestamp = { getTimestampSync: getTimestamp };
|
||||
var timestamp = txService._getBlockTimestamp('aa');
|
||||
timestamp.should.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#onBlock', function() {
|
||||
|
||||
it('should process new blocks that come in from the block service', function(done) {
|
||||
|
||||
var _processTransaction = sandbox.stub(txService, '_processTransaction');
|
||||
|
||||
txService.onBlock(block, function(err, ops) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
_processTransaction.calledOnce.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#_onReorg', function() {
|
||||
it('should perform a reorg', function(done) {
|
||||
var oldList = [];
|
||||
var ops = txService.onReorg([ null, oldList ], function(err, ops) {
|
||||
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
ops.should.deep.equal([]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('#getInputValues', function() {
|
||||
|
||||
it('should add missing input values on a tx', function(done) {
|
||||
|
||||
var put = sandbox.stub().callsArgWith(2, null);
|
||||
txService._db = { put: put };
|
||||
|
||||
sandbox.stub(txService, '_getTransaction').callsArgWith(2, null, tx.txid(), tx);
|
||||
|
||||
tx.__inputValues = [];
|
||||
|
||||
txService.getInputValues(tx, {}, function(err, tx) {
|
||||
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
tx.__inputValues.should.deep.equal([1139033, 1139033, 500000, 1139033]);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -5,7 +5,7 @@ var sinon = require('sinon');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var proxyquire = require('proxyquire');
|
||||
|
||||
var index = require('../../lib');
|
||||
var index = require('../../../lib');
|
||||
var log = index.log;
|
||||
|
||||
var httpStub = {
|
||||
@ -30,7 +30,7 @@ fakeSocket.on('test/event1', function(data) {
|
||||
fakeSocketListener.emit('connection', fakeSocket);
|
||||
fakeSocket.emit('subscribe', 'test/event1');
|
||||
|
||||
var WebService = proxyquire('../../lib/services/web', {http: httpStub, https: httpsStub, fs: fsStub});
|
||||
var WebService = proxyquire('../../../lib/services/web', {http: httpStub, https: httpsStub, fs: fsStub});
|
||||
|
||||
describe('WebService', function() {
|
||||
var defaultNode = new EventEmitter();
|
||||
@ -82,7 +82,7 @@ describe('WebService', function() {
|
||||
it('should pass json request limit to json body parser', function(done) {
|
||||
var node = new EventEmitter();
|
||||
var jsonStub = sinon.stub();
|
||||
var TestWebService = proxyquire('../../lib/services/web', {
|
||||
var TestWebService = proxyquire('../../../lib/services/web', {
|
||||
http: {
|
||||
createServer: sinon.stub()
|
||||
},
|
||||
@ -2,150 +2,124 @@
|
||||
|
||||
var should = require('chai').should();
|
||||
var utils = require('../lib/utils');
|
||||
var sinon = require('sinon');
|
||||
|
||||
describe('Utils', function() {
|
||||
|
||||
describe('#isHash', function() {
|
||||
describe('#isHeight', function() {
|
||||
|
||||
it('false for short string', function() {
|
||||
var a = utils.isHash('ashortstring');
|
||||
a.should.equal(false);
|
||||
it('should detect a height', function() {
|
||||
utils.isHeight(12).should.be.true;
|
||||
});
|
||||
|
||||
it('false for long string', function() {
|
||||
var a = utils.isHash('00000000000000000000000000000000000000000000000000000000000000000');
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for correct length invalid char', function() {
|
||||
var a = utils.isHash('z000000000000000000000000000000000000000000000000000000000000000');
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for invalid type (buffer)', function() {
|
||||
var a = utils.isHash(new Buffer('abcdef', 'hex'));
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for invalid type (number)', function() {
|
||||
var a = utils.isHash(123456);
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('true for hash', function() {
|
||||
var a = utils.isHash('fc63629e2106c3440d7e56751adc8cfa5266a5920c1b54b81565af25aec1998b');
|
||||
a.should.equal(true);
|
||||
it('should detect a non-height', function() {
|
||||
utils.isHeight('aaaaaa').should.be.false;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#isSafeNatural', function() {
|
||||
describe('#isAbsolutePath', function() {
|
||||
|
||||
it('false for float', function() {
|
||||
var a = utils.isSafeNatural(0.1);
|
||||
a.should.equal(false);
|
||||
it('should detect absolute path', function() {
|
||||
utils.isAbsolutePath('/').should.be.true;
|
||||
});
|
||||
|
||||
it('false for string float', function() {
|
||||
var a = utils.isSafeNatural('0.1');
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for string integer', function() {
|
||||
var a = utils.isSafeNatural('1');
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for negative integer', function() {
|
||||
var a = utils.isSafeNatural(-1);
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for negative integer string', function() {
|
||||
var a = utils.isSafeNatural('-1');
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for infinity', function() {
|
||||
var a = utils.isSafeNatural(Infinity);
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for NaN', function() {
|
||||
var a = utils.isSafeNatural(NaN);
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('false for unsafe number', function() {
|
||||
var a = utils.isSafeNatural(Math.pow(2, 53));
|
||||
a.should.equal(false);
|
||||
});
|
||||
|
||||
it('true for positive integer', function() {
|
||||
var a = utils.isSafeNatural(1000);
|
||||
a.should.equal(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#startAtZero', function() {
|
||||
|
||||
it('will set key to zero if not set', function() {
|
||||
var obj = {};
|
||||
utils.startAtZero(obj, 'key');
|
||||
obj.key.should.equal(0);
|
||||
});
|
||||
|
||||
it('not if already set', function() {
|
||||
var obj = {
|
||||
key: 10
|
||||
};
|
||||
utils.startAtZero(obj, 'key');
|
||||
obj.key.should.equal(10);
|
||||
});
|
||||
|
||||
it('not if set to false', function() {
|
||||
var obj = {
|
||||
key: false
|
||||
};
|
||||
utils.startAtZero(obj, 'key');
|
||||
obj.key.should.equal(false);
|
||||
});
|
||||
|
||||
it('not if set to undefined', function() {
|
||||
var obj = {
|
||||
key: undefined
|
||||
};
|
||||
utils.startAtZero(obj, 'key');
|
||||
should.equal(obj.key, undefined);
|
||||
});
|
||||
|
||||
it('not if set to null', function() {
|
||||
var obj = {
|
||||
key: null
|
||||
};
|
||||
utils.startAtZero(obj, 'key');
|
||||
should.equal(obj.key, null);
|
||||
it('should not detect absolute path', function() {
|
||||
utils.isAbsolutePath('.').should.be.false;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#parseParamsWithJSON', function() {
|
||||
it('will parse object', function() {
|
||||
var paramsArg = ['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', '{"start": 100, "end": 1}'];
|
||||
var params = utils.parseParamsWithJSON(paramsArg);
|
||||
params.should.deep.equal(['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', {start: 100, end: 1}]);
|
||||
});
|
||||
it('will parse array', function() {
|
||||
var paramsArg = ['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', '[0, 1]'];
|
||||
var params = utils.parseParamsWithJSON(paramsArg);
|
||||
params.should.deep.equal(['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou', [0, 1]]);
|
||||
});
|
||||
it('will parse numbers', function() {
|
||||
var paramsArg = ['3', 0, 'b', '0', 0x12, '0.0001'];
|
||||
var params = utils.parseParamsWithJSON(paramsArg);
|
||||
params.should.deep.equal([3, 0, 'b', 0, 0x12, 0.0001]);
|
||||
it('should parse json params', function() {
|
||||
utils.parseParamsWithJSON([ '{"test":"1"}', '{}', '[]' ])
|
||||
.should.deep.equal([{test:'1'}, {}, []]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getTerminalKey', function() {
|
||||
it('should get the terminal key for a buffer', function() {
|
||||
utils.getTerminalKey(new Buffer('ffff', 'hex'))
|
||||
.should.deep.equal(new Buffer('010000', 'hex'));
|
||||
});
|
||||
|
||||
it('should get the terminal key for a large buffer', function() {
|
||||
utils.getTerminalKey(Buffer.concat([ new Buffer(new Array(64).join('f'), 'hex'), new Buffer('fe', 'hex') ]))
|
||||
.should.deep.equal(new Buffer('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'hex'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#diffTime', function() {
|
||||
it('should get the difference in time in seconds', function(done) {
|
||||
var time = process.hrtime();
|
||||
setTimeout(function() {
|
||||
var res = utils.diffTime(time);
|
||||
res.should.be.greaterThan(0.1);
|
||||
res.should.be.lessThan(0.5);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendError', function() {
|
||||
it('should send a web-style error', function() {
|
||||
var err = { statusCode: 500, message: 'hi there', stack: 'some stack' };
|
||||
var status = sinon.stub().returnsThis();
|
||||
var send = sinon.stub();
|
||||
var res = { status: status, send: send };
|
||||
utils.sendError(err, res);
|
||||
send.should.be.calledOnce;
|
||||
status.should.be.calledOnce;
|
||||
status.args[0][0].should.equal(500);
|
||||
send.args[0][0].should.equal('hi there');
|
||||
});
|
||||
|
||||
it('should send a 503 in the case where there is no given status code', function() {
|
||||
var err = { message: 'hi there', stack: 'some stack' };
|
||||
var status = sinon.stub().returnsThis();
|
||||
var send = sinon.stub();
|
||||
var res = { status: status, send: send };
|
||||
utils.sendError(err, res);
|
||||
send.should.be.calledOnce;
|
||||
status.should.be.calledOnce;
|
||||
status.args[0][0].should.equal(503);
|
||||
send.args[0][0].should.equal('hi there');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#encodeTip', function() {
|
||||
it('should encode tip', function() {
|
||||
var res = utils.encodeTip({ height: 0xdeadbeef, hash: new Array(65).join('0') }, 'test');
|
||||
res.should.deep.equal({
|
||||
key: new Buffer('ffff7469702d74657374', 'hex'),
|
||||
value: new Buffer('deadbeef00000000000000000000000000000000000000000000000000000000000000000', 'hex')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SimpleMap', function() {
|
||||
var map = new utils.SimpleMap();
|
||||
|
||||
it('should build a simple map', function() {
|
||||
map.should.be.instanceOf(Object);
|
||||
});
|
||||
|
||||
it('should set a key and value', function() {
|
||||
map.set('key', 'value');
|
||||
map.getIndex(0).should.equal('value');
|
||||
});
|
||||
|
||||
it('should get a value for key', function() {
|
||||
map.get('key').should.equal('value');
|
||||
});
|
||||
|
||||
it('should get a get a value at a specific index', function() {
|
||||
map.getIndex(0).should.equal('value');
|
||||
});
|
||||
|
||||
it('should get the last index', function() {
|
||||
map.set('last key', 'last value');
|
||||
map.getLastIndex().should.equal('last value');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user