UNPKG

cryptorescuecore-node

Version:

Full node with extended capabilities using cryptorescuecore and CryptoRescue

1,312 lines (1,272 loc) 186 kB
'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