diff --git a/.gitignore b/.gitignore index d55ff456982..d0501c27e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ release ssl/ stacktrace* tmp +sftp-config.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..5d325b40989 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,52 @@ +dist: 'trusty' +language: node_js +node_js: + - '6.9.4' +cache: + yarn: true + directories: + - test/lisk-js +services: + - postgresql +addons: + postgresql: '9.6' +install: + - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - + - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + - sudo apt-get update && sudo apt-get install yarn + - yarn +before_script: + - createdb lisk_test + - psql -d lisk_test -c "alter user "$USER" with password 'password';" + - wget https://downloads.lisk.io/lisk-node/lisk-node-Linux-x86_64.tar.gz + - tar -zxvf lisk-node-Linux-x86_64.tar.gz + - cd test/lisk-js/; yarn; cd ../.. + - cp test/config.json test/genesisBlock.json . + - node app.js &> .app.log & +env: + matrix: + - TEST=test/api/peer.transactions.stress.js + - TEST=test/api/peer.transactions.votes.js + - TEST=test/api/delegates.js + - TEST=test/api/accounts.js + - TEST=test/api/blocks.js + - TEST=test/api/dapps.js + - TEST=test/api/loader.js + - TEST=test/api/multisignatures.js + - TEST=test/api/peer.js + - TEST=test/api/peer.dapp.js + - TEST=test/api/peer.blocks.js + - TEST=test/api/peer.signatures.js + - TEST=test/api/peer.transactions.collision.js + - TEST=test/api/peer.transactions.delegates.js + - TEST=test/api/peer.transactions.main.js + - TEST=test/api/peer.transactions.signatures.js + - TEST=test/api/peers.js + - TEST=test/api/signatures.js + - TEST=test/api/transactions.js + + - TEST=test/unit/helpers + - TEST=test/unit/logic +script: 'npm run travis' +after_failure: + - cat .app.log diff --git a/Gruntfile.js b/Gruntfile.js index e0c1392c815..e504babab1e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -21,6 +21,8 @@ module.exports = function (grunt) { var release_dir = __dirname + '/release/', version_dir = release_dir + config.version; + var maxBufferSize = require('buffer').kMaxLength - 1; + grunt.initConfig({ obfuscator: { files: files, @@ -68,6 +70,14 @@ module.exports = function (grunt) { }, build: { command: 'cd ' + version_dir + '/ && touch build && echo "v' + today + '" > build' + }, + coverage: { + command: 'node_modules/.bin/istanbul cover --dir test/.coverage ./node_modules/.bin/mocha', + maxBuffer: maxBufferSize + }, + coverageSingle: { + command: 'node_modules/.bin/istanbul cover --dir test/.$TEST_coverage ./node_modules/.bin/mocha $TEST', + maxBuffer: maxBufferSize } }, @@ -126,4 +136,6 @@ module.exports = function (grunt) { grunt.registerTask('default', ['release']); grunt.registerTask('release', ['exec:folder', 'obfuscator', 'exec:package', 'exec:build', 'compress']); + grunt.registerTask('travis', ['jshint', 'exec:coverageSingle']); + grunt.registerTask('test', ['jshint', 'exec:coverage']); }; diff --git a/README.md b/README.md index 52a5060bd24..6dbb6a2139d 100644 --- a/README.md +++ b/README.md @@ -3,49 +3,57 @@ Lisk is a next generation crypto-currency and decentralized application platform, written entirely in JavaScript. For more information please refer to our website: https://lisk.io/. [![Join the chat at https://gitter.im/LiskHQ/lisk](https://badges.gitter.im/LiskHQ/lisk.svg)](https://gitter.im/LiskHQ/lisk?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://travis-ci.org/LiskHQ/lisk.svg?branch=development)](https://travis-ci.org/LiskHQ/lisk) -## Installation +**NOTE:** The following information is applicable to: **Ubuntu 14.04 (LTS) - x86_64**. -**NOTE:** The following is applicable to: **Ubuntu 14.04 (LTS) - x86_64**. +## Prerequisites - In order -Install essentials: +- Tool chain components -- Used for compiling dependencies -``` -sudo apt-get update -sudo apt-get install -y autoconf automake build-essential curl git libtool python -``` + `sudo apt-get install -y python build-essential curl automake autoconf libtool` + +- Git () -- Used for cloning and updating Lisk -Install PostgreSQL (version 9.5.2): + `sudo apt-get install -y git` -``` -curl -sL "https://downloads.lisk.io/scripts/setup_postgresql.Linux" | bash - -sudo -u postgres createuser --createdb $USER -createdb lisk_test -sudo -u postgres psql -d lisk_test -c "alter user "$USER" with password 'password';" -``` +- Nodejs v0.12.17 () -- Nodejs serves as the underlying engine for code execution. -Install Node.js (version 0.12.x) + npm: + ``` + curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash + nvm install v0.12.17 + ``` + +- Install PostgreSQL (version 9.6.1): -``` -curl -sL https://deb.nodesource.com/setup_0.12 | sudo -E bash - -sudo apt-get install -y nodejs -``` + ``` + curl -sL "https://downloads.lisk.io/scripts/setup_postgresql.Linux" | bash - + sudo -u postgres createuser --createdb $USER + createdb lisk_test + createdb lisk_main + sudo -u postgres psql -d lisk_test -c "alter user "$USER" with password 'password';" + sudo -u postgres psql -d lisk_main -c "alter user "$USER" with password 'password';" + ``` + +- Bower () -- Bower helps to install required JavaScript dependencies. -Install grunt-cli (globally): + `npm install -g bower` -``` -sudo npm install grunt-cli -g -``` +- Grunt.js () -- Grunt is used to compile the frontend code and serves other functions. -Install bower (globally): + `npm install -g grunt` + +- Forever () -- Forever manages the node process for Lisk (Optional) -``` -sudo npm install bower -g -``` + `npm install -g forever` -Install node modules: +## Installation Steps + +Clone the Lisk repository using Git and initialize the modules. ``` +git clone https://github.com/LiskHQ/lisk.git +cd lisk npm install ``` @@ -74,23 +82,35 @@ bower install grunt release ``` -## Launch +## Managing Lisk -To launch Lisk: +To test that Lisk is built and configured correctly, run the following command: -``` -node app.js -``` +`node app.js` + +In a browser navigate to: . If Lisk is running on a remote system, switch `localhost` for the external IP Address of the machine. + +Once the process is verified as running correctly, `CTRL+C` and start the process with `forever`. This will fork the process into the background and automatically recover the process if it fails. + +`forever start app.js` + +After the process is started its runtime status and log location can be found by issuing this statement: + +`forever list` + +To stop Lisk after it has been started with `forever`, issue the following command: + +`forever stop app.js` **NOTE:** The **port**, **address** and **config-path** can be overridden by providing the relevant command switch: ``` -node app.js -p [port] -a [address] -c [config-path] +forever start app.js -p [port] -a [address] -c [config-path] ``` ## Tests -Before running any tests, please ensure Lisk is configured to run on the same testnet as used by the test-suite. +Before running any tests, please ensure Lisk is configured to run on the same testnet that is used by the test-suite. Replace **config.json** and **genesisBlock.json** with the corresponding files under the **test** directory: @@ -98,13 +118,20 @@ Replace **config.json** and **genesisBlock.json** with the corresponding files u cp test/config.json test/genesisBlock.json . ``` +**NOTE:** If the node was started with a different genesis block previous, trauncate the database before running tests. + +``` +dropdb lisk_test +createdb lisk_test +``` + **NOTE:** The master passphrase for this genesis block is as follows: ``` wagon stock borrow episode laundry kitten salute link globe zero feed marble ``` -Launch lisk (runs on port 4000): +Launch Lisk (runs on port 4000): ``` node app.js @@ -134,7 +161,7 @@ npm test -- test/lib/transactions.js The MIT License (MIT) -Copyright (c) 2016 Lisk +Copyright (c) 2016-2017 Lisk Copyright (c) 2014-2015 Crypti Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/app.js b/app.js index 31fdb24f2cb..aed027b5759 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,7 @@ var checkIpInList = require('./helpers/checkIpInList.js'); var extend = require('extend'); var fs = require('fs'); var genesisblock = require('./genesisBlock.json'); +var git = require('./helpers/git.js'); var https = require('https'); var Logger = require('./logger.js'); var packageJson = require('./package.json'); @@ -17,6 +18,15 @@ var z_schema = require('./helpers/z_schema.js'); process.stdin.resume(); var versionBuild = fs.readFileSync(path.join(__dirname, 'build'), 'utf8'); +/** + * Hash of last git commit + * + * @private + * @property lastCommit + * @type {String} + * @default '' + */ +var lastCommit = ''; if (typeof gc !== 'undefined') { setInterval(function () { @@ -94,6 +104,13 @@ var config = { var logger = new Logger({ echo: appConfig.consoleLogLevel, errorLevel: appConfig.fileLogLevel, filename: appConfig.logFileName }); +// Trying to get last git commit +try { + lastCommit = git.getLastCommit(); +} catch (err) { + logger.debug('Cannot get last git commit', err.message); +} + var d = require('domain').create(); d.on('error', function (err) { @@ -140,6 +157,20 @@ d.run(function () { build: function (cb) { cb(null, versionBuild); }, + /** + * Returns hash of last git commit + * + * @property lastCommit + * @type {Function} + * @async + * @param {Function} cb Callback function + * @return {Function} cb Callback function from params + * @return {Object} cb.err Always return `null` here + * @return {String} cb.lastCommit Hash of last git commit + */ + lastCommit: function (cb) { + cb(null, lastCommit); + }, genesisblock: function (cb) { cb(null, { @@ -246,6 +277,11 @@ d.run(function () { return value; } + // Ignore conditional fields for transactions list + if (/^.+?:(blockId|recipientId|senderId)$/.test(name)) { + return value; + } + /*jslint eqeq: true*/ if (isNaN(value) || parseInt(value) != value || isNaN(parseInt(value, radix))) { return value; diff --git a/config.json b/config.json index b3d25c2c0c7..976784f6465 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,8 @@ { "port": 8000, "address": "0.0.0.0", - "version": "0.5.2", - "minVersion": "~0.5.0", + "version": "0.6.0", + "minVersion": ">=0.5.0", "fileLogLevel": "info", "logFileName": "logs/lisk.log", "consoleLogLevel": "info", diff --git a/helpers/database.js b/helpers/database.js index 6e9f7d9bcbb..ec7726738d5 100644 --- a/helpers/database.js +++ b/helpers/database.js @@ -102,7 +102,8 @@ function Migrator (pgp, db) { }; this.applyRuntimeQueryFile = function (waterCb) { - var sql = new pgp.QueryFile(path.join('sql', 'runtime.sql'), {minify: true}); + var dirname = path.basename(__dirname) === 'helpers' ? path.join(__dirname, '..') : __dirname; + var sql = new pgp.QueryFile(path.join(dirname, 'sql', 'runtime.sql'), {minify: true}); db.query(sql).then(function () { return waterCb(); @@ -123,7 +124,7 @@ module.exports.connect = function (config, logger, cb) { monitor.attach(pgOptions, config.logEvents); monitor.setTheme('matrix'); - monitor.log = function(msg, info){ + monitor.log = function (msg, info){ logger.log(info.event, info.text); info.display = false; }; diff --git a/helpers/exceptions.js b/helpers/exceptions.js index f598b30414b..9ed1844059d 100644 --- a/helpers/exceptions.js +++ b/helpers/exceptions.js @@ -22,9 +22,11 @@ module.exports = { signatures: [ '5676385569187187158', // 868797 '5384302058030309746', // 869890 - '9352922026980330230', // 925165 + '9352922026980330230' // 925165 + ], + multisignatures: [ + '14122550998639658526' // 1189962 ], - multisignatures: [], votes: [ '5524930565698900323', // 20407 '11613486949732674475', // 123300 diff --git a/helpers/git.js b/helpers/git.js new file mode 100644 index 00000000000..68beecae576 --- /dev/null +++ b/helpers/git.js @@ -0,0 +1,31 @@ +'use strict'; +/** +* Helper module for parsing git commit information +* +* @class git.js +*/ + +var childProcess = require('child_process'); + +/** + * Return hash of last git commit if available + * + * @method getLastCommit + * @public + * @return {String} Hash of last git commit + * @throws {Error} Throws error if cannot get last git commit + */ +function getLastCommit () { + var spawn = childProcess.spawnSync('git', ['rev-parse', 'HEAD']); + var err = spawn.stderr.toString().trim(); + + if (err) { + throw new Error(err); + } else { + return spawn.stdout.toString().trim(); + } +} + +module.exports = { + getLastCommit: getLastCommit +}; \ No newline at end of file diff --git a/logic/peerSweeper.js b/logic/peerSweeper.js index bd35c18fa78..96e7207964b 100644 --- a/logic/peerSweeper.js +++ b/logic/peerSweeper.js @@ -5,7 +5,7 @@ var pgp = require('pg-promise'); // Constructor function PeerSweeper (scope) { this.peers = []; - this.limit = 100; + this.limit = 500; this.scope = scope; var self = this; @@ -13,10 +13,10 @@ function PeerSweeper (scope) { setImmediate(function nextSweep () { if (self.peers.length) { self.sweep(self.peers.splice(0, self.limit), function () { - return setTimeout(nextSweep, 1000); + return setTimeout(nextSweep, 500); }); } else { - return setTimeout(nextSweep, 1000); + return setTimeout(nextSweep, 500); } }); } diff --git a/logic/transaction.js b/logic/transaction.js index 6ed371685ef..95a39e63a7c 100644 --- a/logic/transaction.js +++ b/logic/transaction.js @@ -826,6 +826,7 @@ Transaction.prototype.dbRead = function (raw) { requesterPublicKey: raw.t_requesterPublicKey, senderId: raw.t_senderId, recipientId: raw.t_recipientId, + recipientPublicKey: raw.m_recipientPublicKey || null, amount: parseInt(raw.t_amount), fee: parseInt(raw.t_fee), signature: raw.t_signature, diff --git a/modules/accounts.js b/modules/accounts.js index 2d6832a5d0b..ee55b78361d 100644 --- a/modules/accounts.js +++ b/modules/accounts.js @@ -473,7 +473,18 @@ shared.getAccount = function (req, cb) { return setImmediate(cb, err[0].message); } - self.getAccount({ address: req.body.address }, function (err, account) { + if (!req.body.address && !req.body.publicKey) { + return setImmediate(cb, 'Missing required property: address or publicKey'); + } + + // self.getAccount can accept publicKey as argument, but we also compare here + // if account publicKey match address (when both are supplied) + var address = req.body.publicKey ? self.generateAddressByPublicKey(req.body.publicKey) : req.body.address; + if (req.body.address && req.body.publicKey && address !== req.body.address) { + return setImmediate(cb, 'Account publicKey do not match address'); + } + + self.getAccount({ address: address }, function (err, account) { if (err) { return setImmediate(cb, err); } diff --git a/modules/blocks.js b/modules/blocks.js index 7c5a9881e19..2d2a2df5159 100644 --- a/modules/blocks.js +++ b/modules/blocks.js @@ -879,11 +879,7 @@ Blocks.prototype.loadBlocksFromPeer = function (peer, cb) { } else { var id = (block ? block.id : 'null'); - library.logger.debug(['Block', id].join(' '), err.toString()); - if (block) { library.logger.debug('Block', block); } - - library.logger.warn(['Block', id, 'is not valid, ban 10 min'].join(' '), peer.string); - modules.peers.state(peer.ip, peer.port, 0, 600); + library.logger.debug('Block processing failed', {id: id, err: err.toString(), module: 'blocks', block: block}); } return seriesCb(err); }, true); @@ -1331,16 +1327,17 @@ Blocks.prototype.onReceiveBlock = function (block) { // Fork: Consecutive height but different previous block id. modules.delegates.fork(block, 1); - // If newly received block is older than last one - we have wrong parent and should rewind. - if (block.timestamp < __private.lastBlock.timestamp) { + // We should keep the oldest one or if both have same age - keep one with lower id + if (block.timestamp > __private.lastBlock.timestamp || (block.timestamp === __private.lastBlock.timestamp && block.id > __private.lastBlock.id)) { + library.logger.info('Last block stands'); + return setImmediate(cb); + } else { + // In other cases - we have wrong parent and should rewind. library.logger.info('Last block and parent loses'); async.series([ self.deleteLastBlock, self.deleteLastBlock ], cb); - } else { - library.logger.info('Last block stands'); - return setImmediate(cb); } } else if (block.previousBlock === __private.lastBlock.previousBlock && block.height === __private.lastBlock.height && block.id !== __private.lastBlock.id) { // Fork: Same height and previous block id, but different block id. @@ -1351,10 +1348,12 @@ Blocks.prototype.onReceiveBlock = function (block) { library.logger.warn('Delegate forging on multiple nodes', block.generatorPublicKey); } - // Two competiting blocks on same height - we should always keep the oldest one. - if (block.timestamp < __private.lastBlock.timestamp) { + // Two competiting blocks on same height, we should keep the oldest one or if both have same age - keep one with lower id + if (block.timestamp > __private.lastBlock.timestamp || (block.timestamp === __private.lastBlock.timestamp && block.id > __private.lastBlock.id)) { + library.logger.info('Last block stands'); + return setImmediate(cb); + } else { library.logger.info('Last block loses'); - async.series([ function (seriesCb) { self.deleteLastBlock(seriesCb); @@ -1363,9 +1362,6 @@ Blocks.prototype.onReceiveBlock = function (block) { return __private.receiveBlock(block, seriesCb); } ], cb); - } else { - library.logger.info('Last block stands'); - return setImmediate(cb); } } else { return setImmediate(cb); @@ -1397,6 +1393,33 @@ Blocks.prototype.cleanup = function (cb) { } }; +Blocks.prototype.aggregateBlocksReward = function (filter, cb) { + var params = {}; + + params.generatorPublicKey = filter.generatorPublicKey; + params.delegates = constants.activeDelegates; + + if (filter.start !== undefined) { + params.start = filter.start - constants.epochTime.getTime () / 1000; + } + + if (filter.end !== undefined) { + params.end = filter.end - constants.epochTime.getTime () / 1000; + } + + library.db.query(sql.aggregateBlocksReward(params), params).then(function (rows) { + var data = rows[0]; + if (data.delegate === null) { + return setImmediate(cb, 'Account not found or is not a delegate'); + } + data = { fees: data.fees || '0', rewards: data.rewards || '0', count: data.count || '0' }; + return setImmediate(cb, null, data); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Blocks#aggregateBlocksReward error'); + }); +}; + // Shared shared.getBlock = function (req, cb) { if (!__private.loaded) { diff --git a/modules/dapps.js b/modules/dapps.js index a6df7a3171e..39edc300360 100644 --- a/modules/dapps.js +++ b/modules/dapps.js @@ -1069,7 +1069,14 @@ __private.createSandbox = function (dapp, params, cb) { return setImmediate(cb, err); } - var sandbox = new Sandbox(path.join(dappPath, 'index.js'), dapp.transactionId, params, __private.apiHandler, true); + var withDebug = false; + process.execArgv.forEach( function(item, index) { + if (item.indexOf('--debug') >= 0) { + withDebug = true; + } + }); + + var sandbox = new Sandbox(path.join(dappPath, 'index.js'), dapp.transactionId, params, __private.apiHandler, withDebug); __private.sandboxes[dapp.transactionId] = sandbox; sandbox.on('exit', function () { diff --git a/modules/delegates.js b/modules/delegates.js index 6bc8ef2df41..bbb1c7f14ba 100644 --- a/modules/delegates.js +++ b/modules/delegates.js @@ -86,17 +86,15 @@ __private.attachApi = function () { } router.post('/forging/enable', function (req, res) { + if (!checkIpInList(library.config.forging.access.whiteList, req.ip)) { + return res.json({success: false, error: 'Access denied'}); + } + library.schema.validate(req.body, schema.enableForging, function (err) { if (err) { return res.json({success: false, error: err[0].message}); } - var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - - if (!checkIpInList(library.config.forging.access.whiteList, ip)) { - return res.json({success: false, error: 'Access denied'}); - } - var keypair = library.ed.makeKeypair(crypto.createHash('sha256').update(req.body.secret, 'utf8').digest()); if (req.body.publicKey) { @@ -125,17 +123,15 @@ __private.attachApi = function () { }); router.post('/forging/disable', function (req, res) { + if (!checkIpInList(library.config.forging.access.whiteList, req.ip)) { + return res.json({success: false, error: 'Access denied'}); + } + library.schema.validate(req.body, schema.disableForging, function (err) { if (err) { return res.json({success: false, error: err[0].message}); } - var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - - if (!checkIpInList(library.config.forging.access.whiteList, ip)) { - return res.json({success: false, error: 'Access denied'}); - } - var keypair = library.ed.makeKeypair(crypto.createHash('sha256').update(req.body.secret, 'utf8').digest()); if (req.body.publicKey) { @@ -164,12 +160,21 @@ __private.attachApi = function () { }); router.get('/forging/status', function (req, res) { + if (!checkIpInList(library.config.forging.access.whiteList, req.ip)) { + return res.json({success: false, error: 'Access denied'}); + } + library.schema.validate(req.query, schema.forgingStatus, function (err) { if (err) { return res.json({success: false, error: err[0].message}); } - return res.json({success: true, enabled: !!__private.keypairs[req.query.publicKey]}); + if (req.query.publicKey) { + return res.json({success: true, enabled: !!__private.keypairs[req.query.publicKey]}); + } else { + var delegates_cnt = _.keys(__private.keypairs).length; + return res.json({success: true, enabled: (delegates_cnt > 0 ? true : false), delegates: _.keys(__private.keypairs) }); + } }); }); @@ -619,7 +624,8 @@ shared.getNextForgers = function (req, cb) { return setImmediate(cb, err); } - var currentSlot = slots.getSlotNumber(currentBlock.timestamp); + var currentBlockSlot = slots.getSlotNumber(currentBlock.timestamp); + var currentSlot = slots.getSlotNumber(); var nextForgers = []; for (var i = 1; i <= slots.delegates && i <= limit; i++) { @@ -627,7 +633,7 @@ shared.getNextForgers = function (req, cb) { nextForgers.push (activeDelegates[(currentSlot + i) % slots.delegates]); } } - return setImmediate(cb, null, {currentBlock: currentBlock.height, currentSlot: currentSlot, delegates: nextForgers}); + return setImmediate(cb, null, {currentBlock: currentBlock.height, currentBlockSlot: currentBlockSlot, currentSlot: currentSlot, delegates: nextForgers}); }); }; @@ -755,13 +761,23 @@ shared.getForgedByAccount = function (req, cb) { return setImmediate(cb, err[0].message); } - modules.accounts.getAccount({publicKey: req.body.generatorPublicKey}, ['fees', 'rewards'], function (err, account) { - if (err || !account) { - return setImmediate(cb, err || 'Account not found'); - } - var forged = bignum(account.fees).plus(bignum(account.rewards)).toString(); - return setImmediate(cb, null, {fees: account.fees, rewards: account.rewards, forged: forged}); - }); + if (req.body.start !== undefined || req.body.end !== undefined) { + modules.blocks.aggregateBlocksReward({generatorPublicKey: req.body.generatorPublicKey, start: req.body.start, end: req.body.end}, function (err, reward) { + if (err) { + return setImmediate(cb, err); + } + var forged = bignum(reward.fees).plus(bignum(reward.rewards)).toString(); + return setImmediate(cb, null, {fees: reward.fees, rewards: reward.rewards, forged: forged, count: reward.count}); + }); + } else { + modules.accounts.getAccount({publicKey: req.body.generatorPublicKey}, ['fees', 'rewards'], function (err, account) { + if (err || !account) { + return setImmediate(cb, err || 'Account not found'); + } + var forged = bignum(account.fees).plus(bignum(account.rewards)).toString(); + return setImmediate(cb, null, {fees: account.fees, rewards: account.rewards, forged: forged}); + }); + } }); }; diff --git a/modules/loader.js b/modules/loader.js index 0edfdf64f26..efe9d80a437 100644 --- a/modules/loader.js +++ b/modules/loader.js @@ -21,6 +21,8 @@ __private.genesisBlock = null; __private.total = 0; __private.blocksToSync = 0; __private.syncIntervalId = null; +__private.syncInterval = 10000; +__private.retries = 5; // Constructor function Loader (cb, scope) { @@ -66,11 +68,14 @@ __private.attachApi = function () { __private.syncTrigger = function (turnOn) { if (turnOn === false && __private.syncIntervalId) { + library.logger.trace('Clearing sync interval'); clearTimeout(__private.syncIntervalId); __private.syncIntervalId = null; } if (turnOn === true && !__private.syncIntervalId) { + library.logger.trace('Setting sync interval'); setImmediate(function nextSyncTrigger () { + library.logger.trace('Sync trigger'); library.network.io.sockets.emit('loader/sync', { blocks: __private.blocksToSync, height: modules.blocks.getLastBlock().height @@ -80,6 +85,29 @@ __private.syncTrigger = function (turnOn) { } }; +__private.syncTimer = function () { + library.logger.trace('Setting sync timer'); + setImmediate(function nextSync () { + var lastReceipt = modules.blocks.lastReceipt(); + library.logger.trace('Sync timer trigger', {loaded: __private.loaded, syncing: self.syncing(), last_receipt: lastReceipt}); + + if (__private.loaded && !self.syncing() && (!lastReceipt || lastReceipt.stale)) { + library.sequence.add(function (cb) { + async.retry(__private.retries, __private.sync, cb); + }, function (err) { + if (err) { + library.logger.error('Sync timer', err); + __private.initalize(); + } + + return setTimeout(nextSync, __private.syncInterval); + }); + } else { + return setTimeout(nextSync, __private.syncInterval); + } + }); +}; + __private.loadSignatures = function (cb) { async.waterfall([ function (waterCb) { @@ -166,8 +194,7 @@ __private.loadTransactions = function (cb) { try { transaction = library.logic.transaction.objectNormalize(transaction); } catch (e) { - library.logger.debug(['Transaction', id].join(' '), e.toString()); - if (transaction) { library.logger.debug('Transaction', transaction); } + library.logger.debug('Transaction normalization failed', {id: id, err: e.toString(), module: 'loader', tx: transaction}); library.logger.warn(['Transaction', id, 'is not valid, ban 10 min'].join(' '), peer.string); modules.peers.state(peer.ip, peer.port, 0, 600); @@ -654,32 +681,17 @@ Loader.prototype.sandboxApi = function (call, args, cb) { // Events Loader.prototype.onPeersReady = function () { - var retries = 5; + library.logger.trace('Peers ready', {module: 'loader'}); + // Enforce sync early + __private.syncTimer(); - setImmediate(function nextSeries () { + setImmediate(function load () { async.series({ - sync: function (seriesCb) { - var lastReceipt = modules.blocks.lastReceipt(); - - if (__private.loaded && !self.syncing() && (!lastReceipt || lastReceipt.stale)) { - library.sequence.add(function (cb) { - async.retry(retries, __private.sync, cb); - }, function (err) { - if (err) { - library.logger.log('Sync timer', err); - } - - return setImmediate(seriesCb); - }); - } else { - return setImmediate(seriesCb); - } - }, loadTransactions: function (seriesCb) { if (__private.loaded) { - async.retry(retries, __private.loadTransactions, function (err) { + async.retry(__private.retries, __private.loadTransactions, function (err) { if (err) { - library.logger.log('Unconfirmed transactions timer', err); + library.logger.log('Unconfirmed transactions loader', err); } return setImmediate(seriesCb); @@ -690,9 +702,9 @@ Loader.prototype.onPeersReady = function () { }, loadSignatures: function (seriesCb) { if (__private.loaded) { - async.retry(retries, __private.loadSignatures, function (err) { + async.retry(__private.retries, __private.loadSignatures, function (err) { if (err) { - library.logger.log('Signatures timer', err); + library.logger.log('Signatures loader', err); } return setImmediate(seriesCb); @@ -702,10 +714,13 @@ Loader.prototype.onPeersReady = function () { } } }, function (err) { + library.logger.trace('Transactions and signatures pulled'); + if (err) { __private.initalize(); } - return setTimeout(nextSeries, 10000); + + return __private.syncTimer(); }); }); }; diff --git a/modules/multisignatures.js b/modules/multisignatures.js index 0a658323fc6..c7636f1b98a 100644 --- a/modules/multisignatures.js +++ b/modules/multisignatures.js @@ -357,7 +357,7 @@ shared.sign = function (req, cb) { if (!scope.transaction.requesterPublicKey) { permissionDenied = ( - (scope.sender.multisignatures.indexOf(scope.keypair.publicKey.toString('hex')) === -1) + (!Array.isArray(scope.sender.multisignatures) || scope.sender.multisignatures.indexOf(scope.keypair.publicKey.toString('hex')) === -1) ); } else { permissionDenied = ( diff --git a/modules/peers.js b/modules/peers.js index 7027200e81c..9ef2f1ffeda 100644 --- a/modules/peers.js +++ b/modules/peers.js @@ -46,7 +46,8 @@ __private.attachApi = function () { router.map(shared, { 'get /': 'getPeers', 'get /version': 'version', - 'get /get': 'getPeer' + 'get /get': 'getPeer', + 'get /count': 'count' }); router.use(function (req, res) { @@ -151,6 +152,51 @@ __private.count = function (cb) { }); }; +__private.countByFilter = function (filter, cb) { + var where = []; + var params = {}; + + if (filter.port) { + where.push('"port" = ${port}'); + params.port = filter.port; + } + + if (filter.state >= 0) { + where.push('"state" = ${state}'); + params.state = filter.state; + } + + if (filter.os) { + where.push('"os" = ${os}'); + params.os = filter.os; + } + + if (filter.version) { + where.push('"version" = ${version}'); + params.version = filter.version; + } + + if (filter.broadhash) { + where.push('"broadhash" = ${broadhash}'); + params.broadhash = filter.broadhash; + } + + if (filter.height) { + where.push('"height" = ${height}'); + params.height = filter.height; + } + + library.db.query(sql.countByFilter({ + where: where + }), params).then(function (rows) { + var res = rows.length && rows[0].count; + return setImmediate(cb, null, res); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Peers#count error'); + }); +}; + __private.banManager = function (cb) { library.db.query(sql.banManager, { now: Date.now() }).then(function (res) { return setImmediate(cb, null, res); @@ -244,13 +290,15 @@ Peers.prototype.accept = function (peer) { Peers.prototype.acceptable = function (peers) { return _.chain(peers).filter(function (peer) { - // Removing peers with private ip address - return !ip.isPrivate(peer.ip); + // Removing peers with private or host's ip address + return !(ip.isPrivate(peer.ip) || ip.address('public', 'ipv4', true).some(function (address) { + return [address, library.config.port].join(':') === [peer.ip, peer.port].join(':'); + })); }).uniqWith(function (a, b) { // Removing non-unique peers return (a.ip + a.port) === (b.ip + b.port); // Slicing peers up to maxPeers - }).slice(0, constants.maxPeers).value(); + }).value(); }; Peers.prototype.list = function (options, cb) { @@ -360,6 +408,20 @@ Peers.prototype.update = function (peer) { return __private.sweeper.push('upsert', self.accept(peer).object()); }; +Peers.prototype.pingPeer = function (peer, cb) { + library.logger.trace('Pinging peer: ' + peer.ip + ':' + peer.port); + modules.transport.getFromPeer(peer, { + api: '/height', + method: 'GET' + }, function (err, res) { + if (err) { + return setImmediate(cb, 'Failed to ping peer: ' + peer.ip + ':' + peer.port); + } else { + return setImmediate(cb); + } + }); +}; + Peers.prototype.sandboxApi = function (call, args, cb) { sandboxHelper.callMethod(shared, call, args, cb); }; @@ -378,7 +440,7 @@ Peers.prototype.onBlockchainReady = function () { port: peer.port, version: modules.system.getVersion(), state: 2, - broadhash: modules.system.getBroadhash(), + broadhash: null, height: 1 }); @@ -409,9 +471,11 @@ Peers.prototype.onBlockchainReady = function () { }; Peers.prototype.onPeersReady = function () { + library.logger.trace('onPeersReady'); setImmediate(function nextSeries () { async.series({ updatePeersList: function (seriesCb) { + library.logger.trace('onPeersReady->updatePeersList'); __private.updatePeersList(function (err) { if (err) { library.logger.error('Peers timer', err); @@ -420,20 +484,69 @@ Peers.prototype.onPeersReady = function () { }); }, nextBanManager: function (seriesCb) { + library.logger.trace('onPeersReady->nextBanManager'); __private.banManager(function (err) { if (err) { library.logger.error('Ban manager timer', err); } return setImmediate(seriesCb); }); + }, + updatePeers: function (seriesCb) { + library.logger.trace('onPeersReady->updatePeers'); + self.list({limit: 500}, function (err, peers, consensus) { + if (err) { + library.logger.error('Peers listing failed', err); + return setImmediate(seriesCb, err); + } + + library.logger.trace('onPeersReady->updatePeers', {count: (peers ? peers.length : null)}); + var updated = 0; + async.each(peers, function (peer, eachCb) { + // Pinging only unbanned peers + if (peer && peer.state > 0) { + self.pingPeer(peer, function (err, res) { + if (!err) { + ++updated; + } + return setImmediate(eachCb); + }); + } else { + return setImmediate(eachCb); + } + }, function () { + library.logger.trace('onPeersReady->updatePeers', {updated: updated, total: peers.length}); + return setImmediate(seriesCb); + }); + }); } }, function (err) { - return setTimeout(nextSeries, 60000); + return setTimeout(nextSeries, 5000); }); }); }; // Shared +shared.count = function (req, cb) { + async.series({ + connected: function (cb) { + __private.countByFilter({state: 2}, cb); + }, + disconnected: function (cb) { + __private.countByFilter({state: 1}, cb); + }, + banned: function (cb) { + __private.countByFilter({state: 0}, cb); + } + }, function (err, res) { + if (err) { + return setImmediate(cb, 'Failed to get peer count'); + } + + return setImmediate(cb, null, res); + }); +}; + shared.getPeers = function (req, cb) { library.schema.validate(req.body, schema.getPeers, function (err) { if (err) { @@ -477,8 +590,27 @@ shared.getPeer = function (req, cb) { }); }; +/** + * Returns information about version + * + * @public + * @async + * @method version + * @param {Object} req HTTP request object + * @param {Function} cb Callback function + * @return {Function} cb Callback function from params (through setImmediate) + * @return {Object} cb.err Always return `null` here + * @return {Object} cb.obj Anonymous object with version info + * @return {String} cb.obj.build Build information (if available, otherwise '') + * @return {String} cb.obj.commit Hash of last git commit (if available, otherwise '') + * @return {String} cb.obj.version Lisk version from config file + */ shared.version = function (req, cb) { - return setImmediate(cb, null, {version: library.config.version, build: library.build}); + return setImmediate(cb, null, { + build: library.build, + commit: library.lastCommit, + version: library.config.version + }); }; // Export diff --git a/modules/rounds.js b/modules/rounds.js index 98380bf7bcc..0eab898c720 100644 --- a/modules/rounds.js +++ b/modules/rounds.js @@ -106,7 +106,9 @@ Rounds.prototype.backwardTick = function (block, previousBlock, done) { delete __private.unFeesByRound[round]; delete __private.unRewardsByRound[round]; delete __private.unDelegatesByRound[round]; - }).then(promised.markBlockId); + }).then(function () { + return promised.markBlockId(); + }); } else { return promised.markBlockId(); } diff --git a/modules/sql.js b/modules/sql.js index 859d1adb5df..7c853c4cc69 100644 --- a/modules/sql.js +++ b/modules/sql.js @@ -10,9 +10,10 @@ var sandboxHelper = require('../helpers/sandbox.js'); var modules, library, self, __private = {}, shared = {}; __private.loaded = false; -__private.DOUBLE_DOUBLE_QUOTES = /''/g; __private.SINGLE_QUOTES = /'/g; __private.SINGLE_QUOTES_DOUBLED = '\'\''; +__private.DOUBLE_QUOTES = /"/g; +__private.DOUBLE_QUOTES_DOUBLED = '""'; // Constructor function Sql (cb, scope) { @@ -47,6 +48,10 @@ __private.escape = function (what) { throw 'Unsupported data ' + typeof what; }; +__private.escape2 = function (str) { + return '"' + str.replace(__private.DOUBLE_QUOTES, __private.DOUBLE_QUOTES_DOUBLED) + '"'; +}; + __private.pass = function (obj, dappid) { for (var property in obj) { if (typeof obj[property] === 'object') { @@ -100,6 +105,7 @@ __private.query = function (action, config, cb) { try { sql = jsonSql.build(extend({}, config, defaultConfig)); + library.logger.trace('sql.query:', sql); } catch (e) { return done(e); } @@ -120,7 +126,7 @@ __private.query = function (action, config, cb) { return batchPack.length === 0; }, function (cb) { var fields = Object.keys(config.fields).map(function (field) { - return __private.escape(config.fields[field]); + return __private.escape2(config.fields[field]); // Add double quotes to field identifiers }); sql = 'INSERT INTO ' + 'dapp_' + config.dappid + '_' + config.table + ' (' + fields.join(',') + ') '; var rows = []; diff --git a/modules/system.js b/modules/system.js index b94247ac51b..f63c35f901a 100644 --- a/modules/system.js +++ b/modules/system.js @@ -78,11 +78,14 @@ System.prototype.versionCompatible = function (version) { version = version.replace(rcRegExp, ''); } - if (this.minVersionChar && versionChar) { + // if no range specifier is used for minVersion, check the complete version string (inclusive versionChar) + var rangeRegExp = /[\^~\*]/; + if (this.minVersionChar && versionChar && !rangeRegExp.test(this.minVersion)) { return (version + versionChar) === (this.minVersion + this.minVersionChar); - } else { - return semver.satisfies(version, this.minVersion); } + + // ignore versionChar, check only version + return semver.satisfies(version, this.minVersion); }; System.prototype.getBroadhash = function (cb) { diff --git a/modules/transactions.js b/modules/transactions.js index 35df8203f0a..21a6a261797 100644 --- a/modules/transactions.js +++ b/modules/transactions.js @@ -1,11 +1,11 @@ 'use strict'; +var _ = require('lodash'); var async = require('async'); var ByteBuffer = require('bytebuffer'); var constants = require('../helpers/constants.js'); var crypto = require('crypto'); var extend = require('extend'); -var genesisblock = null; var OrderBy = require('../helpers/orderBy.js'); var Router = require('../helpers/router.js'); var sandboxHelper = require('../helpers/sandbox.js'); @@ -17,7 +17,12 @@ var transactionTypes = require('../helpers/transactionTypes.js'); var Transfer = require('../logic/transfer.js'); // Private fields -var modules, library, self, __private = {}, shared = {}; +var __private = {}; +var shared = {}; +var genesisblock = null; +var modules; +var library; +var self; __private.assetTypes = {}; @@ -49,6 +54,7 @@ __private.attachApi = function () { router.map(shared, { 'get /': 'getTransactions', 'get /get': 'getTransaction', + 'get /count': 'getTransactionsCount', 'get /queued/get': 'getQueuedTransaction', 'get /queued': 'getQueuedTransactions', 'get /multisignatures/get': 'getMultisignatureTransaction', @@ -71,40 +77,86 @@ __private.attachApi = function () { }; __private.list = function (filter, cb) { - var sortFields = sql.sortFields; - var params = {}, where = [], owner = ''; + var params = {}; + var where = []; + var allowedFieldsMap = { + blockId: '"t_blockId" = ${blockId}', + senderPublicKey: '"t_senderPublicKey" = DECODE (${senderPublicKey}, \'hex\')', + recipientPublicKey: '"m_recipientPublicKey" = DECODE (${recipientPublicKey}, \'hex\')', + senderId: '"t_senderId" = ${senderId}', + recipientId: '"t_recipientId" = ${recipientId}', + fromHeight: '"b_height" >= ${fromHeight}', + toHeight: '"b_height" <= ${toHeight}', + fromTimestamp: '"t_timestamp" >= ${fromTimestamp}', + toTimestamp: '"t_timestamp" <= ${toTimestamp}', + senderIds: '"t_senderId" IN (${senderIds:csv})', + recipientIds: '"t_recipientId" IN (${recipientIds:csv})', + senderPublicKeys: 'ENCODE ("t_senderPublicKey", \'hex\') IN (${senderPublicKeys:csv})', + recipientPublicKeys: 'ENCODE ("m_recipientPublicKey", \'hex\') IN (${recipientPublicKeys:csv})', + minAmount: '"t_amount" >= ${minAmount}', + maxAmount: '"t_amount" <= ${minAmount}', + type: '"t_type" = ${type}', + minConfirmations: 'confirmations >= ${minConfirmations}', + limit: null, + offset: null, + orderBy: null, + // FIXME: Backward compatibility, should be removed after transitional period + ownerAddress: null, + ownerPublicKey: null + }; + var owner = ''; + var isFirstWhere = true; + + var processParams = function (value, key) { + var field = String(key).split(':'); + if (field.length === 1) { + // Only field identifier, so using default 'OR' condition + field.unshift('OR'); + } else if (field.length === 2) { + // Condition supplied, checking if correct one + if (_.includes(['or', 'and'], field[0].toLowerCase())) { + field[0] = field[0].toUpperCase(); + } else { + throw new Error('Incorrect condition [' + field[0] + '] for field: ' + field[1]); + } + } else { + // Invalid parameter 'x:y:z' + throw new Error('Invalid parameter supplied: ' + key); + } - if (filter.blockId) { - where.push('"t_blockId" = ${blockId}'); - params.blockId = filter.blockId; - } + if (!_.includes(_.keys(allowedFieldsMap), field[1])) { + throw new Error('Parameter is not supported: ' + field[1]); + } - if (filter.senderPublicKey) { - where.push('"t_senderPublicKey"::bytea = ${senderPublicKey}'); - params.senderPublicKey = filter.senderPublicKey; - } + // Checking for empty parameters, 0 is allowed for few + if (!value && !(value === 0 && _.includes(['fromTimestamp', 'minAmount', 'minConfirmations', 'type', 'offset'], field[1]))) { + throw new Error('Value for parameter [' + field[1] + '] cannot be empty'); + } - if (filter.senderId) { - where.push('"t_senderId" = ${senderId}'); - params.senderId = filter.senderId; - } + if (allowedFieldsMap[field[1]]) { + if (_.includes(['fromTimestamp', 'toTimestamp'], field[1])) { + value = value - constants.epochTime.getTime() / 1000; + } + where.push((!isFirstWhere ? (field[0] + ' ') : '') + allowedFieldsMap[field[1]]); + params[field[1]] = value; + isFirstWhere = false; + } + }; - if (filter.recipientId) { - where.push('"t_recipientId" = ${recipientId}'); - params.recipientId = filter.recipientId; + // Generate list of fields with conditions + try { + _.each(filter, processParams); + } catch (err) { + return setImmediate(cb, err.message); } + // FIXME: Backward compatibility, should be removed after transitional period if (filter.ownerAddress && filter.ownerPublicKey) { - owner = '("t_senderPublicKey"::bytea = ${ownerPublicKey} OR "t_recipientId" = ${ownerAddress})'; + owner = '("t_senderPublicKey" = DECODE (${ownerPublicKey}, \'hex\') OR "t_recipientId" = ${ownerAddress})'; params.ownerPublicKey = filter.ownerPublicKey; params.ownerAddress = filter.ownerAddress; } - if (filter.type >= 0) { - where.push('"t_type" = ${type}'); - params.type = filter.type; - } - if (!filter.limit) { params.limit = 100; } else { @@ -117,16 +169,18 @@ __private.list = function (filter, cb) { params.offset = Math.abs(filter.offset); } - if (params.limit > 100) { - return setImmediate(cb, 'Invalid limit. Maximum is 100'); + if (params.limit > 1000) { + return setImmediate(cb, 'Invalid limit, maximum is 1000'); } var orderBy = OrderBy( filter.orderBy, { sortFields: sql.sortFields, fieldPrefix: function (sortField) { - if (['height', 'blockId', 'confirmations'].indexOf(sortField) > -1) { + if (['height'].indexOf(sortField) > -1) { return 'b_' + sortField; + } else if (['confirmations'].indexOf(sortField) > -1) { + return sortField; } else { return 't_' + sortField; } @@ -380,18 +434,41 @@ Transactions.prototype.onPeersReady = function () { // Shared shared.getTransactions = function (req, cb) { - library.schema.validate(req.body, schema.getTransactions, function (err) { - if (err) { - return setImmediate(cb, err[0].message); - } - - __private.list(req.body, function (err, data) { - if (err) { - return setImmediate(cb, 'Failed to get transactions: ' + err); - } + async.waterfall([ + function (waterCb) { + var params = {}; + var pattern = /(and|or){1}:/i; + + // Filter out 'and:'/'or:' from params to perform schema validation + _.each(req.body, function (value, key) { + var param = String(key).replace(pattern, ''); + // Dealing with array-like parameters (csv comma separated) + if (_.includes(['senderIds', 'recipientIds', 'senderPublicKeys', 'recipientPublicKeys'], param)) { + value = String(value).split(','); + req.body[key] = value; + } + params[param] = value; + }); - return setImmediate(cb, null, {transactions: data.transactions, count: data.count}); - }); + library.schema.validate(params, schema.getTransactions, function (err) { + if (err) { + return setImmediate(waterCb, err[0].message); + } else { + return setImmediate(waterCb, null); + } + }); + }, + function (waterCb) { + __private.list(req.body, function (err, data) { + if (err) { + return setImmediate(waterCb, 'Failed to get transactions: ' + err); + } else { + return setImmediate(waterCb, null, {transactions: data.transactions, count: data.count}); + } + }); + } + ], function (err, res) { + return setImmediate(cb, err, res); }); }; @@ -417,6 +494,20 @@ shared.getTransaction = function (req, cb) { }); }; +shared.getTransactionsCount = function (req, cb) { + library.db.query(sql.count).then(function (transactionsCount) { + return setImmediate(cb, null, { + confirmed: transactionsCount[0].count, + multisignature: __private.transactionPool.multisignature.transactions.length, + unconfirmed: __private.transactionPool.unconfirmed.transactions.length, + queued: __private.transactionPool.queued.transactions.length + }); + + }, function (err) { + return setImmediate(cb, 'Unable to count transactions'); + }); +}; + shared.getQueuedTransaction = function (req, cb) { return __private.getPooledTransaction('getQueuedTransaction', req, cb); }; diff --git a/modules/transport.js b/modules/transport.js index ccc645d7a09..bd28c17c3aa 100644 --- a/modules/transport.js +++ b/modules/transport.js @@ -47,7 +47,7 @@ __private.attachApi = function () { router.use(function (req, res, next) { req.peer = modules.peers.accept( { - ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress, + ip: req.ip, port: req.headers.port } ); @@ -96,8 +96,14 @@ __private.attachApi = function () { router.get('/blocks/common', function (req, res, next) { req.sanitize(req.query, schema.commonBlock, function (err, report, query) { - if (err) { return next(err); } - if (!report.isValid) { return res.json({success: false, error: report.issues}); } + if (err) { + library.logger.debug('Common block request validation failed', {err: err.toString(), req: req.query}); + return next(err); + } + if (!report.isValid) { + library.logger.debug('Common block request validation failed', {err: report, req: req.query}); + return res.json({success: false, error: report.issues}); + } var escapedIds = query.ids // Remove quotes @@ -110,7 +116,7 @@ __private.attachApi = function () { }); if (!escapedIds.length) { - library.logger.warn('Invalid common block request, ban 10 min', req.peer.string); + library.logger.debug('Common block request validation failed', {err: 'ESCAPE', req: req.query}); // Ban peer for 10 minutes __private.banPeer({peer: req.peer, code: 'ECOMMON', req: req, clock: 600}); @@ -155,13 +161,10 @@ __private.attachApi = function () { try { block = library.logic.block.objectNormalize(block); } catch (e) { - library.logger.debug(['Block', id].join(' '), e.toString()); - if (block) { library.logger.debug('Block', block); } + library.logger.debug('Block normalization failed', {err: e.toString(), module: 'transport', block: block }); - if (req.peer) { - // Ban peer for 10 minutes - __private.banPeer({peer: req.peer, code: 'EBLOCK', req: req, clock: 600}); - } + // Ban peer for 10 minutes + __private.banPeer({peer: req.peer, code: 'EBLOCK', req: req, clock: 600}); return res.status(200).json({success: false, error: e.toString()}); } @@ -341,6 +344,10 @@ __private.hashsum = function (obj) { }; __private.banPeer = function (options) { + if (!options.peer || !options.peer.ip || !options.peer.port) { + library.logger.trace('Peer ban skipped', {options: options}); + return false; + } library.logger.warn([options.code, ['Ban', options.peer.string, (options.clock / 60), 'minutes'].join(' '), options.req.method, options.req.url].join(' ')); modules.peers.state(options.peer.ip, options.peer.port, 0, options.clock); }; @@ -436,13 +443,10 @@ __private.receiveTransaction = function (transaction, req, cb) { try { transaction = library.logic.transaction.objectNormalize(transaction); } catch (e) { - library.logger.debug(['Transaction', id].join(' '), e.toString()); - if (transaction) { library.logger.debug('Transaction', transaction); } + library.logger.debug('Transaction normalization failed', {id: id, err: e.toString(), module: 'transport', tx: transaction}); - if (req.peer) { - // Ban peer for 10 minutes - __private.banPeer({peer: req.peer, code: 'ETRANSACTION', req: req, clock: 600}); - } + // Ban peer for 10 minutes + __private.banPeer({peer: req.peer, code: 'ETRANSACTION', req: req, clock: 600}); return setImmediate(cb, 'Invalid transaction body'); } @@ -525,55 +529,52 @@ Transport.prototype.getFromPeer = function (peer, options, cb) { req.body = options.data; } - var request = popsicle.request(req); + popsicle.request(req) + .use(popsicle.plugins.parse(['json'], false)) + .then(function (res) { + if (res.status !== 200) { + // Remove peer + __private.removePeer({peer: peer, code: 'ERESPONSE ' + res.status, req: req}); - request.use(popsicle.plugins.parse(['json'], false)); + return setImmediate(cb, ['Received bad response code', res.status, req.method, req.url].join(' ')); + } else { + var headers = peer.extend(res.headers); - request.then(function (res) { - if (res.status !== 200) { - // Remove peer - __private.removePeer({peer: peer, code: 'ERESPONSE ' + res.status, req: req}); + var report = library.schema.validate(headers, schema.headers); + if (!report) { + // Remove peer + __private.removePeer({peer: peer, code: 'EHEADERS', req: req}); - return setImmediate(cb, ['Received bad response code', res.status, req.method, req.url].join(' ')); - } else { - var headers = peer.extend(res.headers); + return setImmediate(cb, ['Invalid response headers', JSON.stringify(headers), req.method, req.url].join(' ')); + } - var report = library.schema.validate(headers, schema.headers); - if (!report) { - // Remove peer - __private.removePeer({peer: peer, code: 'EHEADERS', req: req}); + if (!modules.system.networkCompatible(headers.nethash)) { + // Remove peer + __private.removePeer({peer: peer, code: 'ENETHASH', req: req}); - return setImmediate(cb, ['Invalid response headers', JSON.stringify(headers), req.method, req.url].join(' ')); - } + return setImmediate(cb, ['Peer is not on the same network', headers.nethash, req.method, req.url].join(' ')); + } - if (!modules.system.networkCompatible(headers.nethash)) { - // Remove peer - __private.removePeer({peer: peer, code: 'ENETHASH', req: req}); + if (!modules.system.versionCompatible(headers.version)) { + // Remove peer + __private.removePeer({peer: peer, code: 'EVERSION:' + headers.version, req: req}); - return setImmediate(cb, ['Peer is not on the same network', headers.nethash, req.method, req.url].join(' ')); - } + return setImmediate(cb, ['Peer is using incompatible version', headers.version, req.method, req.url].join(' ')); + } - if (!modules.system.versionCompatible(headers.version)) { - // Remove peer - __private.removePeer({peer: peer, code: 'EVERSION:' + headers.version, req: req}); + modules.peers.update(peer); - return setImmediate(cb, ['Peer is using incompatible version', headers.version, req.method, req.url].join(' ')); + return setImmediate(cb, null, {body: res.body, peer: peer}); } - - modules.peers.update(peer); - - return setImmediate(cb, null, {body: res.body, peer: peer}); - } - }); - - request.catch(function (err) { + }) + .catch(function (err) { if (peer) { if (err.code === 'EUNAVAILABLE') { // Remove peer __private.removePeer({peer: peer, code: err.code, req: req}); } else { - // Ban peer for 10 minutes - __private.banPeer({peer: peer, code: err.code, req: req, clock: 600}); + // Ban peer for 1 minute + __private.banPeer({peer: peer, code: err.code, req: req, clock: 60}); } } diff --git a/package.json b/package.json index 25b1be12306..0c7270f87e5 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "lisk", - "version": "0.5.2", + "version": "0.6.0", "private": true, "scripts": { "start": "node app.js", - "test": "./node_modules/.bin/mocha", - "cov": "./node_modules/.bin/mocha --require blanket -R html-cov test/helpers test/logic > tmp/coverage.html" + "test": "./node_modules/.bin/grunt test --verbose", + "travis": "./node_modules/.bin/grunt travis --verbose" }, "author": "Boris Povod , Pavel Nekrasov , Oliver Beddows ", "dependencies": { @@ -24,7 +24,7 @@ "express-query-int": "=1.0.1", "express-rate-limit": "=2.5.0", "extend": "=3.0.0", - "ip": "=1.1.3", + "ip": "https://github.com/LiskHQ/node-ip/tarball/ec401f16915024450bf9b843eeb4c5366ef5235c", "json-schema": "=0.2.3", "json-sql": "LiskHQ/json-sql#27e1ed1", "lisk-sandbox": "LiskHQ/lisk-sandbox#162da38", @@ -48,7 +48,6 @@ }, "devDependencies": { "bitcore-mnemonic": "=1.1.1", - "blanket": "=1.2.3", "browserify-bignum": "=1.3.0-2", "buffer": "=4.9.1", "chai": "=3.5.0", @@ -63,23 +62,12 @@ "grunt-exec": "=1.0.1", "grunt-jsdox": "=0.1.7", "grunt-obfuscator": "=0.1.0", + "istanbul": "=0.4.5", "jsdox": "=0.4.10", "jshint": "=2.9.3", "mocha": "=3.1.0", "moment": "=2.15.1", + "sinon": "=1.17.7", "supertest": "=2.0.0" - }, - "config": { - "blanket": { - "pattern": [ - "helpers", - "logic" - ], - "data-cover-never": [ - "node_modules", - "test", - "tmp" - ] - } } } diff --git a/public b/public index 3c340a808f4..11d52333e27 160000 --- a/public +++ b/public @@ -1 +1 @@ -Subproject commit 3c340a808f4900c70a8d3368f3d2520dddefb7de +Subproject commit 11d52333e276b7b1f2f2fae37d18622f762b63b1 diff --git a/schema/accounts.js b/schema/accounts.js index 44f6db70c71..ec5ab44adb3 100644 --- a/schema/accounts.js +++ b/schema/accounts.js @@ -93,9 +93,12 @@ module.exports = { format: 'address', minLength: 1, maxLength: 22 + }, + publicKey: { + type: 'string', + format: 'publicKey' } - }, - required: ['address'] + } }, top: { id: 'accounts.top', diff --git a/schema/delegates.js b/schema/delegates.js index 1b6b681f0c9..038bacd9282 100644 --- a/schema/delegates.js +++ b/schema/delegates.js @@ -43,8 +43,7 @@ module.exports = { type: 'string', format: 'publicKey' } - }, - required: ['publicKey'] + } }, getDelegate: { id: 'delegates.getDelegate', @@ -114,6 +113,12 @@ module.exports = { generatorPublicKey: { type: 'string', format: 'publicKey' + }, + start: { + type: 'integer' + }, + end: { + type: 'integer' } }, required: ['generatorPublicKey'] diff --git a/schema/transactions.js b/schema/transactions.js index 514d7501fa1..2e9d59925c5 100644 --- a/schema/transactions.js +++ b/schema/transactions.js @@ -54,13 +54,77 @@ module.exports = { minimum: 0, maximum: constants.fixedPoint }, + senderPublicKeys: { + type: 'array', + minLength: 1, + 'items': { + type: 'string', + format: 'publicKey' + } + }, + recipientPublicKeys: { + type: 'array', + minLength: 1, + 'items': { + type: 'string', + format: 'publicKey' + } + }, + senderIds: { + type: 'array', + minLength: 1, + 'items': { + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 + } + }, + recipientIds: { + type: 'array', + minLength: 1, + 'items': { + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 + } + }, + fromHeight: { + type: 'integer', + minimum: 1 + }, + toHeight: { + type: 'integer', + minimum: 1 + }, + fromTimestamp: { + type: 'integer', + minimum: 0 + }, + toTimestamp: { + type: 'integer', + minimum: 1 + }, + minAmount: { + type: 'integer', + minimum: 0 + }, + maxAmount: { + type: 'integer', + minimum: 1 + }, + minConfirmations: { + type: 'integer', + minimum: 0 + }, orderBy: { type: 'string' }, limit: { type: 'integer', minimum: 1, - maximum: 100 + maximum: 1000 }, offset: { type: 'integer', diff --git a/sql/blocks.js b/sql/blocks.js index f755195b7d8..1b0199aaa92 100644 --- a/sql/blocks.js +++ b/sql/blocks.js @@ -24,6 +24,56 @@ var BlocksSql = { ].filter(Boolean).join(' '); }, + aggregateBlocksReward: function (params) { + return [ + 'WITH', + 'delegate AS (SELECT', + '1 FROM mem_accounts m WHERE m."isDelegate" = 1 AND m."publicKey" = DECODE (${generatorPublicKey}, \'hex\') LIMIT 1),', + 'borders AS (SELECT', + '(SELECT (CAST(b.height / ${delegates} AS INTEGER) + (CASE WHEN b.height % ${delegates} > 0 THEN 1 ELSE 0 END)) FROM blocks b ORDER BY b.height DESC LIMIT 1) AS current,', + '(SELECT (CAST(b.height / ${delegates} AS INTEGER) + (CASE WHEN b.height % ${delegates} > 0 THEN 1 ELSE 0 END)) FROM blocks b', + (params.start !== undefined ? ' WHERE b.timestamp >= ${start}' : ''), + 'ORDER BY b.height ASC LIMIT 1) AS min,', + '(SELECT (CAST(b.height / ${delegates} AS INTEGER) + (CASE WHEN b.height % ${delegates} > 0 THEN 1 ELSE 0 END)) FROM blocks b', + (params.end !== undefined ? ' WHERE b.timestamp <= ${end}' : ''), + 'ORDER BY b.height DESC LIMIT 1) AS max', + '),', + 'r AS (SELECT DISTINCT ', + '(CAST(b.height / ${delegates} AS INTEGER) + (CASE WHEN b.height % ${delegates} > 0 THEN 1 ELSE 0 END)) AS round', + 'FROM blocks b WHERE b."generatorPublicKey" = DECODE (${generatorPublicKey}, \'hex\')),', + 're AS (SELECT r.round AS round, ((r.round-1)*${delegates})+1 AS min, r.round*${delegates} AS max', + 'FROM r WHERE r.round >= (SELECT min FROM borders) AND round <= (SELECT max FROM borders)),', + 'sum_min AS (SELECT', + 'SUM(CASE WHEN b."generatorPublicKey" = DECODE (${generatorPublicKey}, \'hex\') THEN b.reward ELSE 0 END) AS rewards,', + 'SUM(CASE WHEN b."generatorPublicKey" = DECODE (${generatorPublicKey}, \'hex\') THEN 1 ELSE 0 END) AS blocks', + 'FROM blocks b WHERE b.height BETWEEN (SELECT min FROM re ORDER BY round ASC LIMIT 1) AND (SELECT max FROM re ORDER BY round ASC LIMIT 1)', + (params.start !== undefined ? 'AND b.timestamp >= ${start}' : ''), + '),', + 'sum_max AS (SELECT', + 'SUM(CASE WHEN b."generatorPublicKey" = DECODE (${generatorPublicKey}, \'hex\') THEN b.reward ELSE 0 END) AS rewards,', + 'SUM(CASE WHEN b."generatorPublicKey" = DECODE (${generatorPublicKey}, \'hex\') THEN 1 ELSE 0 END) AS blocks', + 'FROM blocks b WHERE b.height BETWEEN (SELECT min FROM re ORDER BY round DESC LIMIT 1) AND (SELECT max FROM re ORDER BY round DESC LIMIT 1)', + (params.end !== undefined ? 'AND b.timestamp <= ${end}' : ''), + '),', + 'rs AS (SELECT re.*, SUM(b."totalFee") AS fees,', + 'SUM(CASE WHEN b."generatorPublicKey" = DECODE (${generatorPublicKey}, \'hex\') THEN b.reward ELSE 0 END) AS rewards,', + 'SUM(CASE WHEN b."generatorPublicKey" = DECODE (${generatorPublicKey}, \'hex\') THEN 1 ELSE 0 END) AS blocks', + 'FROM re, blocks b WHERE b.height BETWEEN re.min AND re.max GROUP BY re.round, re.min, re.max),', + 'rsc AS (SELECT', + '(CASE WHEN round = borders.current THEN 0 ELSE fees END), round,', + '(CASE WHEN round = borders.min THEN (SELECT blocks FROM sum_min) ELSE (CASE WHEN round = borders.max THEN (SELECT blocks FROM sum_max) ELSE blocks END) END) AS blocks,', + '(CASE WHEN round = borders.min THEN (SELECT rewards FROM sum_min) ELSE (CASE WHEN round = borders.max THEN (SELECT rewards FROM sum_max) ELSE rewards END) END) AS rewards,', + '(SELECT 1 FROM blocks b WHERE b.height = rs.max AND b."generatorPublicKey" = DECODE (${generatorPublicKey}, \'hex\') LIMIT 1) AS last', + 'FROM rs, borders)', + 'SELECT', + '(SELECT * FROM delegate) AS delegate,', + 'SUM(rsc.blocks) AS count,', + 'SUM(floor(rsc.fees/${delegates})*rsc.blocks + (CASE WHEN rsc.last = 1 THEN (rsc.fees-floor(rsc.fees/${delegates})*${delegates}) ELSE 0 END)) AS fees,', + 'SUM(rsc.rewards) AS rewards', + 'FROM rsc' + ].filter(Boolean).join(' '); + }, + list: function (params) { return [ 'SELECT * FROM blocks_list', diff --git a/sql/delegates.js b/sql/delegates.js index ad2a17f490b..425a8d9245b 100644 --- a/sql/delegates.js +++ b/sql/delegates.js @@ -19,7 +19,7 @@ var DelegatesSql = { 'SELECT m."username", m."address", ENCODE(m."publicKey", \'hex\') AS "publicKey", m."vote", m."producedblocks", m."missedblocks"', 'FROM mem_accounts m', 'WHERE m."isDelegate" = 1 AND m."username" LIKE ${q}', - 'ORDER BY ' + [params.sortField, params.sortMethod].join(' '), + 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') + ' NULLS LAST', 'LIMIT ${limit}' ].join(' '); diff --git a/sql/migrations/20170113181857_addConstraintsToPeers.sql b/sql/migrations/20170113181857_addConstraintsToPeers.sql new file mode 100644 index 00000000000..349d695b6ee --- /dev/null +++ b/sql/migrations/20170113181857_addConstraintsToPeers.sql @@ -0,0 +1,11 @@ +/* Add constraints to improve upserts + * + */ + +BEGIN; + +ALTER TABLE "peers" + ADD CONSTRAINT "address_unique" UNIQUE + USING INDEX "peers_unique"; + +COMMIT; \ No newline at end of file diff --git a/sql/migrations/20170124071600_recreateTrsListView.sql b/sql/migrations/20170124071600_recreateTrsListView.sql new file mode 100644 index 00000000000..165ca5b3810 --- /dev/null +++ b/sql/migrations/20170124071600_recreateTrsListView.sql @@ -0,0 +1,33 @@ +/* + * Add 'm_recipientPublicKey' column to 'trs_list' view + * Change 't_senderPublicKey' data type from 'string' to 'bytea' + */ + +BEGIN; + +DROP VIEW IF EXISTS trs_list; + +CREATE VIEW trs_list AS + +SELECT t."id" AS "t_id", + b."height" AS "b_height", + t."blockId" AS "t_blockId", + t."type" AS "t_type", + t."timestamp" AS "t_timestamp", + t."senderPublicKey" AS "t_senderPublicKey", + m."publicKey" AS "m_recipientPublicKey", + t."senderId" AS "t_senderId", + t."recipientId" AS "t_recipientId", + t."amount" AS "t_amount", + t."fee" AS "t_fee", + ENCODE(t."signature", 'hex') AS "t_signature", + ENCODE(t."signSignature", 'hex') AS "t_SignSignature", + t."signatures" AS "t_signatures", + (SELECT MAX("height") + 1 FROM blocks) - b."height" AS "confirmations" + +FROM trs t + +INNER JOIN blocks b ON t."blockId" = b."id" +LEFT JOIN mem_accounts m ON t."recipientId" = m."address"; + +COMMIT; diff --git a/sql/peers.js b/sql/peers.js index d707cf83831..3a59dcddae1 100644 --- a/sql/peers.js +++ b/sql/peers.js @@ -7,11 +7,18 @@ var PeersSql = { banManager: 'UPDATE peers SET "state" = 1, "clock" = null WHERE ("state" = 0 AND "clock" - ${now} < 0)', + countByFilter: function (params) { + return [ + 'SELECT COUNT(*)::int FROM peers', + (params.where.length ? 'WHERE ' + params.where.join(' AND ') : '') + ].filter(Boolean).join(' '); + }, + getByFilter: function (params) { return [ 'SELECT "ip", "port", "state", "os", "version", ENCODE("broadhash", \'hex\') AS "broadhash", "height" FROM peers', (params.where.length ? 'WHERE ' + params.where.join(' AND ') : ''), - (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') : 'ORDER BY RANDOM()'), + (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') + ' NULLS LAST' : 'ORDER BY RANDOM()'), 'LIMIT ${limit} OFFSET ${offset}' ].filter(Boolean).join(' '); }, @@ -34,7 +41,7 @@ var PeersSql = { addDapp: 'INSERT INTO peers_dapp ("peerId", "dappid") VALUES (${peerId}, ${dappId}) ON CONFLICT DO NOTHING', - upsert: 'INSERT INTO peers AS p ("ip", "port", "state", "os", "version", "broadhash", "height") VALUES (${ip}, ${port}, ${state}, ${os}, ${version}, ${broadhash}, ${height}) ON CONFLICT ("ip", "port") DO UPDATE SET ("ip", "port", "state", "os", "version", "broadhash", "height") = (${ip}, ${port}, (CASE WHEN p."state" = 0 THEN p."state" ELSE ${state} END), ${os}, ${version}, (CASE WHEN ${broadhash} IS NULL THEN p."broadhash" ELSE ${broadhash} END), (CASE WHEN ${height} IS NULL THEN p."height" ELSE ${height} END))' + upsert: 'INSERT INTO peers AS p ("ip", "port", "state", "os", "version", "broadhash", "height") VALUES (${ip}, ${port}, ${state}, ${os}, ${version}, ${broadhash}, ${height}) ON CONFLICT ON CONSTRAINT address_unique DO UPDATE SET ("ip", "port", "state", "os", "version", "broadhash", "height") = (${ip}, ${port}, (CASE WHEN p."state" = 0 THEN p."state" ELSE ${state} END), ${os}, ${version}, (CASE WHEN ${broadhash} IS NULL THEN p."broadhash" ELSE ${broadhash} END), (CASE WHEN ${height} IS NULL THEN p."height" ELSE ${height} END))' }; module.exports = PeersSql; diff --git a/sql/rounds.js b/sql/rounds.js index 886e0f17aac..2fff43a4a64 100644 --- a/sql/rounds.js +++ b/sql/rounds.js @@ -19,7 +19,7 @@ var RoundsSql = { updateBlockId: 'UPDATE mem_accounts SET "blockId" = ${newId} WHERE "blockId" = ${oldId};', - summedRound: 'SELECT SUM(b."totalFee")::bigint AS "fees", ARRAY_AGG(b."reward") AS "rewards", ARRAY_AGG(ENCODE(b."generatorPublicKey", \'hex\')) AS "delegates" FROM blocks b WHERE (SELECT (CAST(b."height" / ${activeDelegates} AS INTEGER) + (CASE WHEN b."height" % ${activeDelegates} > 0 THEN 1 ELSE 0 END))) = ${round}' + summedRound: 'WITH round_blocks as (SELECT ((${round}-1)*${activeDelegates})+1 as min, ${round}*${activeDelegates} as max) SELECT SUM(b."totalFee")::bigint AS "fees", ARRAY_AGG(b."reward") AS "rewards", ARRAY_AGG(ENCODE(b."generatorPublicKey", \'hex\')) AS "delegates" FROM blocks b WHERE b.height BETWEEN (SELECT min FROM round_blocks) AND (SELECT max FROM round_blocks)' }; module.exports = RoundsSql; diff --git a/sql/transactions.js b/sql/transactions.js index cb2d2cf7d2c..d5a99f04520 100644 --- a/sql/transactions.js +++ b/sql/transactions.js @@ -15,31 +15,36 @@ var TransactionsSql = { 'height' ], + count: 'SELECT COUNT("id")::int AS "count" FROM trs', + countById: 'SELECT COUNT("id")::int AS "count" FROM trs WHERE "id" = ${id}', countList: function (params) { return [ - 'SELECT COUNT("t_id") FROM trs_list', - 'INNER JOIN blocks b ON "t_blockId" = b."id"', + 'SELECT COUNT(1) FROM trs_list', (params.where.length || params.owner ? 'WHERE' : ''), - (params.where.length ? '(' + params.where.join(' OR ') + ')' : ''), + (params.where.length ? '(' + params.where.join(' ') + ')' : ''), + // FIXME: Backward compatibility, should be removed after transitional period (params.where.length && params.owner ? ' AND ' + params.owner : params.owner) ].filter(Boolean).join(' '); }, list: function (params) { - // Need to fix 'or' or 'and' in query return [ - 'SELECT * FROM trs_list', + 'SELECT "t_id", "b_height", "t_blockId", "t_type", "t_timestamp", "t_senderId", "t_recipientId",', + '"t_amount", "t_fee", "t_signature", "t_SignSignature", "t_signatures", "confirmations",', + 'ENCODE ("t_senderPublicKey", \'hex\') AS "t_senderPublicKey", ENCODE ("m_recipientPublicKey", \'hex\') AS "m_recipientPublicKey"', + 'FROM trs_list', (params.where.length || params.owner ? 'WHERE' : ''), - (params.where.length ? '(' + params.where.join(' OR ') + ')' : ''), + (params.where.length ? '(' + params.where.join(' ') + ')' : ''), + // FIXME: Backward compatibility, should be removed after transitional period (params.where.length && params.owner ? ' AND ' + params.owner : params.owner), - (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') : ''), + (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') + ' NULLS LAST' : ''), 'LIMIT ${limit} OFFSET ${offset}' ].filter(Boolean).join(' '); }, - getById: 'SELECT * FROM trs_list WHERE "t_id" = ${id}', + getById: 'SELECT *, ENCODE ("t_senderPublicKey", \'hex\') AS "t_senderPublicKey", ENCODE ("m_recipientPublicKey", \'hex\') AS "m_recipientPublicKey" FROM trs_list WHERE "t_id" = ${id}', getVotesById: 'SELECT * FROM votes WHERE "transactionId" = ${id}' }; diff --git a/test/api/accounts.js b/test/api/accounts.js index 4a5b9b4b9c9..494c3477aa6 100644 --- a/test/api/accounts.js +++ b/test/api/accounts.js @@ -232,14 +232,31 @@ describe('POST /api/accounts/generatePublicKey', function () { }); }); -describe('GET /accounts?address=', function () { +describe('GET /accounts', function () { - function getAccounts (address, done) { - node.get('/api/accounts?address=' + address, done); + function getAccounts (params, done) { + node.get('/api/accounts?' + params, done); } it('using known address should be ok', function (done) { - getAccounts(node.gAccount.address, function (err, res) { + getAccounts('address=' + node.gAccount.address, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('account').that.is.an('object'); + node.expect(res.body.account).to.have.property('address').to.equal(node.gAccount.address); + node.expect(res.body.account).to.have.property('unconfirmedBalance').that.is.a('string'); + node.expect(res.body.account).to.have.property('balance').that.is.a('string'); + node.expect(res.body.account).to.have.property('publicKey').to.equal(node.gAccount.publicKey); + node.expect(res.body.account).to.have.property('unconfirmedSignature').to.equal(0); + node.expect(res.body.account).to.have.property('secondSignature').to.equal(0); + node.expect(res.body.account).to.have.property('secondPublicKey').to.equal(null); + node.expect(res.body.account).to.have.property('multisignatures').to.a('array'); + node.expect(res.body.account).to.have.property('u_multisignatures').to.a('array'); + done(); + }); + }); + + it('using known address and empty publicKey should be ok', function (done) { + getAccounts('address=' + node.gAccount.address + '&publicKey=', function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('account').that.is.an('object'); node.expect(res.body.account).to.have.property('address').to.equal(node.gAccount.address); @@ -256,7 +273,7 @@ describe('GET /accounts?address=', function () { }); it('using known lowercase address should be ok', function (done) { - getAccounts(node.gAccount.address.toLowerCase(), function (err, res) { + getAccounts('address=' + node.gAccount.address.toLowerCase(), function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('account').that.is.an('object'); node.expect(res.body.account).to.have.property('address').to.equal(node.gAccount.address); @@ -273,7 +290,7 @@ describe('GET /accounts?address=', function () { }); it('using unknown address should fail', function (done) { - getAccounts(account.address, function (err, res) { + getAccounts('address=' + account.address, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; node.expect(res.body).to.have.property('error').to.eql('Account not found'); done(); @@ -281,7 +298,7 @@ describe('GET /accounts?address=', function () { }); it('using invalid address should fail', function (done) { - getAccounts('thisIsNOTALiskAddress', function (err, res) { + getAccounts('address=' + 'thisIsNOTALiskAddress', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; node.expect(res.body).to.have.property('error'); node.expect(res.body.error).to.contain('Object didn\'t pass validation for format address: thisIsNOTALiskAddress'); @@ -290,11 +307,107 @@ describe('GET /accounts?address=', function () { }); it('using empty address should fail', function (done) { - getAccounts('', function (err, res) { + getAccounts('address=', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + node.expect(res.body.error).to.contain('String is too short (0 chars), minimum 1'); + done(); + }); + }); + + it('using known publicKey should be ok', function (done) { + getAccounts('publicKey=' + node.gAccount.publicKey, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('account').that.is.an('object'); + node.expect(res.body.account).to.have.property('address').to.equal(node.gAccount.address); + node.expect(res.body.account).to.have.property('unconfirmedBalance').that.is.a('string'); + node.expect(res.body.account).to.have.property('balance').that.is.a('string'); + node.expect(res.body.account).to.have.property('publicKey').to.equal(node.gAccount.publicKey); + node.expect(res.body.account).to.have.property('unconfirmedSignature').to.equal(0); + node.expect(res.body.account).to.have.property('secondSignature').to.equal(0); + node.expect(res.body.account).to.have.property('secondPublicKey').to.equal(null); + node.expect(res.body.account).to.have.property('multisignatures').to.a('array'); + node.expect(res.body.account).to.have.property('u_multisignatures').to.a('array'); + done(); + }); + }); + + it('using known publicKey and empty address should fail', function (done) { + getAccounts('publicKey=' + node.gAccount.publicKey + '&address=', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.eql('String is too short (0 chars), minimum 1'); + done(); + }); + }); + + it('using unknown publicKey should fail', function (done) { + getAccounts('publicKey=' + account.publicKey, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.eql('Account not found'); + done(); + }); + }); + + it('using invalid publicKey should fail', function (done) { + getAccounts('publicKey=' + 'thisIsNOTALiskAccountPublicKey', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + node.expect(res.body.error).to.contain('Object didn\'t pass validation for format publicKey: thisIsNOTALiskAccountPublicKey'); + done(); + }); + }); + + it('using invalid publicKey (integer) should fail', function (done) { + getAccounts('publicKey=' + '123', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + node.expect(res.body.error).to.contain('Expected type string but found type integer'); + done(); + }); + }); + + it('using empty publicKey should fail', function (done) { + getAccounts('publicKey=', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + node.expect(res.body.error).to.contain('Missing required property: address or publicKey'); + done(); + }); + }); + + it('using empty publicKey and address should fail', function (done) { + getAccounts('publicKey=&address=', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; node.expect(res.body).to.have.property('error'); node.expect(res.body.error).to.contain('String is too short (0 chars), minimum 1'); done(); }); }); + + it('using known address and matching publicKey should be ok', function (done) { + getAccounts('address=' + node.gAccount.address + '&publicKey=' + node.gAccount.publicKey, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('account').that.is.an('object'); + node.expect(res.body.account).to.have.property('address').to.equal(node.gAccount.address); + node.expect(res.body.account).to.have.property('unconfirmedBalance').that.is.a('string'); + node.expect(res.body.account).to.have.property('balance').that.is.a('string'); + node.expect(res.body.account).to.have.property('publicKey').to.equal(node.gAccount.publicKey); + node.expect(res.body.account).to.have.property('unconfirmedSignature').to.equal(0); + node.expect(res.body.account).to.have.property('secondSignature').to.equal(0); + node.expect(res.body.account).to.have.property('secondPublicKey').to.equal(null); + node.expect(res.body.account).to.have.property('multisignatures').to.a('array'); + node.expect(res.body.account).to.have.property('u_multisignatures').to.a('array'); + done(); + }); + }); + + it('using known address and not matching publicKey should fail', function (done) { + getAccounts('address=' + node.gAccount.address + '&publicKey=' + account.publicKey, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + node.expect(res.body.error).to.contain('Account publicKey do not match address'); + done(); + }); + }); + }); diff --git a/test/api/delegates.js b/test/api/delegates.js index 3e648dc27c1..0cb6a23c5f7 100644 --- a/test/api/delegates.js +++ b/test/api/delegates.js @@ -629,6 +629,33 @@ describe('GET /api/delegates', function () { done(); }); }); + + it('using orderBy with any of sort fields should not place NULLs first', function (done) { + var delegatesSortFields = ['approval', 'productivity', 'rate', 'vote']; + node.async.each(delegatesSortFields, function (sortField, cb) { + node.get('/api/delegates?orderBy=' + sortField, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('delegates').that.is.an('array'); + + var dividedIndices = res.body.delegates.reduce(function (memo, peer, index) { + memo[peer[sortField] === null ? 'nullIndices' : 'notNullIndices'].push(index); + return memo; + }, {notNullIndices: [], nullIndices: []}); + + if (dividedIndices.nullIndices.length && dividedIndices.notNullIndices.length) { + var ascOrder = function (a, b) { return a - b; }; + dividedIndices.notNullIndices.sort(ascOrder); + dividedIndices.nullIndices.sort(ascOrder); + + node.expect(dividedIndices.notNullIndices[dividedIndices.notNullIndices.length - 1]) + .to.be.at.most(dividedIndices.nullIndices[0]); + } + cb(); + }); + }, function () { + done(); + }); + }); }); describe('GET /api/delegates/count', function () { @@ -959,10 +986,11 @@ describe('GET /api/delegates/search', function () { }); describe('GET /api/delegates/forging/status', function () { - it('using no params should fail', function (done) { + it('using no params should be ok', function (done) { node.get('/api/delegates/forging/status', function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.eql('Missing required property: publicKey'); + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('enabled').to.be.true; + node.expect(res.body).to.have.property('delegates').that.is.an('array'); done(); }); }); @@ -978,7 +1006,8 @@ describe('GET /api/delegates/forging/status', function () { it('using empty publicKey should be ok', function (done) { node.get('/api/delegates/forging/status?publicKey=', function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; - node.expect(res.body).to.have.property('enabled').to.be.false; + node.expect(res.body).to.have.property('enabled').to.be.true; + node.expect(res.body).to.have.property('delegates').that.is.an('array'); done(); }); }); @@ -1000,12 +1029,144 @@ describe('GET /api/delegates/forging/status', function () { }); }); +describe('GET /api/delegates/forging/getForgedByAccount', function () { + + var validParams; + + beforeEach(function () { + validParams = { + generatorPublicKey: '9d3058175acab969f41ad9b86f7a2926c74258670fe56b37c429c01fca9f2f0f', + start: 0, + end: 0 + }; + }); + + function buildParams () { + return [ + 'generatorPublicKey=' + validParams.generatorPublicKey, + validParams.start !== undefined ? 'start=' + validParams.start : '', + validParams.end !== undefined ? 'end=' + validParams.end : '', + ].filter(Boolean).join('&'); + } + + it('using no params should fail', function (done) { + node.get('/api/delegates/forging/getForgedByAccount', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.eql('Missing required property: generatorPublicKey'); + done(); + }); + }); + + it('using valid params should be ok', function (done) { + delete validParams.start; + delete validParams.end; + + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('fees').that.is.a('string'); + node.expect(res.body).to.have.property('rewards').that.is.a('string'); + node.expect(res.body).to.have.property('forged').that.is.a('string'); + done(); + }); + }); + + it('using valid params with borders should be ok', function (done) { + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('fees').that.is.a('string').and.eql('0'); + node.expect(res.body).to.have.property('rewards').that.is.a('string').and.eql('0'); + node.expect(res.body).to.have.property('forged').that.is.a('string').and.eql('0'); + node.expect(res.body).to.have.property('count').that.is.a('string').and.eql('0'); + done(); + }); + }); + + it('using unknown generatorPublicKey should fail', function (done) { + validParams.generatorPublicKey = node.randomAccount().publicKey; + delete validParams.start; + delete validParams.end; + + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.eql('Account not found'); + done(); + }); + }); + + it('using unknown generatorPublicKey with borders should fail', function (done) { + validParams.generatorPublicKey = node.randomAccount().publicKey; + + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.eql('Account not found or is not a delegate'); + done(); + }); + }); + + it('using invalid generatorPublicKey should fail', function (done) { + validParams.generatorPublicKey = 'invalidPublicKey'; + + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.eql('Object didn\'t pass validation for format publicKey: invalidPublicKey'); + done(); + }); + }); + + it('using no start should be ok', function (done) { + delete validParams.start; + + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('fees').that.is.a('string').and.eql('0'); + node.expect(res.body).to.have.property('rewards').that.is.a('string').and.eql('0'); + node.expect(res.body).to.have.property('forged').that.is.a('string').and.eql('0'); + node.expect(res.body).to.have.property('count').that.is.a('string').and.eql('0'); + done(); + }); + }); + + it('using no end should be ok', function (done) { + delete validParams.end; + + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('fees').that.is.a('string'); + node.expect(res.body).to.have.property('rewards').that.is.a('string'); + node.expect(res.body).to.have.property('forged').that.is.a('string'); + node.expect(res.body).to.have.property('count').that.is.a('string'); + done(); + }); + }); + + it('using string start should fail', function (done) { + validParams.start = 'one'; + + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.eql('Expected type integer but found type string'); + done(); + }); + }); + + it('using string end should fail', function (done) { + validParams.end = 'two'; + + node.get('/api/delegates/forging/getForgedByAccount?' + buildParams(), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.eql('Expected type integer but found type string'); + done(); + }); + }); +}); + describe('GET /api/delegates/getNextForgers', function () { it('using no params should be ok', function (done) { node.get('/api/delegates/getNextForgers', function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('currentBlock').that.is.a('number'); + node.expect(res.body).to.have.property('currentBlockSlot').that.is.a('number'); node.expect(res.body).to.have.property('currentSlot').that.is.a('number'); node.expect(res.body).to.have.property('delegates').that.is.an('array'); node.expect(res.body.delegates).to.have.lengthOf(10); @@ -1017,6 +1178,7 @@ describe('GET /api/delegates/getNextForgers', function () { node.get('/api/delegates/getNextForgers?' + 'limit=1', function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('currentBlock').that.is.a('number'); + node.expect(res.body).to.have.property('currentBlockSlot').that.is.a('number'); node.expect(res.body).to.have.property('currentSlot').that.is.a('number'); node.expect(res.body).to.have.property('delegates').that.is.an('array'); node.expect(res.body.delegates).to.have.lengthOf(1); @@ -1028,6 +1190,7 @@ describe('GET /api/delegates/getNextForgers', function () { node.get('/api/delegates/getNextForgers?' + 'limit=101', function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('currentBlock').that.is.a('number'); + node.expect(res.body).to.have.property('currentBlockSlot').that.is.a('number'); node.expect(res.body).to.have.property('currentSlot').that.is.a('number'); node.expect(res.body).to.have.property('delegates').that.is.an('array'); node.expect(res.body.delegates).to.have.lengthOf(101); diff --git a/test/api/multisignatures.js b/test/api/multisignatures.js index f1906c56332..f533a9ca081 100644 --- a/test/api/multisignatures.js +++ b/test/api/multisignatures.js @@ -61,7 +61,9 @@ function confirmTransaction (transactionId, passphrases, done) { secret: passphrase, transactionId: transactionId }, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; + if (err || !res.body.success) { + return untilCb(err || res.body.error); + } node.expect(res.body).to.have.property('transactionId').to.equal(transactionId); count++; return untilCb(); @@ -160,10 +162,10 @@ describe('PUT /api/multisignatures', function () { delete validParams.keysgroup; node.put('/api/multisignatures', validParams, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error'); - done(); - }); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + done(); + }); }); it('using string keysgroup should fail', function (done) { @@ -547,3 +549,45 @@ describe('POST /api/multisignatures/sign (transaction)', function () { }); }); }); + +describe('POST /api/multisignatures/sign (regular account)', function () { + + var transactionId; + + before(function (done) { + node.put('/api/transactions/', { + secret: node.gAccount.password , + amount: 1, + recipientId: accounts[0].address + }, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactionId').that.is.not.empty; + transactionId = res.body.transactionId; + done(); + }); + }); + + it('should be impossible to sign the transaction', function (done) { + node.onNewBlock(function (err) { + node.get('/api/transactions/get?id=' + transactionId, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transaction'); + node.expect(res.body.transaction).to.have.property('id').to.equal(transactionId); + confirmTransaction(transactionId, [multisigAccount.password], function (err, res) { + node.expect(err).not.to.be.empty; + done(); + }); + }); + }); + }); + + it('should have no pending multisignatures', function (done) { + node.get('/api/multisignatures/pending?publicKey=' + accounts[0].publicKey, function (err, res) { + node.expect(res.body).to.have.property('success'); + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactions').that.is.an('array'); + node.expect(res.body.transactions.length).to.equal(0); + done(); + }); + }); +}); diff --git a/test/api/peer.js b/test/api/peer.js index 1791d29fea0..7719fdf4b0d 100644 --- a/test/api/peer.js +++ b/test/api/peer.js @@ -1,11 +1,16 @@ 'use strict'; /*jslint mocha:true, expr:true */ var node = require('./../node.js'); +var ip = require('ip'); describe('GET /peer/list', function () { before(function (done) { - node.addPeers(2, done); + node.addPeers(2, '0.0.0.0', done); + }); + + before(function (done) { + node.addPeers(1, ip.address('public'), done); }); it('using incorrect nethash in headers should fail', function (done) { @@ -50,6 +55,20 @@ describe('GET /peer/list', function () { done(); }); }); + + it('should not accept itself as a peer', function (done) { + node.get('/peer/list') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('peers').that.is.an('array'); + res.body.peers.forEach(function (peer) { + node.expect(peer).to.have.property('ip').that.is.a('string'); + node.expect(peer.ip).not.to.equal(ip.address('public')); + }); + done(); + }); + }); }); describe('GET /peer/height', function () { diff --git a/test/api/peer.transactions.collision.js b/test/api/peer.transactions.collision.js index 77591376d68..6bd3d1e8d62 100644 --- a/test/api/peer.transactions.collision.js +++ b/test/api/peer.transactions.collision.js @@ -38,7 +38,7 @@ describe('POST /peer/transactions', function () { postTransaction(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('message').to.equal('Invalid sender public key: b26dd40ba33e4785e49ddc4f106c0493ed00695817235c778f487aea5866400a expected: ce33db918b059a6e99c402963b42cf51c695068007ef01d8c383bb8a41270263'); + node.expect(res.body).to.have.property('message').to.equal('Failed to verify signature'); done(); }); }); diff --git a/test/api/peers.js b/test/api/peers.js index 7109e25ae64..df7d5d65898 100644 --- a/test/api/peers.js +++ b/test/api/peers.js @@ -1,6 +1,7 @@ 'use strict'; /*jslint mocha:true, expr:true */ var node = require('./../node.js'); +var peersSortFields = require('../../sql/peers').sortFields; describe('GET /api/peers/version', function () { @@ -8,12 +9,26 @@ describe('GET /api/peers/version', function () { node.get('/api/peers/version', function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('build').to.be.a('string'); + node.expect(res.body).to.have.property('commit').to.be.a('string'); node.expect(res.body).to.have.property('version').to.be.a('string'); done(); }); }); }); +describe('GET /api/peers/count', function () { + + it('should be ok', function (done) { + node.get('/api/peers/count', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('connected').that.is.a('number'); + node.expect(res.body).to.have.property('disconnected').that.is.a('number'); + node.expect(res.body).to.have.property('banned').that.is.a('number'); + done (); + }); + }); +}); + describe('GET /api/peers', function () { it('using invalid ip should fail', function (done) { @@ -335,6 +350,32 @@ describe('GET /api/peers', function () { }); }); + it('using orderBy with any of sort fields should not place NULLs first', function (done) { + node.async.each(peersSortFields, function (sortField, cb) { + node.get('/api/peers?orderBy=' + sortField, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('peers').that.is.an('array'); + + var dividedIndices = res.body.peers.reduce(function (memo, peer, index) { + memo[peer[sortField] === null ? 'nullIndices' : 'notNullIndices'].push(index); + return memo; + }, {notNullIndices: [], nullIndices: []}); + + if (dividedIndices.nullIndices.length && dividedIndices.notNullIndices.length) { + var ascOrder = function (a, b) { return a - b; }; + dividedIndices.notNullIndices.sort(ascOrder); + dividedIndices.nullIndices.sort(ascOrder); + + node.expect(dividedIndices.notNullIndices[dividedIndices.notNullIndices.length - 1]) + .to.be.at.most(dividedIndices.nullIndices[0]); + } + cb(); + }); + }, function () { + done(); + }); + }); + it('using string limit should fail', function (done) { var limit = 'one'; var params = 'limit=' + limit; @@ -441,7 +482,7 @@ describe('GET /api/peers/get', function () { var validParams; before(function (done) { - node.addPeers(1, function (err, headers) { + node.addPeers(1, '0.0.0.0', function (err, headers) { validParams = headers; done(); }); diff --git a/test/api/transactions.js b/test/api/transactions.js index b91b738d2fe..c4a11002f19 100644 --- a/test/api/transactions.js +++ b/test/api/transactions.js @@ -1,6 +1,7 @@ 'use strict'; /*jslint mocha:true, expr:true */ var node = require('./../node.js'); +var transactionSortFields = require('../../sql/transactions').sortFields; var account = node.randomTxAccount(); var account2 = node.randomTxAccount(); @@ -94,6 +95,150 @@ describe('GET /api/transactions', function () { }); }); + it('using valid parameters with and/or should be ok', function (done) { + var limit = 10; + var offset = 0; + var orderBy = 'amount:asc'; + + var params = [ + 'and:blockId=' + '1', + 'or:senderId=' + node.gAccount.address, + 'or:recipientId=' + account.address, + 'limit=' + limit, + 'offset=' + offset, + 'orderBy=' + orderBy + ]; + + node.get('/api/transactions?' + params.join('&'), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactions').that.is.an('array'); + node.expect(res.body.transactions).to.have.length.within(transactionList.length, limit); + for (var i = 0; i < res.body.transactions.length; i++) { + if (res.body.transactions[i + 1]) { + node.expect(res.body.transactions[i].amount).to.be.at.most(res.body.transactions[i + 1].amount); + } + } + done(); + }); + }); + + it('using valid parameters with/without and/or should be ok', function (done) { + var limit = 10; + var offset = 0; + var orderBy = 'amount:asc'; + + var params = [ + 'and:blockId=' + '1', + 'or:senderId=' + node.gAccount.address, + 'or:recipientId=' + account.address, + 'fromHeight=' + 1, + 'toHeight=' + 666, + 'and:fromTimestamp=' + 0, + 'and:minAmount=' + 0, + 'limit=' + limit, + 'offset=' + offset, + 'orderBy=' + orderBy + ]; + + node.get('/api/transactions?' + params.join('&'), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactions').that.is.an('array'); + node.expect(res.body.transactions).to.have.length.within(transactionList.length, limit); + for (var i = 0; i < res.body.transactions.length; i++) { + if (res.body.transactions[i + 1]) { + node.expect(res.body.transactions[i].amount).to.be.at.most(res.body.transactions[i + 1].amount); + } + } + done(); + }); + }); + + it('using valid array-like parameters should be ok', function (done) { + var limit = 10; + var offset = 0; + var orderBy = 'amount:asc'; + + var params = [ + 'blockId=' + '1', + 'or:senderIds=' + node.gAccount.address + ',' + account.address, + 'or:recipientIds=' + account.address + ',' + account2.address, + 'or:senderPublicKeys=' + node.gAccount.publicKey, + 'or:recipientPublicKeys=' + node.gAccount.publicKey + ',' + account.publicKey, + 'limit=' + limit, + 'offset=' + offset, + 'orderBy=' + orderBy + ]; + + node.get('/api/transactions?' + params.join('&'), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactions').that.is.an('array'); + node.expect(res.body.transactions).to.have.length.within(transactionList.length, limit); + for (var i = 0; i < res.body.transactions.length; i++) { + if (res.body.transactions[i + 1]) { + node.expect(res.body.transactions[i].amount).to.be.at.most(res.body.transactions[i + 1].amount); + } + } + done(); + }); + }); + + it('using one invalid field name with and/or should fail', function (done) { + var limit = 10; + var offset = 0; + var orderBy = 'amount:asc'; + + var params = [ + 'and:blockId=' + '1', + 'or:senderId=' + node.gAccount.address, + 'or:whatever=' + account.address, + 'limit=' + limit, + 'offset=' + offset, + 'orderBy=' + orderBy + ]; + + node.get('/api/transactions?' + params.join('&'), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + done(); + }); + }); + + it('using invalid condition should fail', function (done) { + var params = [ + 'whatever:senderId=' + node.gAccount.address + ]; + + node.get('/api/transactions?' + params.join('&'), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + done(); + }); + }); + + it('using invalid field name (x:y:z) should fail', function (done) { + var params = [ + 'or:whatever:senderId=' + node.gAccount.address + ]; + + node.get('/api/transactions?' + params.join('&'), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + done(); + }); + }); + + it('using empty parameter should fail', function (done) { + var params = [ + 'and:publicKey=' + ]; + + node.get('/api/transactions?' + params.join('&'), function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error'); + done(); + }); + }); + it('using type should be ok', function (done) { var type = node.txTypes.SEND; var params = 'type=' + type; @@ -123,8 +268,8 @@ describe('GET /api/transactions', function () { }); }); - it('using limit > 100 should fail', function (done) { - var limit = 101; + it('using limit > 1000 should fail', function (done) { + var limit = 1001; var params = 'limit=' + limit; node.get('/api/transactions?' + params, function (err, res) { @@ -215,6 +360,32 @@ describe('GET /api/transactions', function () { done(); }); }); + + it('using orderBy with any of sort fields should not place NULLs first', function (done) { + node.async.each(transactionSortFields, function (sortField, cb) { + node.get('/api/transactions?orderBy=' + sortField, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactions').that.is.an('array'); + + var dividedIndices = res.body.transactions.reduce(function (memo, peer, index) { + memo[peer[sortField] === null ? 'nullIndices' : 'notNullIndices'].push(index); + return memo; + }, {notNullIndices: [], nullIndices: []}); + + if (dividedIndices.nullIndices.length && dividedIndices.notNullIndices.length) { + var ascOrder = function (a, b) { return a - b; }; + dividedIndices.notNullIndices.sort(ascOrder); + dividedIndices.nullIndices.sort(ascOrder); + + node.expect(dividedIndices.notNullIndices[dividedIndices.notNullIndices.length - 1]) + .to.be.at.most(dividedIndices.nullIndices[0]); + } + cb(); + }); + }, function () { + done(); + }); + }); }); describe('GET /api/transactions/get?id=', function () { @@ -247,6 +418,20 @@ describe('GET /api/transactions/get?id=', function () { }); }); +describe('GET /api/transactions/count', function () { + + it('should be ok', function (done) { + node.get('/api/transactions/count', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('confirmed').that.is.an('number'); + node.expect(res.body).to.have.property('queued').that.is.an('number'); + node.expect(res.body).to.have.property('multisignature').that.is.an('number'); + node.expect(res.body).to.have.property('unconfirmed').that.is.an('number'); + done(); + }); + }); +}); + describe('GET /api/transactions/queued/get?id=', function () { it('using unknown id should be ok', function (done) { diff --git a/test/common/globalBefore.js b/test/common/globalBefore.js new file mode 100644 index 00000000000..706a6281c09 --- /dev/null +++ b/test/common/globalBefore.js @@ -0,0 +1,52 @@ +'use strict'; + +var node = require('./../node.js'); + +/** + * @param {string} table + * @param {Logger} logger + * @param {Object} db + * @param {Function} cb + */ +function clearDatabaseTable (db, logger, table, cb) { + db.query('DELETE FROM ' + table).then(function (result) { + cb(null, result); + }).catch(function (err) { + logger.err('Failed to clear database table: ' + table); + throw err; + }); +} + +/** + * @param {Function} cb + * @param {Number} [retries=10] retries + * @param {Number} [timeout=200] timeout + */ +function waitUntilBlockchainReady (cb, retries, timeout) { + if (!retries) { + retries = 10; + } + if (!timeout) { + timeout = 200; + } + (function fetchBlockchainStatus () { + node.get('/api/loader/status', function (err, res) { + node.expect(err).to.not.exist; + retries -= 1; + if (!res.body.success && res.body.error === 'Blockchain is loading' && retries >= 0) { + return setTimeout(function () { + fetchBlockchainStatus(); + }, timeout); + } + else if (res.body.success && res.body.loaded) { + return cb(); + } + return cb('Failed to load blockchain'); + }); + })(); +} + +module.exports = { + clearDatabaseTable: clearDatabaseTable, + waitUntilBlockchainReady: waitUntilBlockchainReady +}; diff --git a/test/common/initModule.js b/test/common/initModule.js new file mode 100644 index 00000000000..61ec117d5c4 --- /dev/null +++ b/test/common/initModule.js @@ -0,0 +1,66 @@ +'use strict'; + +var express = require('express'); +var merge = require('lodash/merge'); + +var path = require('path'); +var dirname = path.join(__dirname, '..', '..'); +var config = require(path.join(dirname, '/config.json')); +var database = require(path.join(dirname, '/helpers', 'database.js')); +var genesisblock = require(path.join(dirname, '/genesisBlock.json')); +var Logger = require(dirname + '/logger.js'); + +var modulesLoader = new function() { + + this.db = null; + this.logger = new Logger({ echo: null, errorLevel: config.fileLogLevel, filename: config.logFileName }); + /** + * @param {Function} Module + * @param {Object} scope + * @param {Function} cb + */ + this.init = function (Module, scope, cb) { + new Module(function (err, module) { + return cb(err, module); + }, merge({}, scope, { + network: { + app: express() + }, + genesisblock: genesisblock + })); + }; + + /** + * @param {Function} Module + * @param {Function} cb + */ + this.initWithDb = function (Module, cb) { + this.getDbConnection(function (err, db) { + if (err) { + return cb(err); + } + this.init(Module, {db: db}, cb); + }.bind(this)); + }; + + /** + * @param {Function} cb + */ + this.getDbConnection = function (cb) { + if (this.db) { + return cb(null, this.db); + } + database.connect(config.db, this.logger, function (err, db) { + if (err) { + return cb(err); + } + this.db = db; + cb(null, this.db); + }.bind(this)); + }; + +}; + +module.exports = { + modulesLoader: modulesLoader +}; \ No newline at end of file diff --git a/test/index.js b/test/index.js index 86fe0fcaa5f..66a8b8e5d0e 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,6 @@ require('./unit/helpers/request-limiter.js'); require('./unit/logic/blockReward.js'); +require('./unit/modules/peers.js'); require('./api/accounts.js'); require('./api/blocks.js'); diff --git a/test/mocha.opts b/test/mocha.opts index 05ea6d8ec12..4aa4037ca41 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1 +1,5 @@ ---timeout 250s +--timeout 500s +--reporter spec +--quiet false +--clearRequireCache false +--noFail false \ No newline at end of file diff --git a/test/node.js b/test/node.js index bd6ef163ce8..f0a8d135e68 100644 --- a/test/node.js +++ b/test/node.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /*jslint mocha:true, expr:true */ // Root object var node = {}; @@ -186,7 +186,7 @@ node.waitForNewBlock = function (height, blocksToWait, cb) { }; // Adds peers to local node -node.addPeers = function (numOfPeers, cb) { +node.addPeers = function (numOfPeers, ip, cb) { var operatingSystems = ['win32','win64','ubuntu','debian', 'centos']; var port = 4000; var os, version; @@ -205,7 +205,7 @@ node.addPeers = function (numOfPeers, cb) { height: 1, nethash: node.config.nethash, os: os, - ip: '0.0.0.0', + ip: ip, port: port, version: version } @@ -361,5 +361,9 @@ node.put = function (path, params, done) { return abstractRequest({ verb: 'PUT', path: path, params: params }, done); }; +before(function (done) { + require('./common/globalBefore').waitUntilBlockchainReady(done); +}); + // Exports module.exports = node; diff --git a/test/unit/modules/peers.js b/test/unit/modules/peers.js new file mode 100644 index 00000000000..4bcfabe6486 --- /dev/null +++ b/test/unit/modules/peers.js @@ -0,0 +1,61 @@ +'use strict'; /*jslint mocha:true, expr:true */ + +var chai = require('chai'); +var express = require('express'); +var sinon = require('sinon'); +var node = require('../../node.js'); + +var clearDatabaseTable = require('../../common/globalBefore').clearDatabaseTable; +var modulesLoader = require('../../common/initModule').modulesLoader; +var Peer = require('../../../logic/peer'); +var Peers = require('../../../modules/peers'); +var PeerSweeper = require('../../../logic/peerSweeper'); + +var randomPeer = { + 'broadhash': '198f2b61a8eb95fbeed58b8216780b68f697f26b849acf00c8c93bb9b24f783d', + 'dappid': null, + 'height': 1, + 'ip': '40.40.40.40', + 'os': 'unknown', + 'port': 4000, + 'state': 2, + 'version': '0.1.2' +}; + +describe('peers', function () { + + var peers; + + before(function (done) { + modulesLoader.initWithDb(Peers, function (err, peersModule) { + if (err) { + return done(err); + } + peers = peersModule; + done(); + }); + }); + + describe('update', function () { + + before(function (done) { + sinon.stub(PeerSweeper.prototype, 'push').returns(true); + done(); + }); + + it('should call PeerSweeper push with proper parameters', function (done) { + node.expect(peers.update(randomPeer)).to.be.ok; + sinon.assert.calledWith(PeerSweeper.prototype.push, 'upsert', new Peer(randomPeer).object()); + done(); + }); + }); + + after(function (done) { + modulesLoader.getDbConnection(function (err, db) { + if (err) { + return done(err); + } + clearDatabaseTable(db, modulesLoader.logger, 'peers', done); + }); + }); +});