cryptorescuecore-node
Version:
Full node with extended capabilities using cryptorescuecore and CryptoRescue
1,312 lines (1,272 loc) • 186 kB
JavaScript
'use strict';
/* jshint sub: true */
var path = require('path');
var EventEmitter = require('events').EventEmitter;
var should = require('chai').should();
var crypto = require('crypto');
var cryptorescuecore = require('cryptorescuecore-lib');
var _ = cryptorescuecore.deps._;
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var fs = require('fs');
var sinon = require('sinon');
var index = require('../../lib');
var log = index.log;
var errors = index.errors;
var Transaction = cryptorescuecore.Transaction;
var readFileSync = sinon.stub().returns(fs.readFileSync(path.resolve(__dirname, '../data/cryptorescue.conf')));
var CryptoRescueService = proxyquire('../../lib/services/cryptorescued', {
fs: {
readFileSync: readFileSync
}
});
var defaultCryptoRescueConf = fs.readFileSync(path.resolve(__dirname, '../data/default.cryptorescue.conf'), 'utf8');
describe('CryptoRescue Service', function() {
var txhex = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000';
var baseConfig = {
node: {
network: cryptorescuecore.Networks.testnet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
describe('@constructor', function() {
it('will create an instance', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
should.exist(cryptorescued);
});
it('will create an instance without `new`', function() {
var cryptorescued = CryptoRescueService(baseConfig);
should.exist(cryptorescued);
});
it('will init caches', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
should.exist(cryptorescued.utxosCache);
should.exist(cryptorescued.txidsCache);
should.exist(cryptorescued.balanceCache);
should.exist(cryptorescued.summaryCache);
should.exist(cryptorescued.transactionDetailedCache);
should.exist(cryptorescued.transactionCache);
should.exist(cryptorescued.rawTransactionCache);
should.exist(cryptorescued.blockCache);
should.exist(cryptorescued.rawBlockCache);
should.exist(cryptorescued.blockHeaderCache);
should.exist(cryptorescued.zmqKnownTransactions);
should.exist(cryptorescued.zmqKnownBlocks);
should.exist(cryptorescued.lastTip);
should.exist(cryptorescued.lastTipTimeout);
});
it('will init clients', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.nodes.should.deep.equal([]);
cryptorescued.nodesIndex.should.equal(0);
cryptorescued.nodes.push({client: sinon.stub()});
should.exist(cryptorescued.client);
});
it('will set subscriptions', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.subscriptions.should.deep.equal({
address: {},
rawtransaction: [],
hashblock: []
});
});
});
describe('#_initDefaults', function() {
it('will set transaction concurrency', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued._initDefaults({transactionConcurrency: 10});
cryptorescued.transactionConcurrency.should.equal(10);
cryptorescued._initDefaults({});
cryptorescued.transactionConcurrency.should.equal(5);
});
});
describe('@dependencies', function() {
it('will have no dependencies', function() {
CryptoRescueService.dependencies.should.deep.equal([]);
});
});
describe('#getAPIMethods', function() {
it('will return spec', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var methods = cryptorescued.getAPIMethods();
should.exist(methods);
methods.length.should.equal(21);
});
});
describe('#getPublishEvents', function() {
it('will return spec', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var events = cryptorescued.getPublishEvents();
should.exist(events);
events.length.should.equal(3);
events[0].name.should.equal('cryptorescued/rawtransaction');
events[0].scope.should.equal(cryptorescued);
events[0].subscribe.should.be.a('function');
events[0].unsubscribe.should.be.a('function');
events[1].name.should.equal('cryptorescued/hashblock');
events[1].scope.should.equal(cryptorescued);
events[1].subscribe.should.be.a('function');
events[1].unsubscribe.should.be.a('function');
events[2].name.should.equal('cryptorescued/addresstxid');
events[2].scope.should.equal(cryptorescued);
events[2].subscribe.should.be.a('function');
events[2].unsubscribe.should.be.a('function');
});
it('will call subscribe/unsubscribe with correct args', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.subscribe = sinon.stub();
cryptorescued.unsubscribe = sinon.stub();
var events = cryptorescued.getPublishEvents();
events[0].subscribe('test');
cryptorescued.subscribe.args[0][0].should.equal('rawtransaction');
cryptorescued.subscribe.args[0][1].should.equal('test');
events[0].unsubscribe('test');
cryptorescued.unsubscribe.args[0][0].should.equal('rawtransaction');
cryptorescued.unsubscribe.args[0][1].should.equal('test');
events[1].subscribe('test');
cryptorescued.subscribe.args[1][0].should.equal('hashblock');
cryptorescued.subscribe.args[1][1].should.equal('test');
events[1].unsubscribe('test');
cryptorescued.unsubscribe.args[1][0].should.equal('hashblock');
cryptorescued.unsubscribe.args[1][1].should.equal('test');
});
});
describe('#subscribe', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('will push to subscriptions', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter = {};
cryptorescued.subscribe('hashblock', emitter);
cryptorescued.subscriptions.hashblock[0].should.equal(emitter);
var emitter2 = {};
cryptorescued.subscribe('rawtransaction', emitter2);
cryptorescued.subscriptions.rawtransaction[0].should.equal(emitter2);
});
});
describe('#unsubscribe', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('will remove item from subscriptions', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = {};
var emitter2 = {};
var emitter3 = {};
var emitter4 = {};
var emitter5 = {};
cryptorescued.subscribe('hashblock', emitter1);
cryptorescued.subscribe('hashblock', emitter2);
cryptorescued.subscribe('hashblock', emitter3);
cryptorescued.subscribe('hashblock', emitter4);
cryptorescued.subscribe('hashblock', emitter5);
cryptorescued.subscriptions.hashblock.length.should.equal(5);
cryptorescued.unsubscribe('hashblock', emitter3);
cryptorescued.subscriptions.hashblock.length.should.equal(4);
cryptorescued.subscriptions.hashblock[0].should.equal(emitter1);
cryptorescued.subscriptions.hashblock[1].should.equal(emitter2);
cryptorescued.subscriptions.hashblock[2].should.equal(emitter4);
cryptorescued.subscriptions.hashblock[3].should.equal(emitter5);
});
it('will not remove item an already unsubscribed item', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = {};
var emitter3 = {};
cryptorescued.subscriptions.hashblock= [emitter1];
cryptorescued.unsubscribe('hashblock', emitter3);
cryptorescued.subscriptions.hashblock.length.should.equal(1);
cryptorescued.subscriptions.hashblock[0].should.equal(emitter1);
});
});
describe('#subscribeAddress', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('will not an invalid address', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter = new EventEmitter();
cryptorescued.subscribeAddress(emitter, ['invalidaddress']);
should.not.exist(cryptorescued.subscriptions.address['invalidaddress']);
});
it('will add a valid address', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter = new EventEmitter();
cryptorescued.subscribeAddress(emitter, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
should.exist(cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
});
it('will handle multiple address subscribers', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
var emitter2 = new EventEmitter();
cryptorescued.subscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.subscribeAddress(emitter2, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
should.exist(cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(2);
});
it('will not add the same emitter twice', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
cryptorescued.subscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.subscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
should.exist(cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1);
});
});
describe('#unsubscribeAddress', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('it will remove a subscription', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
var emitter2 = new EventEmitter();
cryptorescued.subscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.subscribeAddress(emitter2, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
should.exist(cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(2);
cryptorescued.unsubscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1);
});
it('will unsubscribe subscriptions for an emitter', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
var emitter2 = new EventEmitter();
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2];
cryptorescued.unsubscribeAddress(emitter1);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1);
});
it('will NOT unsubscribe subscription with missing address', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
var emitter2 = new EventEmitter();
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2];
cryptorescued.unsubscribeAddress(emitter1, ['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo']);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(2);
});
it('will NOT unsubscribe subscription with missing emitter', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
var emitter2 = new EventEmitter();
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter2];
cryptorescued.unsubscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'][0].should.equal(emitter2);
});
it('will remove empty addresses', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
var emitter2 = new EventEmitter();
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2];
cryptorescued.unsubscribeAddress(emitter1, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
cryptorescued.unsubscribeAddress(emitter2, ['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
should.not.exist(cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br']);
});
it('will unsubscribe emitter for all addresses', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
var emitter2 = new EventEmitter();
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2];
cryptorescued.subscriptions.address['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'] = [emitter1, emitter2];
sinon.spy(cryptorescued, 'unsubscribeAddressAll');
cryptorescued.unsubscribeAddress(emitter1);
cryptorescued.unsubscribeAddressAll.callCount.should.equal(1);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1);
cryptorescued.subscriptions.address['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'].length.should.equal(1);
});
});
describe('#unsubscribeAddressAll', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('will unsubscribe emitter for all addresses', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var emitter1 = new EventEmitter();
var emitter2 = new EventEmitter();
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'] = [emitter1, emitter2];
cryptorescued.subscriptions.address['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'] = [emitter1, emitter2];
cryptorescued.subscriptions.address['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'] = [emitter2];
cryptorescued.subscriptions.address['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou'] = [emitter1];
cryptorescued.unsubscribeAddress(emitter1);
cryptorescued.subscriptions.address['2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br'].length.should.equal(1);
cryptorescued.subscriptions.address['1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo'].length.should.equal(1);
cryptorescued.subscriptions.address['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'].length.should.equal(1);
should.not.exist(cryptorescued.subscriptions.address['3CMNFxN1oHBc4R1EpboAL5yzHGgE611Xou']);
});
});
describe('#_getDefaultConfig', function() {
it('will generate config file from defaults', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var config = cryptorescued._getDefaultConfig();
config.should.equal(defaultCryptoRescueConf);
});
});
describe('#_loadSpawnConfiguration', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('will parse a cryptorescue.conf file', function() {
var TestCryptoRescue = proxyquire('../../lib/services/cryptorescued', {
fs: {
readFileSync: readFileSync,
existsSync: sinon.stub().returns(true),
writeFileSync: sinon.stub()
},
mkdirp: {
sync: sinon.stub()
}
});
var cryptorescued = new TestCryptoRescue(baseConfig);
cryptorescued.options.spawn.datadir = '/tmp/.cryptorescue';
var node = {};
cryptorescued._loadSpawnConfiguration(node);
should.exist(cryptorescued.spawn.config);
cryptorescued.spawn.config.should.deep.equal({
addressindex: 1,
checkblocks: 144,
dbcache: 8192,
maxuploadtarget: 1024,
port: 20000,
rpcport: 50001,
rpcallowip: '127.0.0.1',
rpcuser: 'cryptorescue',
rpcpassword: 'local321',
server: 1,
spentindex: 1,
timestampindex: 1,
txindex: 1,
upnp: 0,
whitelist: '127.0.0.1',
zmqpubhashblock: 'tcp://127.0.0.1:28332',
zmqpubrawtx: 'tcp://127.0.0.1:28332'
});
});
it('will expand relative datadir to absolute path', function() {
var TestCryptoRescue = proxyquire('../../lib/services/cryptorescued', {
fs: {
readFileSync: readFileSync,
existsSync: sinon.stub().returns(true),
writeFileSync: sinon.stub()
},
mkdirp: {
sync: sinon.stub()
}
});
var config = {
node: {
network: cryptorescuecore.Networks.testnet,
configPath: '/tmp/.cryptorescuecore/cryptorescuecore-node.json'
},
spawn: {
datadir: './data',
exec: 'testpath'
}
};
var cryptorescued = new TestCryptoRescue(config);
cryptorescued.options.spawn.datadir = './data';
var node = {};
cryptorescued._loadSpawnConfiguration(node);
cryptorescued.options.spawn.datadir.should.equal('/tmp/.cryptorescuecore/data');
});
it('should throw an exception if txindex isn\'t enabled in the configuration', function() {
var TestCryptoRescue = proxyquire('../../lib/services/cryptorescued', {
fs: {
readFileSync: sinon.stub().returns(fs.readFileSync(__dirname + '/../data/badcryptorescue.conf')),
existsSync: sinon.stub().returns(true),
},
mkdirp: {
sync: sinon.stub()
}
});
var cryptorescued = new TestCryptoRescue(baseConfig);
(function() {
cryptorescued._loadSpawnConfiguration({datadir: './test'});
}).should.throw(cryptorescuecore.errors.InvalidState);
});
it('should NOT set https options if node https options are set', function() {
var writeFileSync = function(path, config) {
config.should.equal(defaultCryptoRescueConf);
};
var TestCryptoRescue = proxyquire('../../lib/services/cryptorescued', {
fs: {
writeFileSync: writeFileSync,
readFileSync: readFileSync,
existsSync: sinon.stub().returns(false)
},
mkdirp: {
sync: sinon.stub()
}
});
var config = {
node: {
network: {
name: 'regtest'
},
https: true,
httpsOptions: {
key: 'key.pem',
cert: 'cert.pem'
}
},
spawn: {
datadir: 'testdir',
exec: 'testexec'
}
};
var cryptorescued = new TestCryptoRescue(config);
cryptorescued.options.spawn.datadir = '/tmp/.cryptorescue';
var node = {};
cryptorescued._loadSpawnConfiguration(node);
});
});
describe('#_checkConfigIndexes', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'warn');
});
afterEach(function() {
sandbox.restore();
});
it('should warn the user if reindex is set to 1 in the cryptorescue.conf file', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var config = {
txindex: 1,
addressindex: 1,
spentindex: 1,
server: 1,
zmqpubrawtx: 1,
zmqpubhashblock: 1,
reindex: 1
};
var node = {};
cryptorescued._checkConfigIndexes(config, node);
log.warn.callCount.should.equal(1);
node._reindex.should.equal(true);
});
it('should warn if zmq port and hosts do not match', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var config = {
txindex: 1,
addressindex: 1,
spentindex: 1,
server: 1,
zmqpubrawtx: 'tcp://127.0.0.1:28332',
zmqpubhashblock: 'tcp://127.0.0.1:28331',
reindex: 1
};
var node = {};
(function() {
cryptorescued._checkConfigIndexes(config, node);
}).should.throw('"zmqpubrawtx" and "zmqpubhashblock"');
});
});
describe('#_resetCaches', function() {
it('will reset LRU caches', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var keys = [];
for (var i = 0; i < 10; i++) {
keys.push(crypto.randomBytes(32));
cryptorescued.transactionDetailedCache.set(keys[i], {});
cryptorescued.utxosCache.set(keys[i], {});
cryptorescued.txidsCache.set(keys[i], {});
cryptorescued.balanceCache.set(keys[i], {});
cryptorescued.summaryCache.set(keys[i], {});
}
cryptorescued._resetCaches();
should.equal(cryptorescued.transactionDetailedCache.get(keys[0]), undefined);
should.equal(cryptorescued.utxosCache.get(keys[0]), undefined);
should.equal(cryptorescued.txidsCache.get(keys[0]), undefined);
should.equal(cryptorescued.balanceCache.get(keys[0]), undefined);
should.equal(cryptorescued.summaryCache.get(keys[0]), undefined);
});
});
describe('#_tryAllClients', function() {
it('will retry for each node client', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.tryAllInterval = 1;
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArgWith(0, new Error('test'))
}
});
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArgWith(0, new Error('test'))
}
});
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArg(0)
}
});
cryptorescued._tryAllClients(function(client, next) {
client.getInfo(next);
}, function(err) {
if (err) {
return done(err);
}
cryptorescued.nodes[0].client.getInfo.callCount.should.equal(1);
cryptorescued.nodes[1].client.getInfo.callCount.should.equal(1);
cryptorescued.nodes[2].client.getInfo.callCount.should.equal(1);
done();
});
});
it('will start using the current node index (round-robin)', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.tryAllInterval = 1;
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArgWith(0, new Error('2'))
}
});
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArgWith(0, new Error('3'))
}
});
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArgWith(0, new Error('1'))
}
});
cryptorescued.nodesIndex = 2;
cryptorescued._tryAllClients(function(client, next) {
client.getInfo(next);
}, function(err) {
err.should.be.instanceOf(Error);
err.message.should.equal('3');
cryptorescued.nodes[0].client.getInfo.callCount.should.equal(1);
cryptorescued.nodes[1].client.getInfo.callCount.should.equal(1);
cryptorescued.nodes[2].client.getInfo.callCount.should.equal(1);
cryptorescued.nodesIndex.should.equal(2);
done();
});
});
it('will get error if all clients fail', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.tryAllInterval = 1;
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArgWith(0, new Error('test'))
}
});
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArgWith(0, new Error('test'))
}
});
cryptorescued.nodes.push({
client: {
getInfo: sinon.stub().callsArgWith(0, new Error('test'))
}
});
cryptorescued._tryAllClients(function(client, next) {
client.getInfo(next);
}, function(err) {
should.exist(err);
err.should.be.instanceOf(Error);
err.message.should.equal('test');
done();
});
});
});
describe('#_wrapRPCError', function() {
it('will convert cryptorescued-rpc error object into JavaScript error', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var error = cryptorescued._wrapRPCError({message: 'Test error', code: -1});
error.should.be.an.instanceof(errors.RPCError);
error.code.should.equal(-1);
error.message.should.equal('Test error');
});
});
describe('#_initChain', function() {
var sandbox = sinon.sandbox.create();
beforeEach(function() {
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('will set height and genesis buffer', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var genesisBuffer = new Buffer([]);
cryptorescued.getRawBlock = sinon.stub().callsArgWith(1, null, genesisBuffer);
cryptorescued.nodes.push({
client: {
getBestBlockHash: function(callback) {
callback(null, {
result: 'bestblockhash'
});
},
getBlock: function(hash, callback) {
if (hash === 'bestblockhash') {
callback(null, {
result: {
height: 5000
}
});
}
},
getBlockHash: function(num, callback) {
callback(null, {
result: 'genesishash'
});
}
}
});
cryptorescued._initChain(function() {
log.info.callCount.should.equal(1);
cryptorescued.getRawBlock.callCount.should.equal(1);
cryptorescued.getRawBlock.args[0][0].should.equal('genesishash');
cryptorescued.height.should.equal(5000);
cryptorescued.genesisBuffer.should.equal(genesisBuffer);
done();
});
});
it('it will handle error from getBestBlockHash', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var getBestBlockHash = sinon.stub().callsArgWith(0, {code: -1, message: 'error'});
cryptorescued.nodes.push({
client: {
getBestBlockHash: getBestBlockHash
}
});
cryptorescued._initChain(function(err) {
err.should.be.instanceOf(Error);
done();
});
});
it('it will handle error from getBlock', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var getBestBlockHash = sinon.stub().callsArgWith(0, null, {});
var getBlock = sinon.stub().callsArgWith(1, {code: -1, message: 'error'});
cryptorescued.nodes.push({
client: {
getBestBlockHash: getBestBlockHash,
getBlock: getBlock
}
});
cryptorescued._initChain(function(err) {
err.should.be.instanceOf(Error);
done();
});
});
it('it will handle error from getBlockHash', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var getBestBlockHash = sinon.stub().callsArgWith(0, null, {});
var getBlock = sinon.stub().callsArgWith(1, null, {
result: {
height: 10
}
});
var getBlockHash = sinon.stub().callsArgWith(1, {code: -1, message: 'error'});
cryptorescued.nodes.push({
client: {
getBestBlockHash: getBestBlockHash,
getBlock: getBlock,
getBlockHash: getBlockHash
}
});
cryptorescued._initChain(function(err) {
err.should.be.instanceOf(Error);
done();
});
});
it('it will handle error from getRawBlock', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var getBestBlockHash = sinon.stub().callsArgWith(0, null, {});
var getBlock = sinon.stub().callsArgWith(1, null, {
result: {
height: 10
}
});
var getBlockHash = sinon.stub().callsArgWith(1, null, {});
cryptorescued.nodes.push({
client: {
getBestBlockHash: getBestBlockHash,
getBlock: getBlock,
getBlockHash: getBlockHash
}
});
cryptorescued.getRawBlock = sinon.stub().callsArgWith(1, new Error('test'));
cryptorescued._initChain(function(err) {
err.should.be.instanceOf(Error);
done();
});
});
});
describe('#_getDefaultConf', function() {
afterEach(function() {
cryptorescuecore.Networks.disableRegtest();
baseConfig.node.network = cryptorescuecore.Networks.testnet;
});
it('will get default rpc port for livenet', function() {
var config = {
node: {
network: cryptorescuecore.Networks.livenet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
var cryptorescued = new CryptoRescueService(config);
cryptorescued._getDefaultConf().rpcport.should.equal(8766);
});
it('will get default rpc port for testnet', function() {
var config = {
node: {
network: cryptorescuecore.Networks.testnet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
var cryptorescued = new CryptoRescueService(config);
cryptorescued._getDefaultConf().rpcport.should.equal(18332);
});
it('will get default rpc port for regtest', function() {
cryptorescuecore.Networks.enableRegtest();
var config = {
node: {
network: cryptorescuecore.Networks.testnet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
var cryptorescued = new CryptoRescueService(config);
cryptorescued._getDefaultConf().rpcport.should.equal(18332);
});
});
describe('#_getNetworkConfigPath', function() {
afterEach(function() {
cryptorescuecore.Networks.disableRegtest();
baseConfig.node.network = cryptorescuecore.Networks.testnet;
});
it('will get default config path for livenet', function() {
var config = {
node: {
network: cryptorescuecore.Networks.livenet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
var cryptorescued = new CryptoRescueService(config);
should.equal(cryptorescued._getNetworkConfigPath(), undefined);
});
it('will get default rpc port for testnet', function() {
var config = {
node: {
network: cryptorescuecore.Networks.testnet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
var cryptorescued = new CryptoRescueService(config);
cryptorescued._getNetworkConfigPath().should.equal('testnet3/cryptorescue.conf');
});
it('will get default rpc port for regtest', function() {
cryptorescuecore.Networks.enableRegtest();
var config = {
node: {
network: cryptorescuecore.Networks.testnet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
var cryptorescued = new CryptoRescueService(config);
cryptorescued._getNetworkConfigPath().should.equal('regtest/cryptorescue.conf');
});
});
describe('#_getNetworkOption', function() {
afterEach(function() {
cryptorescuecore.Networks.disableRegtest();
baseConfig.node.network = cryptorescuecore.Networks.testnet;
});
it('return --testnet for testnet', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.node.network = cryptorescuecore.Networks.testnet;
cryptorescued._getNetworkOption().should.equal('--testnet');
});
it('return --regtest for testnet', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.node.network = cryptorescuecore.Networks.testnet;
cryptorescuecore.Networks.enableRegtest();
cryptorescued._getNetworkOption().should.equal('--regtest');
});
it('return undefined for livenet', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.node.network = cryptorescuecore.Networks.livenet;
cryptorescuecore.Networks.enableRegtest();
should.equal(cryptorescued._getNetworkOption(), undefined);
});
});
describe('#_zmqBlockHandler', function() {
it('will emit block', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var node = {};
var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex');
cryptorescued._rapidProtectedUpdateTip = sinon.stub();
cryptorescued.on('block', function(block) {
block.should.equal(message);
done();
});
cryptorescued._zmqBlockHandler(node, message);
});
it('will not emit same block twice', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var node = {};
var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex');
cryptorescued._rapidProtectedUpdateTip = sinon.stub();
cryptorescued.on('block', function(block) {
block.should.equal(message);
done();
});
cryptorescued._zmqBlockHandler(node, message);
cryptorescued._zmqBlockHandler(node, message);
});
it('will call function to update tip', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var node = {};
var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex');
cryptorescued._rapidProtectedUpdateTip = sinon.stub();
cryptorescued._zmqBlockHandler(node, message);
cryptorescued._rapidProtectedUpdateTip.callCount.should.equal(1);
cryptorescued._rapidProtectedUpdateTip.args[0][0].should.equal(node);
cryptorescued._rapidProtectedUpdateTip.args[0][1].should.equal(message);
});
it('will emit to subscribers', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var node = {};
var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex');
cryptorescued._rapidProtectedUpdateTip = sinon.stub();
var emitter = new EventEmitter();
cryptorescued.subscriptions.hashblock.push(emitter);
emitter.on('cryptorescued/hashblock', function(blockHash) {
blockHash.should.equal(message.toString('hex'));
done();
});
cryptorescued._zmqBlockHandler(node, message);
});
});
describe('#_rapidProtectedUpdateTip', function() {
it('will limit tip updates with rapid calls', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var callCount = 0;
cryptorescued._updateTip = function() {
callCount++;
callCount.should.be.within(1, 2);
if (callCount > 1) {
done();
}
};
var node = {};
var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex');
var count = 0;
function repeat() {
cryptorescued._rapidProtectedUpdateTip(node, message);
count++;
if (count < 50) {
repeat();
}
}
repeat();
});
});
describe('#_updateTip', function() {
var sandbox = sinon.sandbox.create();
var message = new Buffer('00000000002e08fc7ae9a9aa5380e95e2adcdc5752a4a66a7d3a22466bd4e6aa', 'hex');
beforeEach(function() {
sandbox.stub(log, 'error');
sandbox.stub(log, 'info');
});
afterEach(function() {
sandbox.restore();
});
it('log and emit rpc error from get block', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.syncPercentage = sinon.stub();
cryptorescued.on('error', function(err) {
err.code.should.equal(-1);
err.message.should.equal('Test error');
log.error.callCount.should.equal(1);
done();
});
var node = {
client: {
getBlock: sinon.stub().callsArgWith(1, {message: 'Test error', code: -1})
}
};
cryptorescued._updateTip(node, message);
});
it('emit synced if percentage is 100', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.syncPercentage = sinon.stub().callsArgWith(0, null, 100);
cryptorescued.on('synced', function() {
done();
});
var node = {
client: {
getBlock: sinon.stub()
}
};
cryptorescued._updateTip(node, message);
});
it('NOT emit synced if percentage is less than 100', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.syncPercentage = sinon.stub().callsArgWith(0, null, 99);
cryptorescued.on('synced', function() {
throw new Error('Synced called');
});
var node = {
client: {
getBlock: sinon.stub()
}
};
cryptorescued._updateTip(node, message);
log.info.callCount.should.equal(1);
done();
});
it('log and emit error from syncPercentage', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.syncPercentage = sinon.stub().callsArgWith(0, new Error('test'));
cryptorescued.on('error', function(err) {
log.error.callCount.should.equal(1);
err.message.should.equal('test');
done();
});
var node = {
client: {
getBlock: sinon.stub()
}
};
cryptorescued._updateTip(node, message);
});
it('reset caches and set height', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.syncPercentage = sinon.stub();
cryptorescued._resetCaches = sinon.stub();
cryptorescued.on('tip', function(height) {
cryptorescued._resetCaches.callCount.should.equal(1);
height.should.equal(10);
cryptorescued.height.should.equal(10);
done();
});
var node = {
client: {
getBlock: sinon.stub().callsArgWith(1, null, {
result: {
height: 10
}
})
}
};
cryptorescued._updateTip(node, message);
});
it('will NOT update twice for the same hash', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued.syncPercentage = sinon.stub();
cryptorescued._resetCaches = sinon.stub();
cryptorescued.on('tip', function() {
done();
});
var node = {
client: {
getBlock: sinon.stub().callsArgWith(1, null, {
result: {
height: 10
}
})
}
};
cryptorescued._updateTip(node, message);
cryptorescued._updateTip(node, message);
});
it('will not call syncPercentage if node is stopping', function(done) {
var config = {
node: {
network: cryptorescuecore.Networks.testnet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
var cryptorescued = new CryptoRescueService(config);
cryptorescued.syncPercentage = sinon.stub();
cryptorescued._resetCaches = sinon.stub();
cryptorescued.node.stopping = true;
var node = {
client: {
getBlock: sinon.stub().callsArgWith(1, null, {
result: {
height: 10
}
})
}
};
cryptorescued.on('tip', function() {
cryptorescued.syncPercentage.callCount.should.equal(0);
done();
});
cryptorescued._updateTip(node, message);
});
});
describe('#_getAddressesFromTransaction', function() {
it('will get results using cryptorescuecore.Transaction', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var wif = 'L2Gkw3kKJ6N24QcDuH4XDqt9cTqsKTVNDGz1CRZhk9cq4auDUbJy';
var privkey = cryptorescuecore.PrivateKey.fromWIF(wif);
var inputAddress = privkey.toAddress(cryptorescuecore.Networks.testnet);
var outputAddress = cryptorescuecore.Address('2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br');
var tx = cryptorescuecore.Transaction();
tx.from({
txid: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
outputIndex: 0,
script: cryptorescuecore.Script(inputAddress),
address: inputAddress.toString(),
satoshis: 5000000000
});
tx.to(outputAddress, 5000000000);
tx.sign(privkey);
var addresses = cryptorescued._getAddressesFromTransaction(tx);
addresses.length.should.equal(2);
addresses[0].should.equal(inputAddress.toString());
addresses[1].should.equal(outputAddress.toString());
});
it('will handle non-standard script types', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var tx = cryptorescuecore.Transaction();
tx.addInput(cryptorescuecore.Transaction.Input({
prevTxId: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
script: cryptorescuecore.Script('OP_TRUE'),
outputIndex: 1,
output: {
script: cryptorescuecore.Script('OP_TRUE'),
satoshis: 5000000000
}
}));
tx.addOutput(cryptorescuecore.Transaction.Output({
script: cryptorescuecore.Script('OP_TRUE'),
satoshis: 5000000000
}));
var addresses = cryptorescued._getAddressesFromTransaction(tx);
addresses.length.should.equal(0);
});
it('will handle unparsable script types or missing input script', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var tx = cryptorescuecore.Transaction();
tx.addOutput(cryptorescuecore.Transaction.Output({
script: new Buffer('4c', 'hex'),
satoshis: 5000000000
}));
var addresses = cryptorescued._getAddressesFromTransaction(tx);
addresses.length.should.equal(0);
});
it('will return unique values', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var tx = cryptorescuecore.Transaction();
var address = cryptorescuecore.Address('2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br');
tx.addOutput(cryptorescuecore.Transaction.Output({
script: cryptorescuecore.Script(address),
satoshis: 5000000000
}));
tx.addOutput(cryptorescuecore.Transaction.Output({
script: cryptorescuecore.Script(address),
satoshis: 5000000000
}));
var addresses = cryptorescued._getAddressesFromTransaction(tx);
addresses.length.should.equal(1);
});
});
describe('#_notifyAddressTxidSubscribers', function() {
it('will emit event if matching addresses', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo';
cryptorescued._getAddressesFromTransaction = sinon.stub().returns([address]);
var emitter = new EventEmitter();
cryptorescued.subscriptions.address[address] = [emitter];
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
var transaction = {};
emitter.on('cryptorescued/addresstxid', function(data) {
data.address.should.equal(address);
data.txid.should.equal(txid);
done();
});
sinon.spy(emitter, 'emit');
cryptorescued._notifyAddressTxidSubscribers(txid, transaction);
emitter.emit.callCount.should.equal(1);
});
it('will NOT emit event without matching addresses', function() {
var cryptorescued = new CryptoRescueService(baseConfig);
var address = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo';
cryptorescued._getAddressesFromTransaction = sinon.stub().returns([address]);
var emitter = new EventEmitter();
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
var transaction = {};
emitter.emit = sinon.stub();
cryptorescued._notifyAddressTxidSubscribers(txid, transaction);
emitter.emit.callCount.should.equal(0);
});
});
describe('#_zmqTransactionHandler', function() {
it('will emit to subscribers', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var expectedBuffer = new Buffer(txhex, 'hex');
var emitter = new EventEmitter();
cryptorescued.subscriptions.rawtransaction.push(emitter);
emitter.on('cryptorescued/rawtransaction', function(hex) {
hex.should.be.a('string');
hex.should.equal(expectedBuffer.toString('hex'));
done();
});
var node = {};
cryptorescued._zmqTransactionHandler(node, expectedBuffer);
});
it('will NOT emit to subscribers more than once for the same tx', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var expectedBuffer = new Buffer(txhex, 'hex');
var emitter = new EventEmitter();
cryptorescued.subscriptions.rawtransaction.push(emitter);
emitter.on('cryptorescued/rawtransaction', function() {
done();
});
var node = {};
cryptorescued._zmqTransactionHandler(node, expectedBuffer);
cryptorescued._zmqTransactionHandler(node, expectedBuffer);
});
it('will emit "tx" event', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var expectedBuffer = new Buffer(txhex, 'hex');
cryptorescued.on('tx', function(buffer) {
buffer.should.be.instanceof(Buffer);
buffer.toString('hex').should.equal(expectedBuffer.toString('hex'));
done();
});
var node = {};
cryptorescued._zmqTransactionHandler(node, expectedBuffer);
});
it('will NOT emit "tx" event more than once for the same tx', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
var expectedBuffer = new Buffer(txhex, 'hex');
cryptorescued.on('tx', function() {
done();
});
var node = {};
cryptorescued._zmqTransactionHandler(node, expectedBuffer);
cryptorescued._zmqTransactionHandler(node, expectedBuffer);
});
});
describe('#_checkSyncedAndSubscribeZmqEvents', function() {
var sandbox = sinon.sandbox.create();
before(function() {
sandbox.stub(log, 'error');
});
after(function() {
sandbox.restore();
});
it('log errors, update tip and subscribe to zmq events', function(done) {
var cryptorescued = new CryptoRescueService(baseConfig);
cryptorescued._updateTip = sinon.stub();
cryptorescued._subscribeZmqEvents = sinon.stub();
var blockEvents = 0;
cryptorescued.on('block', function() {
blockEvents++;
});
var getBestBlockHash = sinon.stub().callsArgWith(0, null, {
result: '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45'
});
getBestBlockHash.onCall(0).callsArgWith(0, {code: -1 , message: 'Test error'});
var progress = 0.90;
function getProgress() {
progress = progress + 0.01;
return progress;
}
var info = {};
Object.defineProperty(info, 'result', {
get: function() {
return {
verificationprogress: getProgress()
};
}
});
var getBlockchainInfo = sinon.stub().callsArgWith(0, null, info);
getBlockchainInfo.onCall(0).callsArgWith(0, {code: -1, message: 'Test error'});
var node = {
_reindex: true,
_reindexWait: 1,
_tipUpdateInterval: 1,
client: {
getBestBlockHash: getBestBlockHash,
getBlockchainInfo: getBlockchainInfo
}
};
cryptorescued._checkSyncedAndSubscribeZmqEvents(node);
setTimeout(function() {
log.error.callCount.should.equal(2);
blockEvents.should.equal(11);
cryptorescued._updateTip.callCount.should.equal(11);
cryptorescued._subscribeZmqEvents.callCount.should.equal(1);
done();
}, 200);
});
it('it will clear interval if node is stopping', function(done) {
var config = {
node: {
network: cryptorescuecore.Networks.testnet
},
spawn: {
datadir: 'testdir',
exec: 'testpath'
}
};
var cryptorescued = new CryptoRescueService(config);
var getBestBlockHash = sinon.stub().callsArgWith(0, {code: -1, message: 'error'});
var node = {
_tipUpdateInterval: 1,
client: {
getBestBlockHash: getBestBlockHash
}
};
cryptorescued._checkSyncedAndSubscribeZmqEvents(node);
setTimeout(function() {
cryptorescued.node.stopping = true;
var count = getBestBlockHash.callCount;
setTimeout(function() {
getBestBlo