├── logs └── .gitkeep ├── pids └── .gitkeep ├── tasks ├── .gitkeep ├── .gitignore └── newMigration.js ├── sql ├── migrations │ ├── .gitkeep │ ├── 20180814120000_add_unique_constraint_to_votes.sql │ ├── 20180814130000_add_unique_constraint_to_delegates.sql │ ├── 20160908215531_protectMemAccountsColumns.sql │ ├── 20160724114255_createMemoryTables.sql │ ├── 20171123154400_addIndexOnVote.sql │ ├── 20160723182901_createViews.sql │ └── 20160723182900_createSchema.sql ├── runtime.sql ├── nodeManager.js ├── multisignatures.js ├── transport.js ├── delegates.js ├── loader.js ├── peers.js ├── transactions.js ├── rounds.js └── blocks.js ├── .jshintignore ├── test ├── mocha.opts ├── genesisPassphrase.testnet.json ├── genesisPassphrase.json ├── api │ ├── loader.js │ ├── peer.js │ ├── peer.signatures.js │ ├── peers.js │ ├── peer.transactions.stress.js │ ├── peer.transactions.signatures.js │ ├── signatures.js │ ├── peer.transactions.delegates.js │ └── accounts.js ├── index.js ├── unit │ └── helpers │ │ └── request-limiter.js └── config.json ├── helpers ├── milestoneBlocks.js ├── validator │ ├── package.json │ ├── utils.js │ └── field.js ├── json-schema │ ├── package.json │ └── field.js ├── transactionTypes.js ├── exceptions.js ├── z_schema-express.js ├── crypto.js ├── diff.js ├── router.js ├── inserts.js ├── request-limiter.js ├── constants.js ├── checkIpInList.js ├── sequence.js ├── slots.js ├── orderBy.js ├── z_schema.js └── database.js ├── docs ├── public │ ├── images │ │ └── gray.png │ └── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── fleurons.eot │ │ ├── fleurons.ttf │ │ ├── fleurons.woff │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── roboto-black.eot │ │ ├── roboto-black.ttf │ │ └── roboto-black.woff └── template │ └── arkio.jst ├── .gitmodules ├── schema ├── nodeManager.js ├── api.public.js ├── signatures.js ├── blocks.js ├── loader.js ├── multisignatures.js ├── transport.js ├── accounts.js ├── api.peer.js ├── peers.js ├── delegates.js └── transactions.js ├── .gitignore ├── .jshintrc ├── optional └── example.js ├── networks.json ├── modules ├── system.js └── signatures.js ├── config.json ├── config.devnet.json ├── config.main.json ├── logger.js ├── package.json ├── logic ├── blockReward.js ├── transfer.js ├── signature.js ├── vote.js └── delegate.js ├── config.mainnet.json ├── Gruntfile.js ├── views └── example.pug ├── config.testnet.json ├── Vagrantfile └── README.md /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pids/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sql/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | helpers/bignum.js 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 250s 2 | -------------------------------------------------------------------------------- /tasks/.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | accounts.js 3 | -------------------------------------------------------------------------------- /helpers/milestoneBlocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | }; 5 | -------------------------------------------------------------------------------- /docs/public/images/gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/images/gray.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/ark-js"] 2 | path = test/ark-js 3 | url = https://github.com/arkecosystem/ark-js.git 4 | -------------------------------------------------------------------------------- /helpers/validator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validator", 3 | "version": "0.0.1", 4 | "main":"validator.js" 5 | } 6 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/fleurons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/fleurons.eot -------------------------------------------------------------------------------- /docs/public/fonts/fleurons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/fleurons.ttf -------------------------------------------------------------------------------- /docs/public/fonts/fleurons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/fleurons.woff -------------------------------------------------------------------------------- /helpers/json-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema", 3 | "version": "0.0.1", 4 | "main":"validator.js" 5 | } 6 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/roboto-black.eot -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/roboto-black.ttf -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArkEcosystemArchive/ark-node/HEAD/docs/public/fonts/roboto-black.woff -------------------------------------------------------------------------------- /sql/runtime.sql: -------------------------------------------------------------------------------- 1 | /* Ark Runtime 2 | * 3 | */ 4 | 5 | BEGIN; 6 | 7 | UPDATE "peers" SET "state" = 1, "clock" = NULL WHERE "state" != 0; 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /helpers/transactionTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | SEND: 0, 5 | SIGNATURE: 1, 6 | DELEGATE: 2, 7 | VOTE: 3, 8 | MULTI: 4, 9 | IPFS: 5 10 | }; 11 | -------------------------------------------------------------------------------- /sql/migrations/20180814120000_add_unique_constraint_to_votes.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE UNIQUE INDEX IF NOT EXISTS "votes_uniq" ON votes ("votes", "transactionId"); 4 | 5 | COMMIT; 6 | -------------------------------------------------------------------------------- /sql/nodeManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var NodeManagerSql = { 4 | getTransactionId: 'SELECT "id" FROM transactions WHERE "id" = ${id}' 5 | }; 6 | 7 | module.exports = NodeManagerSql; 8 | -------------------------------------------------------------------------------- /schema/nodeManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | transactions: { 5 | id: 'nodeManager.transactions', 6 | type: 'array', 7 | uniqueItems: true, 8 | required: ['transactions'] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /sql/multisignatures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MultisignaturesSql = { 4 | getAccounts: 'SELECT ARRAY_AGG("accountId") AS "accountId" FROM mem_accounts2multisignatures WHERE "dependentId" = ${publicKey}' 5 | }; 6 | 7 | module.exports = MultisignaturesSql; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .ed25519-node 3 | .idea/ 4 | .project 5 | __MACOSX/ 6 | logs/*.log 7 | node_modules 8 | nodejs/ 9 | npm-debug.log 10 | release 11 | ssl/ 12 | stacktrace* 13 | tmp 14 | .tags 15 | config.main.json 16 | genesisBlock.main.json 17 | .vagrant 18 | private/ 19 | -------------------------------------------------------------------------------- /sql/migrations/20180814130000_add_unique_constraint_to_delegates.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE UNIQUE INDEX IF NOT EXISTS "delegates_username_uniq" ON delegates ("username"); 4 | CREATE UNIQUE INDEX IF NOT EXISTS "delegates_transactionId_uniq" ON delegates ("transactionId"); 5 | 6 | COMMIT; 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqnull": true, 4 | "eqeqeq": true, 5 | "undef": true, 6 | "newcap" : false, 7 | "node": true, 8 | "quotmark": "single", 9 | "strict" : true, 10 | "globals": { 11 | "gc": true, 12 | "RequestSanitizer": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/genesisPassphrase.testnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "passphrase": "aunt mesh almost hedgehog boss six joke harsh alert lunch gasp city", 3 | "balance": 12500000000000000, 4 | "publicKey": "028f984271e401d5846addf179ff0a9ec32d1f9da6703bca3b0473a67f7b98c5ec", 5 | "address": "aAgSd8PZYjLB64x1cg2WiiJD5QVT1KZkQy" 6 | } -------------------------------------------------------------------------------- /test/genesisPassphrase.json: -------------------------------------------------------------------------------- 1 | { 2 | "passphrase": "peace vanish bleak box tuna woman rally manage undo royal lucky since", 3 | "balance": 12500000000000000, 4 | "publicKey": "03f0726b59f56ac009a8bd3f9623f681cdd5318dc4f5042b4938716c46b1b05e93", 5 | "address": "AYv4BfHZc5RRkCT6xz3iCw9BbTcK2Xo57m", 6 | "wif": "SAH57bsiNcG4Cw3F1GdnqgT5Et6iqM5uwASDWXUteZ8VkhghkNUp" 7 | } -------------------------------------------------------------------------------- /schema/api.public.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 'GET:/api/blocks/getHeight':{ 5 | id: 'GET:/api/blocks/getHeight', 6 | type: 'object', 7 | properties: { 8 | success: { 9 | type: 'boolean' 10 | }, 11 | height: { 12 | type: 'integer', 13 | minimum: 0 14 | } 15 | }, 16 | required: ['success','height'] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /helpers/json-schema/field.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | 5 | module.exports = JsonSchemaField; 6 | 7 | var Field = require('../validator').prototype.Field; 8 | 9 | function JsonSchemaField (validator, path, value, rule, thisArg) { 10 | Field.call(this, validator, path, value, rule, thisArg); 11 | } 12 | 13 | util.inherits(JsonSchemaField, Field); 14 | -------------------------------------------------------------------------------- /test/api/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var node = require('./../node.js'); 4 | 5 | describe('GET /api/loader/status/ping', function () { 6 | 7 | it('should be ok', function (done) { 8 | node.get('/api/loader/status/ping', function (err, res) { 9 | node.expect(res.body).to.have.property('success').to.be.ok; 10 | done(); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /helpers/exceptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | senderPublicKey: [ 5 | ], 6 | signatures: [ 7 | ], 8 | votes: [ 9 | ], 10 | balance:[ 11 | "608c7aeba0895da4517496590896eb325a0b5d367e1b186b1c07d7651a568b9e" 12 | ], 13 | blocks: [ 14 | "2220204643144799909", 15 | "12932134768278785054", 16 | "5100737560399219173", 17 | "9919055269822630432" 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /helpers/z_schema-express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (z_schema) { 4 | return function (req, res, next) { 5 | req.sanitize = sanitize; 6 | 7 | function sanitize (value, schema, callback) { 8 | return z_schema.validate(value, schema, function (err, valid) { 9 | return callback(null, { 10 | isValid: valid, 11 | issues: err ? err[0].message + ': ' + err[0].path : null 12 | }, value); 13 | }); 14 | } 15 | 16 | next(); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /schema/signatures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | addSignature: { 5 | id: 'signatures.addSignature', 6 | type: 'object', 7 | properties: { 8 | secret: { 9 | type: 'string', 10 | minLength: 1 11 | }, 12 | secondSecret: { 13 | type: 'string', 14 | minLength: 1 15 | }, 16 | publicKey: { 17 | type: 'string', 18 | format: 'publicKey' 19 | }, 20 | multisigAccountPublicKey: { 21 | type: 'string', 22 | format: 'publicKey' 23 | } 24 | }, 25 | required: ['secret', 'secondSecret'] 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./api/accounts.js'); 2 | require('./api/blocks.js'); 3 | require('./api/delegates.js'); 4 | require('./api/loader.js'); 5 | require('./api/multisignatures.js'); 6 | require('./api/peer.js'); 7 | require('./api/peer.transactions.delegates.js'); 8 | require('./api/peer.transactions.js'); 9 | require('./api/peer.transactions.signatures.js'); 10 | require('./api/peer.transactions.votes.js'); 11 | require('./api/peers.js'); 12 | require('./api/signatures.js'); 13 | require('./api/transactions.js'); 14 | 15 | require('./unit/helpers/request-limiter.js'); 16 | require('./unit/logic/blockReward.js'); 17 | -------------------------------------------------------------------------------- /helpers/crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var arkjs = require('arkjs'); 4 | 5 | function Crypto(scope){ 6 | this.scope = scope; 7 | this.network = scope.config.network; 8 | } 9 | 10 | Crypto.prototype.makeKeypair = function (seed) { 11 | return arkjs.crypto.getKeys(seed, this.network); 12 | }; 13 | 14 | Crypto.prototype.sign = function (hash, keypair) { 15 | return keypair.sign(hash).toDER().toString("hex"); 16 | }; 17 | 18 | Crypto.prototype.verify = function (hash, signatureBuffer, publicKeyBuffer) { 19 | try { 20 | var ecsignature = arkjs.ECSignature.fromDER(signatureBuffer); 21 | var ecpair = arkjs.ECPair.fromPublicKeyBuffer(publicKeyBuffer, this.network); 22 | return ecpair.verify(hash, ecsignature); 23 | } catch (error){ 24 | return false; 25 | } 26 | }; 27 | 28 | module.exports = Crypto; 29 | -------------------------------------------------------------------------------- /tasks/newMigration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var moment = require('moment'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | module.exports = function (grunt) { 8 | grunt.registerTask('newMigration', 'Create a new migration file.', function (name) { 9 | var migration = { 10 | id: moment().format('YYYYMMDDHHmmss'), 11 | name: String(name) 12 | }; 13 | 14 | if (!migration.name.match(/^[a-z]+$/i)) { 15 | grunt.fail.fatal('Invalid migration name'); 16 | } 17 | 18 | migration.filename = ( 19 | migration.id + '_' + migration.name + '.sql' 20 | ); 21 | 22 | grunt.log.write('Creating migration file: ' + migration.filename); 23 | 24 | fs.writeFile(path.join('sql', 'migrations', migration.filename), '', function (err) { 25 | if (err) { grunt.fail.fatal(err); } 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /helpers/validator/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | 5 | exports.extend = extend; 6 | exports.copy = copy; 7 | exports.inherits = util.inherits; 8 | 9 | function extend (target, source) { 10 | if (!target || typeof target !== 'object') { return target; } 11 | 12 | Array.prototype.slice.call(arguments).forEach(function(source){ 13 | if (!source || typeof source !== 'object') { return; } 14 | 15 | util._extend(target, source); 16 | }); 17 | 18 | return target; 19 | } 20 | 21 | function copy (target) { 22 | if (!target || typeof target !== 'object') { return target; } 23 | 24 | if (Array.isArray(target)) { 25 | return target.map(copy); 26 | } else if (target.constructor === Object) { 27 | var result = {}; 28 | Object.getOwnPropertyNames(target).forEach(function(name){ 29 | result[name] = copy(target[name]); 30 | }); 31 | return result; 32 | } else { 33 | return target; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /helpers/diff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | reverse: function (diff) { 5 | var copyDiff = diff.slice(); 6 | for (var i = 0; i < copyDiff.length; i++) { 7 | var math = copyDiff[i][0] === '-' ? '+' : '-'; 8 | copyDiff[i] = math + copyDiff[i].slice(1); 9 | } 10 | return copyDiff; 11 | }, 12 | 13 | merge: function (source, diff) { 14 | var res = source ? source.slice() : []; 15 | var index; 16 | 17 | for (var i = 0; i < diff.length; i++) { 18 | var math = diff[i][0]; 19 | var publicKey = diff[i].slice(1); 20 | 21 | if (math === '+') { 22 | res = res || []; 23 | 24 | index = -1; 25 | if (res) { 26 | index = res.indexOf(publicKey); 27 | } 28 | if (index !== -1) { 29 | return false; 30 | } 31 | 32 | res.push(publicKey); 33 | } 34 | if (math === '-') { 35 | index = -1; 36 | if (res) { 37 | index = res.indexOf(publicKey); 38 | } 39 | if (index === -1) { 40 | return false; 41 | } 42 | res.splice(index, 1); 43 | if (!res.length) { 44 | res = null; 45 | } 46 | } 47 | } 48 | return res; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /sql/transport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var TransportSql = { 4 | getCommonBlock: 'SELECT MAX("height") AS "height", "id", "previousBlock", "timestamp" FROM blocks WHERE "id" IN ($1:csv) GROUP BY "id" ORDER BY "height" DESC', 5 | getBlockTransactions: 'SELECT rawtxs::json from blocks WHERE id = ${block_id}', 6 | blockList: 'SELECT id, version, height, timestamp, "previousBlock", "numberOfTransactions" ,"totalAmount", "totalFee", reward, "payloadLength", encode("payloadHash", \'hex\') as "payloadHash", encode("generatorPublicKey", \'hex\') as "generatorPublicKey", encode("blockSignature", \'hex\') as "blockSignature", rawtxs::json as transactions from blocks WHERE "height" > ${lastBlockHeight} ORDER BY "height" ASC LIMIT ${limit}', 7 | block: 'SELECT id, version, height, timestamp, "previousBlock", "numberOfTransactions" ,"totalAmount", "totalFee", reward, "payloadLength", encode("payloadHash", \'hex\') as "payloadHash", encode("generatorPublicKey", \'hex\') as "generatorPublicKey", encode("blockSignature", \'hex\') as "blockSignature", rawtxs::json as transactions from blocks WHERE id = ${id}', 8 | getTransactionId: 'SELECT "id" FROM transactions WHERE "id" = ${id}' 9 | }; 10 | 11 | module.exports = TransportSql; 12 | -------------------------------------------------------------------------------- /helpers/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var extend = require('extend'); 4 | 5 | function Map (root, config) { 6 | var router = this; 7 | 8 | Object.keys(config).forEach(function (params) { 9 | var route = params.split(' '); 10 | if (route.length !== 2 || ['post', 'get', 'put'].indexOf(route[0]) === -1) { 11 | throw Error('Invalid map config'); 12 | } 13 | router[route[0]](route[1], function (req, res, next) { 14 | root[config[params]]({'body': route[0] === 'get' ? req.query : req.body}, function (err, response) { 15 | if (err) { 16 | res.json({'success': false, 'error': err}); 17 | } else { 18 | return res.json(extend({}, {'success': true}, response)); 19 | } 20 | }); 21 | }); 22 | }); 23 | } 24 | 25 | /** 26 | * @title Router 27 | * @overview Router stub 28 | * @returns {*} 29 | */ 30 | var Router = function () { 31 | var router = require('express').Router(); 32 | 33 | router.use(function (req, res, next) { 34 | res.header('Access-Control-Allow-Origin', '*'); 35 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 36 | next(); 37 | }); 38 | 39 | router.map = Map; 40 | 41 | return router; 42 | }; 43 | 44 | module.exports = Router; 45 | -------------------------------------------------------------------------------- /optional/example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Router = require('../helpers/router.js'); 4 | 5 | // Private fields 6 | var modules, library, self, __private = {}; 7 | 8 | // Constructor 9 | function Server (cb, scope) { 10 | library = scope; 11 | self = this; 12 | 13 | return cb(null, self); 14 | } 15 | 16 | // Private methods 17 | __private.attachApi = function () { 18 | var router = new Router(); 19 | 20 | router.get('/', function (req, res) { 21 | res.render('./example.pug', {nethash: library.config.nethash}); 22 | }); 23 | 24 | router.get('/getStats', function (req, res) { 25 | res.status(200).send({ 26 | lastBlock: modules.blockchain.getLastBlock(), 27 | transactionPool: modules.transactionPool.getMempoolSize() 28 | }); 29 | }); 30 | 31 | library.network.app.engine('pug', require('pug').__express); 32 | 33 | library.network.app.use('/app', router); 34 | }; 35 | 36 | // 37 | Server.prototype.onBind = function (scope) { 38 | modules = scope; 39 | }; 40 | 41 | // 42 | Server.prototype.onAttachPublicApi = function () { 43 | __private.attachApi(); 44 | }; 45 | 46 | 47 | // 48 | Server.prototype.cleanup = function (cb) { 49 | return cb(); 50 | }; 51 | 52 | // Shared 53 | 54 | // Export 55 | module.exports = Server; 56 | -------------------------------------------------------------------------------- /helpers/inserts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pgp = require('pg-promise'); 4 | 5 | function Inserts (record, values, concat) { 6 | if (!(this instanceof Inserts)) { 7 | return new Inserts(record, values, concat); 8 | } 9 | 10 | var self = this; 11 | 12 | if (!record || !record.table || !record.values) { 13 | throw 'Inserts: Invalid record argument'; 14 | } 15 | 16 | if (!values) { 17 | throw 'Inserts: Invalid values argument'; 18 | } 19 | 20 | this.namedTemplate = function () { 21 | return record.fields.map(function (field, index) { 22 | return '${' + field + '}'; 23 | }).join(','); 24 | }; 25 | 26 | this._template = this.namedTemplate(); 27 | 28 | this.template = function () { 29 | var values; 30 | var fields = record.fields.map(pgp.as.name).join(','); 31 | if (concat) { 32 | values = '$1'; 33 | } else { 34 | values = '(' + this.namedTemplate() + ')'; 35 | } 36 | return pgp.as.format('INSERT INTO $1~($2^) VALUES $3^', [record.table, fields, values]); 37 | }; 38 | 39 | this._rawDBType = true; 40 | 41 | this.formatDBType = function () { 42 | return values.map(function (v) { 43 | return '(' + pgp.as.format(self._template, v) + ')'; 44 | }).join(','); 45 | }; 46 | } 47 | 48 | module.exports = Inserts; 49 | -------------------------------------------------------------------------------- /schema/blocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../helpers/constants.js'); 4 | 5 | module.exports = { 6 | loadBlocksFromPeer: { 7 | id: 'blocks.loadBlocksFromPeer', 8 | type: 'array' 9 | }, 10 | getBlock: { 11 | id: 'blocks.getBlock', 12 | type: 'object', 13 | properties: { 14 | id: { 15 | type: 'string', 16 | minLength: 1 17 | } 18 | }, 19 | required: ['id'] 20 | }, 21 | getBlocks: { 22 | id: 'blocks.getBlocks', 23 | type: 'object', 24 | properties: { 25 | limit: { 26 | type: 'integer', 27 | minimum: 0, 28 | maximum: 100 29 | }, 30 | orderBy: { 31 | type: 'string' 32 | }, 33 | offset: { 34 | type: 'integer', 35 | minimum: 0 36 | }, 37 | generatorPublicKey: { 38 | type: 'string', 39 | format: 'publicKey' 40 | }, 41 | totalAmount: { 42 | type: 'integer', 43 | minimum: 0, 44 | maximum: constants.totalAmount 45 | }, 46 | totalFee: { 47 | type: 'integer', 48 | minimum: 0, 49 | maximum: constants.totalAmount 50 | }, 51 | reward: { 52 | type: 'integer', 53 | minimum: 0 54 | }, 55 | previousBlock: { 56 | type: 'string' 57 | }, 58 | height: { 59 | type: 'integer' 60 | } 61 | } 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /sql/delegates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pgp = require('pg-promise'); 4 | 5 | var DelegatesSql = { 6 | sortFields: [ 7 | 'username', 8 | 'address', 9 | 'publicKey', 10 | 'vote', 11 | 'missedblocks', 12 | 'producedblocks' 13 | ], 14 | 15 | count: 'SELECT COUNT(*)::int FROM delegates', 16 | 17 | search: function (params) { 18 | var sql = [ 19 | 'SELECT m."username", m."address", ENCODE(m."publicKey", \'hex\') AS "publicKey", m."vote", m."producedblocks", m."missedblocks"', 20 | 'FROM mem_accounts m', 21 | 'WHERE m."isDelegate" = 1 AND m."username" LIKE ${q}', 22 | 'ORDER BY ' + [params.sortField, params.sortMethod].join(' '), 23 | 'LIMIT ${limit}' 24 | ].join(' '); 25 | 26 | params.q = '%' + String(params.q).toLowerCase() + '%'; 27 | return pgp.as.format(sql, params); 28 | }, 29 | 30 | insertFork: 'INSERT INTO forks_stat ("delegatePublicKey", "blockTimestamp", "blockId", "blockHeight", "previousBlock", "cause") VALUES (${delegatePublicKey}, ${blockTimestamp}, ${blockId}, ${blockHeight}, ${previousBlock}, ${cause});', 31 | 32 | getVoters: 'SELECT ARRAY_AGG("accountId") AS "accountIds" FROM mem_accounts2delegates WHERE "dependentId" = ${publicKey}' 33 | }; 34 | 35 | module.exports = DelegatesSql; 36 | -------------------------------------------------------------------------------- /sql/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LoaderSql = { 4 | countBlocks: 'SELECT COUNT("rowId")::int FROM blocks', 5 | 6 | countMemAccounts: 'SELECT COUNT(*)::int FROM mem_accounts WHERE "blockId" = (SELECT "id" FROM "blocks" ORDER BY "height" DESC LIMIT 1)', 7 | 8 | resetMemAccounts: [ 9 | 'UPDATE mem_accounts SET "u_isDelegate" = "isDelegate", "u_secondSignature" = "secondSignature", "u_username" = "username", "u_balance" = "balance", "u_delegates" = "delegates", "u_multisignatures" = "multisignatures", "u_multimin" = "multimin", "u_multilifetime" = "multilifetime";', 10 | 'DELETE FROM mem_accounts2u_delegates; INSERT INTO mem_accounts2u_delegates SELECT * FROM mem_accounts2delegates;', 11 | 'DELETE FROM mem_accounts2u_multisignatures; INSERT INTO mem_accounts2u_multisignatures SELECT * FROM mem_accounts2multisignatures;', 12 | ].join(''), 13 | 14 | getOrphanedMemAccounts: 'SELECT a."blockId", b."id" FROM mem_accounts a LEFT OUTER JOIN blocks b ON b."id" = a."blockId" WHERE a."blockId" IS NOT NULL AND a."blockId" != \'0\' AND b."id" IS NULL', 15 | 16 | getDelegates: 'SELECT ENCODE("publicKey", \'hex\') FROM mem_accounts WHERE "isDelegate" = 1', 17 | 18 | getTransactionId: 'SELECT "id" FROM transactions WHERE "id" = ${id}' 19 | }; 20 | 21 | module.exports = LoaderSql; 22 | -------------------------------------------------------------------------------- /helpers/request-limiter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RateLimit = require('express-rate-limit'); 4 | 5 | var defaults = { 6 | max: 0, // Disabled 7 | delayMs: 0, // Disabled 8 | delayAfter: 0, // Disabled 9 | windowMs: 60000 // 1 minute window 10 | }; 11 | 12 | function applyLimits (limits) { 13 | if (typeof limits === 'object') { 14 | return { 15 | max: Math.floor(limits.max) || defaults.max, 16 | delayMs: Math.floor(limits.delayMs) || defaults.delayMs, 17 | delayAfter: Math.floor(limits.delayAfter) || defaults.delayAfter, 18 | windowMs: Math.floor(limits.windowMs) || defaults.windowMs 19 | }; 20 | } else { 21 | return defaults; 22 | } 23 | } 24 | 25 | module.exports = function (app, config) { 26 | if (config.trustProxy) { 27 | app.enable('trust proxy'); 28 | } 29 | 30 | config.api = config.api || {}; 31 | config.api.options = config.api.options || {}; 32 | 33 | config.peers = config.peers || {}; 34 | config.peers.options = config.peers.options || {}; 35 | 36 | var limits = { 37 | client: applyLimits(config.api.options.limits), 38 | peer: applyLimits(config.peers.options.limits) 39 | }; 40 | 41 | limits.middleware = { 42 | client: app.use('/api/', new RateLimit(limits.client)), 43 | peer: app.use('/peer/', new RateLimit(limits.peer)) 44 | }; 45 | 46 | return limits; 47 | }; 48 | -------------------------------------------------------------------------------- /networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "ark": { 3 | "messagePrefix": "ARK message:\n", 4 | "bip32": { 5 | "public": 46090600, 6 | "private": 46089520 7 | }, 8 | "pubKeyHash": 23, 9 | "wif": 170, 10 | "client": { 11 | "token":"ARK", 12 | "symbol":"Ѧ", 13 | "explorer":"https://explorer.ark.io" 14 | } 15 | }, 16 | "bitcoin": { 17 | "messagePrefix": "Bitcoin message:\n", 18 | "bip32": { 19 | "public": 46090600, 20 | "private": 46089520 21 | }, 22 | "pubKeyHash": 1, 23 | "wif": 1, 24 | "client": { 25 | "token":"Bitcoin", 26 | "symbol":"B" 27 | } 28 | }, 29 | "testnet": { 30 | "messagePrefix": "TESTNET message:\n", 31 | "bip32": { 32 | "public": 70617039, 33 | "private": 70615956 34 | }, 35 | "pubKeyHash": 82, 36 | "wif": 186, 37 | "client": { 38 | "token":"TARK", 39 | "symbol":"TѦ", 40 | "explorer":"http://texplorer.ark.io" 41 | } 42 | }, 43 | "devnet": { 44 | "messagePrefix": "DEVNET message:\n", 45 | "bip32": { 46 | "public": 70617039, 47 | "private": 70615956 48 | }, 49 | "pubKeyHash": 30, 50 | "wif": 187, 51 | "client": { 52 | "token":"DARK", 53 | "symbol":"DѦ", 54 | "explorer":"http://dexplorer.ark.io" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /helpers/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | activeDelegates: 51, 5 | maximumVotes: 1, 6 | addressLength: 208, 7 | blockHeaderLength: 248, 8 | confirmationLength: 77, 9 | epochTime: new Date(Date.UTC(2017, 2, 21, 13, 0, 0, 0)), 10 | fees:{ 11 | send: 10000000, 12 | vote: 100000000, 13 | secondsignature: 500000000, 14 | delegate: 2500000000, 15 | multisignature: 500000000 16 | }, 17 | feeStart: 1, 18 | feeStartVolume: 10000 * 100000000, 19 | fixedPoint : Math.pow(10, 8), 20 | forgingTimeOut: 3060, // 102 blocks / 2 rounds 21 | maxAddressesLength: 208 * 128, 22 | maxAmount: 100000000, 23 | maxClientConnections: 100, 24 | maxConfirmations : 77 * 100, 25 | maxPayloadLength: 2 * 1024 * 1024, 26 | maxRequests: 10000 * 12, 27 | maxSignaturesLength: 196 * 256, 28 | maxTxsPerBlock: 50, 29 | blocktime: 8, 30 | numberLength: 100000000, 31 | requestLength: 104, 32 | rewards: { 33 | milestones: [ 34 | 200000000, // Initial Reward 35 | 200000000, // Milestone 1 36 | 200000000, // Milestone 2 37 | 200000000, // Milestone 3 38 | 200000000 // Milestone 4 39 | ], 40 | offset: 75600, // Start rewards at block, ie 7 days after net start 41 | distance: 3000000, // Distance between each milestone 42 | }, 43 | signatureLength: 196, 44 | totalAmount: 12500000000000004, // TODO: Fix properly because this value exceeds JS Number precision 45 | unconfirmedTransactionTimeOut: 10800 // 1080 blocks 46 | }; 47 | -------------------------------------------------------------------------------- /helpers/checkIpInList.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var ip = require('ip'); 5 | 6 | /* 7 | Checks if ip address is in list (e.g. whitelist, blacklist). 8 | 9 | @param list an array of ip addresses or ip subnets 10 | @param addr the ip address to check if in array 11 | @param returnListIsEmpty the return value, if list is empty (default: true) 12 | @returns true if ip is in the list, false otherwise 13 | */ 14 | function CheckIpInList (list, addr, returnListIsEmpty) { 15 | var i, n; 16 | 17 | if (!_.isBoolean(returnListIsEmpty)) { 18 | returnListIsEmpty = true; 19 | } 20 | 21 | if (!_.isArray(list) || list.length === 0) { 22 | return returnListIsEmpty; 23 | } 24 | 25 | if (!list._subNets) { // First call, create subnet list 26 | list._subNets = []; 27 | for (i = list.length - 1; i >= 0; i--) { 28 | var entry = list[i]; 29 | if (ip.isV4Format(entry)) { // IPv4 host entry 30 | entry = entry + '/32'; 31 | } else if (ip.isV6Format(entry)) { // IPv6 host entry 32 | entry = entry + '/128'; 33 | } 34 | try { 35 | var subnet = ip.cidrSubnet(entry); 36 | list._subNets.push(subnet); 37 | } catch (err) { 38 | console.error('CheckIpInList:', err.toString()); 39 | } 40 | } 41 | } 42 | 43 | if (list._subNets.length === 0) { 44 | return returnListIsEmpty; 45 | } 46 | 47 | // Check subnets 48 | for (i = 0, n = list._subNets.length; i < n; i++) { 49 | if (list._subNets[i].contains(addr)) { 50 | return true; 51 | } 52 | } 53 | 54 | // IP address not found 55 | return false; 56 | } 57 | 58 | module.exports = CheckIpInList; 59 | -------------------------------------------------------------------------------- /schema/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | loadSignatures: { 5 | id: 'loader.loadSignatures', 6 | type: 'object', 7 | properties: { 8 | signatures: { 9 | type: 'array', 10 | uniqueItems: true 11 | } 12 | }, 13 | required: ['signatures'] 14 | }, 15 | loadUnconfirmedTransactions: { 16 | id: 'loader.loadUnconfirmedTransactions', 17 | type: 'object', 18 | properties: { 19 | transactions: { 20 | type: 'array', 21 | uniqueItems: true 22 | } 23 | }, 24 | required: ['transactions'] 25 | }, 26 | getNetwork: { 27 | peers: { 28 | id: 'loader.getNetwork.peers', 29 | type: 'object', 30 | properties: { 31 | peers: { 32 | type: 'array', 33 | uniqueItems: true 34 | } 35 | }, 36 | required: ['peers'] 37 | }, 38 | peer: { 39 | id: 'loader.getNetwork.peer', 40 | type: 'object', 41 | properties: { 42 | ip: { 43 | type: 'string', 44 | format: 'ip' 45 | }, 46 | port: { 47 | type: 'integer', 48 | minimum: 1, 49 | maximum: 65535 50 | }, 51 | state: { 52 | type: 'integer', 53 | minimum: 0, 54 | maximum: 3 55 | }, 56 | os: { 57 | type: 'string' 58 | }, 59 | version: { 60 | type: 'string' 61 | } 62 | }, 63 | required: ['ip', 'port'] 64 | }, 65 | height: { 66 | id: 'loader.getNetwork.height', 67 | type: 'object', 68 | properties: { 69 | height: { 70 | type: 'integer', 71 | minimum: 0 72 | }, 73 | id: { 74 | type: 'string', 75 | minLength: 1 76 | } 77 | }, 78 | required: ['height'] 79 | } 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /modules/system.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var os = require('os'); 4 | 5 | // Private fields 6 | var modules, library, self, __private = {}, shared = {}; 7 | 8 | // Constructor 9 | function System (cb, scope) { 10 | library = scope; 11 | self = this; 12 | 13 | __private.version = library.config.version; 14 | __private.port = library.config.port; 15 | __private.nethash = library.config.nethash; 16 | __private.osName = os.platform() + os.release(); 17 | 18 | return cb(null, self); 19 | } 20 | 21 | // Private methods 22 | 23 | // Public methods 24 | // 25 | //__API__ `getOS` 26 | 27 | // 28 | System.prototype.getOS = function () { 29 | return __private.osName; 30 | }; 31 | 32 | // 33 | //__API__ `getVersion` 34 | 35 | // 36 | System.prototype.getVersion = function () { 37 | return __private.version; 38 | }; 39 | 40 | // 41 | //__API__ `getPort` 42 | 43 | // 44 | System.prototype.getPort = function () { 45 | return __private.port; 46 | }; 47 | 48 | // 49 | //__API__ `getNethash` 50 | 51 | // 52 | System.prototype.getNethash = function () { 53 | return __private.nethash; 54 | }; 55 | 56 | // Events 57 | // 58 | //__EVENT__ `onBind` 59 | 60 | // 61 | System.prototype.onBind = function (scope) { 62 | modules = scope; 63 | }; 64 | 65 | // 66 | //__API__ `isMyself` 67 | 68 | // 69 | System.prototype.isMyself = function (peer) { 70 | var interfaces = os.networkInterfaces(); 71 | return Object.keys(interfaces).some(function(family){ 72 | return interfaces[family].some(function(nic){ 73 | return nic.address == peer.ip && peer.port == __private.port; 74 | }); 75 | }); 76 | } 77 | 78 | // Shared 79 | 80 | // Export 81 | module.exports = System; 82 | -------------------------------------------------------------------------------- /helpers/sequence.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var extend = require('extend'); 4 | var util = require('util'); 5 | 6 | function Sequence (config) { 7 | var _default = { 8 | onWarning: null, 9 | warningLimit: 50 10 | }; 11 | _default = extend(_default, config); 12 | var self = this; 13 | this.sequence = []; 14 | 15 | setImmediate(function nextSequenceTick () { 16 | if (_default.onWarning && self.sequence.length >= _default.warningLimit) { 17 | _default.onWarning(self.sequence.length, _default.warningLimit); 18 | } 19 | self.__tick(function() { 20 | setTimeout(nextSequenceTick, 0); 21 | }); 22 | }); 23 | } 24 | 25 | // 26 | //__API__ `__tick` 27 | 28 | // 29 | Sequence.prototype.__tick = function (cb) { 30 | var task = this.sequence.shift(); 31 | if (!task) { 32 | return setImmediate(cb); 33 | } 34 | var args = [function (err, res) { 35 | if (task.done) { 36 | task.done(err, res); 37 | } 38 | setImmediate(cb); 39 | }]; 40 | if (task.args) { 41 | args = args.concat(task.args); 42 | } 43 | task.worker.apply(task.worker, args); 44 | }; 45 | 46 | // 47 | //__API__ `add` 48 | 49 | // 50 | Sequence.prototype.add = function (worker, args, done) { 51 | if (!done && args && typeof(args) === 'function') { 52 | done = args; 53 | args = undefined; 54 | } 55 | if (worker && typeof(worker) === 'function') { 56 | var task = {worker: worker, done: done}; 57 | if (util.isArray(args)) { 58 | task.args = args; 59 | } 60 | this.sequence.push(task); 61 | } 62 | }; 63 | 64 | // 65 | //__API__ `count` 66 | 67 | // 68 | Sequence.prototype.count = function () { 69 | return this.sequence.length; 70 | }; 71 | 72 | module.exports = Sequence; 73 | -------------------------------------------------------------------------------- /schema/multisignatures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | getAccounts: { 5 | id: 'multisignatures.getAccounts', 6 | type: 'object', 7 | properties: { 8 | publicKey: { 9 | type: 'string', 10 | format: 'publicKey' 11 | } 12 | }, 13 | required: ['publicKey'] 14 | }, 15 | pending: { 16 | id: 'multisignatures.pending', 17 | type: 'object', 18 | properties: { 19 | publicKey: { 20 | type: 'string', 21 | format: 'publicKey' 22 | } 23 | }, 24 | required: ['publicKey'] 25 | }, 26 | sign: { 27 | id: 'multisignatures.sign', 28 | type: 'object', 29 | properties: { 30 | secret: { 31 | type: 'string', 32 | minLength: 1, 33 | maxLength: 100 34 | }, 35 | secondSecret: { 36 | type: 'string', 37 | minLength: 1, 38 | maxLength: 100 39 | }, 40 | publicKey: { 41 | type: 'string', 42 | format: 'publicKey' 43 | }, 44 | transactionId: { 45 | type: 'string' 46 | } 47 | }, 48 | required: ['transactionId', 'secret'] 49 | }, 50 | addMultisignature: { 51 | id: 'multisignatures.addMultisignature', 52 | type: 'object', 53 | properties: { 54 | secret: { 55 | type: 'string', 56 | minLength: 1, 57 | maxLength: 100 58 | }, 59 | publicKey: { 60 | type: 'string', 61 | format: 'publicKey' 62 | }, 63 | secondSecret: { 64 | type: 'string', 65 | minLength: 1, 66 | maxLength: 100 67 | }, 68 | min: { 69 | type: 'integer', 70 | minimum: 1, 71 | maximum: 16 72 | }, 73 | lifetime: { 74 | type: 'integer', 75 | minimum: 1, 76 | maximum: 72 77 | }, 78 | keysgroup: { 79 | type: 'array', 80 | minLength: 1, 81 | maxLength: 10 82 | } 83 | }, 84 | required: ['min', 'lifetime', 'keysgroup', 'secret'] 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /schema/transport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | headers: { 5 | id: 'transport.headers', 6 | type: 'object', 7 | properties: { 8 | ip: { 9 | type: 'string', 10 | format: 'ip' 11 | }, 12 | port: { 13 | type: 'integer', 14 | minimum: 1, 15 | maximum: 65535 16 | }, 17 | os: { 18 | type: 'string', 19 | maxLength: 64 20 | }, 21 | nethash: { 22 | type: 'string', 23 | maxLength: 64 24 | }, 25 | version: { 26 | type: 'string', 27 | maxLength: 11 28 | } 29 | }, 30 | required: ['ip', 'port', 'nethash', 'version'] 31 | }, 32 | commonBlock: { 33 | id: 'transport.commonBlock', 34 | type: 'object', 35 | properties: { 36 | ids: { 37 | type: 'string', 38 | format: 'csv' 39 | } 40 | }, 41 | required: ['ids'] 42 | }, 43 | transactionsFromIds: { 44 | id: 'transport.transactionsFromIds', 45 | type: 'object', 46 | properties: { 47 | ids: { 48 | type: 'string', 49 | format: 'csv' 50 | } 51 | }, 52 | required: ['ids'] 53 | }, 54 | blocks: { 55 | id: 'transport.blocks', 56 | type: 'object', 57 | properties: { 58 | lastBlockHeight: { 59 | type: 'integer' 60 | } 61 | }, 62 | }, 63 | block: { 64 | id: 'transport.block', 65 | type: 'object', 66 | properties: { 67 | id: { 68 | type: 'string' 69 | } 70 | }, 71 | }, 72 | signatures: { 73 | id: 'transport.signatures', 74 | type: 'object', 75 | properties: { 76 | signature: { 77 | type: 'object', 78 | properties: { 79 | transaction: { 80 | type: 'string' 81 | }, 82 | signature: { 83 | type: 'string', 84 | format: 'signature' 85 | } 86 | }, 87 | required: ['transaction', 'signature'] 88 | } 89 | }, 90 | required: ['signature'] 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4000, 3 | "address": "0.0.0.0", 4 | "version": "0.2.1", 5 | "fileLogLevel": "info", 6 | "logFileName": "logs/ark.log", 7 | "consoleLogLevel": "debug", 8 | "trustProxy": false, 9 | "db": { 10 | "host": "localhost", 11 | "port": 5432, 12 | "database": "ark_test", 13 | "user": null, 14 | "password": "password", 15 | "poolSize": 20, 16 | "poolIdleTimeout": 30000, 17 | "reapIntervalMillis": 1000, 18 | "logEvents": [ 19 | "error" 20 | ] 21 | }, 22 | "api": { 23 | "access": { 24 | "whiteList": [] 25 | }, 26 | "options": { 27 | "limits": { 28 | "max": 0, 29 | "delayMs": 0, 30 | "delayAfter": 0, 31 | "windowMs": 60000 32 | } 33 | } 34 | }, 35 | "peers": { 36 | "minimumNetworkReach": 1, 37 | "list": [ 38 | { 39 | "ip": "127.0.0.1", 40 | "port": 4000 41 | } 42 | ], 43 | "blackList": [], 44 | "options": { 45 | "limits": { 46 | "max": 0, 47 | "delayMs": 0, 48 | "delayAfter": 0, 49 | "windowMs": 60000 50 | }, 51 | "maxUpdatePeers": 20, 52 | "timeout": 5000 53 | } 54 | }, 55 | "forging": { 56 | "coldstart": 60, 57 | "force": true, 58 | "secret": [], 59 | "access": { 60 | "whiteList": [ 61 | "127.0.0.1" 62 | ] 63 | } 64 | }, 65 | "loading": { 66 | "verifyOnLoading": false, 67 | "loadPerIteration": 5000 68 | }, 69 | "modules": { 70 | "example":"./optional/example.js" 71 | }, 72 | "ssl": { 73 | "enabled": false, 74 | "options": { 75 | "port": 443, 76 | "address": "0.0.0.0", 77 | "key": "./ssl/ark.key", 78 | "cert": "./ssl/ark.crt" 79 | } 80 | }, 81 | "nethash": "31a3d2f549dcb0ce6856b5d440cb7a1402d766f290a89d408f155f937f106f8c" 82 | } 83 | -------------------------------------------------------------------------------- /helpers/slots.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('./constants.js'); 4 | 5 | /** 6 | * Get time from Ark epoch. 7 | * @param {number|undefined} time Time in unix seconds 8 | * @returns {number} 9 | */ 10 | 11 | function beginEpochTime () { 12 | var d = constants.epochTime; 13 | 14 | return d; 15 | } 16 | 17 | function getEpochTime (time) { 18 | if (time === undefined) { 19 | time = (new Date()).getTime(); 20 | } 21 | 22 | var d = beginEpochTime(); 23 | var t = d.getTime(); 24 | 25 | return Math.floor((time - t) / 1000); 26 | } 27 | 28 | module.exports = { 29 | interval: constants.blocktime, 30 | delegates: constants.activeDelegates, 31 | 32 | getTime: function (time) { 33 | return getEpochTime(time); 34 | }, 35 | 36 | getRealTime: function (epochTime) { 37 | if (epochTime === undefined) { 38 | epochTime = this.getTime(); 39 | } 40 | 41 | var d = beginEpochTime(); 42 | var t = Math.floor(d.getTime() / 1000) * 1000; 43 | 44 | return t + epochTime * 1000; 45 | }, 46 | 47 | getSlotNumber: function (epochTime) { 48 | if (epochTime === undefined) { 49 | epochTime = this.getTime(); 50 | } 51 | 52 | return Math.floor(epochTime / this.interval); 53 | }, 54 | 55 | // Forging is allowed only during the first half of blocktime 56 | isForgingAllowed: function (epochTime) { 57 | if (epochTime === undefined) { 58 | epochTime = this.getTime(); 59 | } 60 | 61 | return Math.floor(epochTime / this.interval) == Math.floor((epochTime + this.interval / 2) / this.interval); 62 | }, 63 | 64 | getSlotTime: function (slot) { 65 | return slot * this.interval; 66 | }, 67 | 68 | getNextSlot: function () { 69 | var slot = this.getSlotNumber(); 70 | 71 | return slot + 1; 72 | }, 73 | 74 | getLastSlot: function (nextSlot) { 75 | return nextSlot + this.delegates; 76 | }, 77 | 78 | roundTime: function (date) { 79 | return Math.floor(date.getTime() / 1000) * 1000; 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /helpers/orderBy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function OrderBy (orderBy, options) { 4 | options = (typeof options === 'object') ? options : {}; 5 | options.sortField = options.sortField || null; 6 | options.sortMethod = options.sortMethod || null; 7 | options.sortFields = Array.isArray(options.sortFields) ? options.sortFields : []; 8 | 9 | if (typeof options.quoteField === 'undefined') { 10 | options.quoteField = true; 11 | } else { 12 | options.quoteField = Boolean(options.quoteField); 13 | } 14 | 15 | var sortField, sortMethod; 16 | 17 | if (orderBy) { 18 | var sort = String(orderBy).split(':'); 19 | sortField = sort[0].replace(/[^\w\s]/gi, ''); 20 | 21 | if (sort.length === 2) { 22 | sortMethod = sort[1] === 'desc' ? 'DESC' : 'ASC'; 23 | } 24 | } 25 | 26 | function prefixField (sortField) { 27 | if (!sortField) { 28 | return sortField; 29 | } else if (typeof options.fieldPrefix === 'string') { 30 | return options.fieldPrefix + sortField; 31 | } else if (typeof options.fieldPrefix === 'function') { 32 | return options.fieldPrefix(sortField); 33 | } else { 34 | return sortField; 35 | } 36 | } 37 | 38 | function quoteField (sortField) { 39 | if (sortField && options.quoteField) { 40 | return ('"' + sortField + '"'); 41 | } else { 42 | return sortField; 43 | } 44 | } 45 | 46 | var emptyWhiteList = options.sortFields.length === 0; 47 | 48 | var inWhiteList = options.sortFields.length >= 1 && options.sortFields.indexOf(sortField) > -1; 49 | 50 | if (sortField) { 51 | if (emptyWhiteList || inWhiteList) { 52 | sortField = prefixField(sortField); 53 | } else { 54 | return { 55 | error: 'Invalid sort field' 56 | }; 57 | } 58 | } else { 59 | sortField = prefixField(options.sortField); 60 | } 61 | 62 | if (!sortMethod) { 63 | sortMethod = options.sortMethod; 64 | } 65 | 66 | return { 67 | sortField: quoteField(sortField), 68 | sortMethod: sortMethod 69 | }; 70 | } 71 | 72 | module.exports = OrderBy; 73 | -------------------------------------------------------------------------------- /sql/peers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PeersSql = { 4 | sortFields: ['ip', 'port', 'state', 'os', 'version'], 5 | 6 | count: 'SELECT COUNT(*)::int FROM peers', 7 | 8 | banManager: 'UPDATE peers SET "state" = 1, "clock" = null WHERE ("state" = 0 AND "clock" - ${now} < 0);', 9 | 10 | getByFilter: function (params) { 11 | return [ 12 | 'SELECT "ip", "port", "state", "os", "version" FROM peers', 13 | (params.where.length ? 'WHERE ' + params.where.join(' AND ') : ''), 14 | (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') : 'ORDER BY random()'), 15 | 'LIMIT ${limit} OFFSET ${offset}' 16 | ].filter(Boolean).join(' '); 17 | }, 18 | 19 | randomList: function (params) { 20 | return [ 21 | 'SELECT p."ip", p."port", p."state", p."os", p."version" FROM peers p', 22 | 'WHERE p."state" > 0 ORDER BY RANDOM() LIMIT ${limit}' 23 | ].filter(Boolean).join(' '); 24 | }, 25 | 26 | state: 'UPDATE peers SET "state" = ${state}, "clock" = ${clock} WHERE "ip" = ${ip} AND "port" = ${port};', 27 | 28 | remove: 'DELETE FROM peers WHERE "ip" = ${ip} AND "port" = ${port};', 29 | 30 | getByIdPort: 'SELECT "id" FROM peers WHERE "ip" = ${ip} AND "port" = ${port}', 31 | 32 | insert: 'INSERT INTO peers ("ip", "port", "state", "os", "version") VALUES (${ip}, ${port}, ${state}, ${os}, ${version}) ON CONFLICT DO NOTHING;', 33 | 34 | upsertWithState: 'INSERT INTO peers ("ip", "port", "state", "os", "version") VALUES (${ip}, ${port}, ${state}, ${os}, ${version}) ON CONFLICT ("ip", "port") DO UPDATE SET ("ip", "port", "state", "os", "version") = (${ip}, ${port}, (CASE WHEN EXCLUDED."state" = 0 THEN EXCLUDED."state" ELSE ${state} END), ${os}, ${version})', 35 | 36 | upsertWithoutState: 'INSERT INTO peers ("ip", "port", "os", "version") VALUES (${ip}, ${port}, ${os}, ${version}) ON CONFLICT ("ip", "port") DO UPDATE SET ("ip", "port", "os", "version") = (${ip}, ${port}, ${os}, ${version})', 37 | 38 | insertSeed: 'INSERT INTO peers("ip", "port", "state") VALUES(${ip}, ${port}, ${state}) ON CONFLICT DO NOTHING;', 39 | }; 40 | 41 | module.exports = PeersSql; 42 | -------------------------------------------------------------------------------- /config.devnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4002, 3 | "address": "0.0.0.0", 4 | "version": "1.1.1", 5 | "fileLogLevel": "info", 6 | "logFileName": "logs/ark.log", 7 | "consoleLogLevel": "debug", 8 | "trustProxy": false, 9 | "db": { 10 | "host": "localhost", 11 | "port": 5432, 12 | "database": "ark_devnet", 13 | "user": null, 14 | "password": "password", 15 | "poolSize": 20, 16 | "poolIdleTimeout": 30000, 17 | "reapIntervalMillis": 1000, 18 | "logEvents": [ 19 | "error" 20 | ] 21 | }, 22 | "api": { 23 | "mount": true, 24 | "access": { 25 | "whiteList": [] 26 | }, 27 | "options": { 28 | "limits": { 29 | "max": 0, 30 | "delayMs": 0, 31 | "delayAfter": 0, 32 | "windowMs": 60000 33 | } 34 | } 35 | }, 36 | "peers": { 37 | "minimumNetworkReach": 10, 38 | "list": [ 39 | { 40 | "ip": "167.114.29.51", 41 | "port": 4002 42 | }, 43 | { 44 | "ip": "167.114.29.52", 45 | "port": 4002 46 | }, 47 | { 48 | "ip": "167.114.29.53", 49 | "port": 4002 50 | }, 51 | { 52 | "ip": "167.114.29.54", 53 | "port": 4002 54 | }, 55 | { 56 | "ip": "167.114.29.55", 57 | "port": 4002 58 | } 59 | ], 60 | "blackList": [], 61 | "options": { 62 | "limits": { 63 | "max": 0, 64 | "delayMs": 0, 65 | "delayAfter": 0, 66 | "windowMs": 60000 67 | }, 68 | "maxUpdatePeers": 20, 69 | "timeout": 5000 70 | } 71 | }, 72 | "forging": { 73 | "coldstart": 6, 74 | "force": true, 75 | "secret": [], 76 | "access": { 77 | "whiteList": [ 78 | "127.0.0.1" 79 | ] 80 | } 81 | }, 82 | "loading": { 83 | "verifyOnLoading": false, 84 | "loadPerIteration": 5000 85 | }, 86 | "ssl": { 87 | "enabled": false, 88 | "options": { 89 | "port": 443, 90 | "address": "0.0.0.0", 91 | "key": "./ssl/ark.key", 92 | "cert": "./ssl/ark.crt" 93 | } 94 | }, 95 | "network": "devnet", 96 | "nethash": "578e820911f24e039733b45e4882b73e301f813a0d2c31330dafda84534ffa23" 97 | } 98 | -------------------------------------------------------------------------------- /schema/accounts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | open: { 5 | id: 'accounts.openAccount', 6 | type: 'object', 7 | properties: { 8 | secret: { 9 | type: 'string', 10 | minLength: 1, 11 | maxLength: 100 12 | } 13 | }, 14 | required: ['secret'] 15 | }, 16 | getBalance: { 17 | id: 'accounts.getBalance', 18 | type: 'object', 19 | properties: { 20 | address: { 21 | type: 'string', 22 | minLength: 1, 23 | format: 'address' 24 | } 25 | }, 26 | required: ['address'] 27 | }, 28 | getPublicKey: { 29 | id: 'accounts.getPublickey', 30 | type: 'object', 31 | properties: { 32 | address: { 33 | type: 'string', 34 | minLength: 1, 35 | format: 'address' 36 | } 37 | }, 38 | required: ['address'] 39 | }, 40 | generatePublicKey: { 41 | id: 'accounts.generatePublickey', 42 | type: 'object', 43 | properties: { 44 | secret: { 45 | type: 'string', 46 | minLength: 1 47 | } 48 | }, 49 | required: ['secret'] 50 | }, 51 | getDelegates: { 52 | id: 'accounts.getDelegates', 53 | type: 'object', 54 | properties: { 55 | address: { 56 | type: 'string', 57 | minLength: 1, 58 | format: 'address' 59 | } 60 | }, 61 | required: ['address'] 62 | }, 63 | addDelegates: { 64 | id: 'accounts.addDelegates', 65 | type: 'object', 66 | properties: { 67 | secret: { 68 | type: 'string', 69 | minLength: 1 70 | }, 71 | publicKey: { 72 | type: 'string', 73 | format: 'publicKey' 74 | }, 75 | secondSecret: { 76 | type: 'string', 77 | minLength: 1 78 | } 79 | } 80 | }, 81 | getAccount: { 82 | id: 'accounts.getAccount', 83 | type: 'object', 84 | properties: { 85 | address: { 86 | type: 'string', 87 | minLength: 1, 88 | format: 'address' 89 | } 90 | }, 91 | required: ['address'] 92 | }, 93 | top: { 94 | id: 'accounts.top', 95 | type: 'object', 96 | properties: { 97 | limit: { 98 | type: 'integer', 99 | minimum: 0, 100 | maximum: 100 101 | }, 102 | offset: { 103 | type: 'integer', 104 | minimum: 0 105 | } 106 | } 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /test/api/peer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var node = require('./../node.js'); 4 | 5 | describe('GET /peer/list', function () { 6 | 7 | before(function (done) { 8 | node.addPeers(2, done); 9 | }); 10 | 11 | it('using incorrect nethash in headers should fail', function (done) { 12 | node.get('/peer/list') 13 | .set('nethash', 'incorrect') 14 | .end(function (err, res) { 15 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 16 | node.expect(res.body).to.have.property('success').to.be.not.ok; 17 | node.expect(res.body.expected).to.equal(node.config.nethash); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('using valid headers should be ok', function (done) { 23 | node.get('/peer/list') 24 | .end(function (err, res) { 25 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 26 | node.expect(res.body).to.have.property('peers').that.is.an('array'); 27 | node.expect(res.body.peers).to.have.length.of.at.least(0); 28 | res.body.peers.forEach(function (peer) { 29 | node.expect(peer).to.have.property('ip').that.is.a('string'); 30 | node.expect(peer).to.have.property('port').that.is.a('number'); 31 | node.expect(peer).to.have.property('os'); 32 | node.expect(peer).to.have.property('version'); 33 | }); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('GET /peer/height', function () { 40 | 41 | it('using incorrect nethash in headers should fail', function (done) { 42 | node.get('/peer/height') 43 | .set('nethash', 'incorrect') 44 | .end(function (err, res) { 45 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 46 | node.expect(res.body).to.have.property('success').to.be.not.ok; 47 | node.expect(res.body.expected).to.equal(node.config.nethash); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('using valid headers should be ok', function (done) { 53 | node.get('/peer/height') 54 | .end(function (err, res) { 55 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 56 | node.expect(res.body).to.have.property('success').to.be.ok; 57 | node.expect(res.body).to.be.an('object').that.has.property('height'); 58 | node.expect(res.body.height).to.be.a('number').to.be.above(1); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /docs/template/arkio.jst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 8 | 9 | 10 | 11 | <% if (!hasTitle) { %> 12 |
13 |

<%= title %>

14 | 15 |
16 |
17 | <% } %> 18 |
19 |
20 | <% if (sources.length > 1) { %> 21 | 38 | <% } %> 39 | 60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /schema/api.peer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 'GET:/peer/status':{ 5 | id: 'GET:/peer/status', 6 | type: 'object', 7 | properties: { 8 | success: { 9 | type: 'boolean' 10 | }, 11 | height: { 12 | type: 'integer', 13 | minimum: 0 14 | }, 15 | currentSlot: { 16 | type: 'integer', 17 | minimum: 0 18 | }, 19 | forgingAllowed: { 20 | type: 'boolean' 21 | }, 22 | header: { 23 | type: 'object' 24 | } 25 | }, 26 | required: ['success','height','header','currentSlot','forgingAllowed'] 27 | }, 28 | 'GET:/peer/height':{ 29 | id: 'GET:/peer/height', 30 | type: 'object', 31 | properties: { 32 | success: { 33 | type: 'boolean' 34 | }, 35 | height: { 36 | type: 'integer', 37 | minimum: 0 38 | }, 39 | header: { 40 | type: 'object' 41 | } 42 | }, 43 | required: ['success','height','header'] 44 | }, 45 | 'POST:/peer/transactions':{ 46 | id: 'POST:/peer/transactions', 47 | type: 'object' 48 | }, 49 | 'GET:/peer/transactions':{ 50 | id: 'GET:/peer/transactions', 51 | type: 'object', 52 | properties: { 53 | success: { 54 | type: 'boolean' 55 | }, 56 | transactions: { 57 | type: 'array', 58 | uniqueItems: true 59 | } 60 | }, 61 | required: ['transactions'] 62 | }, 63 | 'GET:/peer/transactionsFromIds':{ 64 | id: 'POST:/peer/transactionsFromIds', 65 | type: 'object' 66 | }, 67 | 'GET:/peer/blocks':{ 68 | id: 'GET:/peer/blocks', 69 | type: 'object', 70 | properties: { 71 | success: { 72 | type: 'boolean' 73 | }, 74 | blocks: { 75 | type: 'array' 76 | }, 77 | }, 78 | required: ['blocks'] 79 | }, 80 | 'POST:/peer/blocks':{ 81 | id: 'POST:/peer/blocks', 82 | type: 'object', 83 | properties: { 84 | success: { 85 | type: 'boolean' 86 | }, 87 | blockId: { 88 | type: 'string' 89 | }, 90 | }, 91 | required: ['success', 'blockId'] 92 | }, 93 | 'GET:/peer/block':{ 94 | id: 'GET:/peer/block', 95 | type: 'object' 96 | }, 97 | 'GET:/peer/blocks/common':{ 98 | id: 'GET:/peer/blocks/common', 99 | type: 'object' 100 | }, 101 | 'GET:/peer/list':{ 102 | id: 'GET:/peer/list', 103 | type: 'object', 104 | properties: { 105 | success: { 106 | type: 'boolean' 107 | }, 108 | peers: { 109 | type: 'array' 110 | }, 111 | }, 112 | required: ['peers'] 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /sql/transactions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var TransactionsSql = { 4 | sortFields: [ 5 | 'id', 6 | 'blockId', 7 | 'amount', 8 | 'fee', 9 | 'type', 10 | 'timestamp', 11 | 'senderPublicKey', 12 | 'senderId', 13 | 'recipientId', 14 | 'confirmations', 15 | 'height' 16 | ], 17 | 18 | countById: 'SELECT COUNT("id")::int AS "count" FROM transactions WHERE "id" = ${id}', 19 | 20 | countList: function (params) { 21 | return [ 22 | 'SELECT COUNT("id") FROM transactions', 23 | (params.where.length || params.owner ? 'WHERE' : ''), 24 | (params.where.length ? '(' + params.where.join(' OR ') + ')' : ''), 25 | (params.where.length && params.owner ? ' AND ' + params.owner : params.owner) 26 | ].filter(Boolean).join(' '); 27 | }, 28 | 29 | list: function (params) { 30 | // Need to fix 'or' or 'and' in query 31 | return [ 32 | 'SELECT t.id, b.id as blockid, type, t.timestamp, amount, fee, "vendorField", "senderId", "recipientId", encode("senderPublicKey", \'hex\') as "senderPublicKey", encode("requesterPublicKey", \'hex\') as "requesterPublicKey", encode("signature", \'hex\') as "signature", encode("signSignature", \'hex\') as "signSignature", signatures::json as signatures, rawasset::json as asset, (SELECT MAX(height) + 1 FROM blocks) - b.height AS confirmations FROM transactions t, blocks b WHERE b.id = t."blockId"', 33 | (params.where.length || params.owner ? 'AND' : ''), 34 | (params.where.length ? '(' + params.where.join(' OR ') + ')' : ''), 35 | (params.where.length && params.owner ? ' AND ' + params.owner : params.owner), 36 | (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') : ''), 37 | 'LIMIT ${limit} OFFSET ${offset}' 38 | ].filter(Boolean).join(' '); 39 | }, 40 | 41 | getById: 'SELECT t.id, b.id as blockid, b.height, type, t.timestamp, amount, fee, "vendorField", "senderId", "recipientId", encode("senderPublicKey", \'hex\') as "senderPublicKey", encode("requesterPublicKey", \'hex\') as "requesterPublicKey", encode("signature", \'hex\') as "signature", encode("signSignature", \'hex\') as "signSignature", signatures::json as signatures, rawasset::json as asset, (SELECT MAX(height) + 1 FROM blocks) - b.height AS confirmations FROM transactions t, blocks b WHERE b.id = t."blockId" AND t.id = ${id}', 42 | 43 | getVotesById: 'SELECT * FROM votes WHERE "transactionId" = ${id}' 44 | 45 | }; 46 | 47 | module.exports = TransactionsSql; 48 | -------------------------------------------------------------------------------- /config.main.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8000, 3 | "address": "0.0.0.0", 4 | "version": "0.3.3", 5 | "fileLogLevel": "info", 6 | "logFileName": "logs/ark.log", 7 | "consoleLogLevel": "debug", 8 | "trustProxy": false, 9 | "db": { 10 | "host": "localhost", 11 | "port": 5432, 12 | "database": "ark_main", 13 | "user": null, 14 | "password": "password", 15 | "poolSize": 20, 16 | "poolIdleTimeout": 30000, 17 | "reapIntervalMillis": 1000, 18 | "logEvents": [ 19 | "error" 20 | ] 21 | }, 22 | "api": { 23 | "access": { 24 | "whiteList": [] 25 | }, 26 | "options": { 27 | "limits": { 28 | "max": 0, 29 | "delayMs": 0, 30 | "delayAfter": 0, 31 | "windowMs": 60000 32 | } 33 | } 34 | }, 35 | "peers": { 36 | "list": [ 37 | { 38 | "ip": "40.68.214.86", 39 | "port": 8000 40 | }, 41 | { 42 | "ip": "13.70.207.248", 43 | "port": 8000 44 | }, 45 | { 46 | "ip": "13.89.42.130", 47 | "port": 8000 48 | }, 49 | { 50 | "ip": "52.160.98.183", 51 | "port": 8000 52 | }, 53 | { 54 | "ip": "40.121.84.254", 55 | "port": 8000 56 | } 57 | ], 58 | "blackList": [], 59 | "options": { 60 | "limits": { 61 | "max": 0, 62 | "delayMs": 0, 63 | "delayAfter": 0, 64 | "windowMs": 60000 65 | }, 66 | "maxUpdatePeers": 20, 67 | "timeout": 5000 68 | } 69 | }, 70 | "forging": { 71 | "force": false, 72 | "secret": [], 73 | "access": { 74 | "whiteList": [ 75 | "127.0.0.1" 76 | ] 77 | } 78 | }, 79 | "loading": { 80 | "verifyOnLoading": false, 81 | "loadPerIteration": 5000 82 | }, 83 | "ssl": { 84 | "enabled": false, 85 | "options": { 86 | "port": 443, 87 | "address": "0.0.0.0", 88 | "key": "./ssl/ark.key", 89 | "cert": "./ssl/ark.crt" 90 | } 91 | }, 92 | "nethash": "ed14889723f24ecc54871d058d98ce91ff2f973192075c0155ba2b7b70ad2511" 93 | } 94 | -------------------------------------------------------------------------------- /sql/migrations/20160908215531_protectMemAccountsColumns.sql: -------------------------------------------------------------------------------- 1 | /* Protect Mem Accounts Columns 2 | * 3 | */ 4 | 5 | BEGIN; 6 | 7 | DROP TRIGGER IF EXISTS protect_mem_accounts ON "mem_accounts"; 8 | 9 | DROP FUNCTION IF EXISTS revert_mem_account(); 10 | 11 | CREATE FUNCTION revert_mem_account() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$ 12 | BEGIN 13 | 14 | -- As per columns marked as immutable within application layer (logic/account.js). 15 | 16 | -- Revert any change of address 17 | IF NEW."address" <> OLD."address" THEN 18 | RAISE WARNING 'Reverting change of address from % to %', OLD."address", NEW."address"; 19 | NEW."address" = OLD."address"; 20 | END IF; 21 | 22 | -- Revert any change of u_username 23 | IF NEW."u_username" <> OLD."u_username" AND OLD."u_username" IS NOT NULL THEN 24 | RAISE WARNING 'Reverting change of u_username from % to %', OLD."u_username", NEW."u_username"; 25 | NEW."u_username" = OLD."u_username"; 26 | END IF; 27 | 28 | -- Revert any change of username 29 | IF NEW."username" <> OLD."username" AND OLD."username" IS NOT NULL THEN 30 | RAISE WARNING 'Reverting change of username from % to %', OLD."username", NEW."username"; 31 | NEW."username" = OLD."username"; 32 | END IF; 33 | 34 | -- Revert any change of virginity 35 | -- If account is no longer a virgin 36 | IF NEW."virgin" <> OLD."virgin" AND OLD."virgin" = 0 THEN 37 | RAISE WARNING 'Reverting change of virgin from % to %', OLD."virgin", NEW."virgin"; 38 | NEW."virgin" = OLD."virgin"; 39 | END IF; 40 | 41 | -- Revert any change of publicKey 42 | -- If account is no longer a virgin 43 | -- And publicKey is already set 44 | IF NEW."publicKey" <> OLD."publicKey" AND OLD."virgin" = 0 AND OLD."publicKey" IS NOT NULL THEN 45 | RAISE WARNING 'Reverting change of publicKey from % to %', ENCODE(OLD."publicKey", 'hex'), ENCODE(NEW."publicKey", 'hex'); 46 | NEW."publicKey" = OLD."publicKey"; 47 | END IF; 48 | 49 | -- Revert any change of secondPublicKey 50 | -- If secondPublicKey is already set 51 | IF NEW."secondPublicKey" <> OLD."secondPublicKey" AND OLD."secondPublicKey" IS NOT NULL THEN 52 | RAISE WARNING 'Reverting change of secondPublicKey from % to %', ENCODE(OLD."secondPublicKey", 'hex'), ENCODE(NEW."secondPublicKey", 'hex'); 53 | NEW."secondPublicKey" = OLD."secondPublicKey"; 54 | END IF; 55 | 56 | RETURN NEW; 57 | 58 | END $$; 59 | 60 | CREATE TRIGGER protect_mem_accounts 61 | BEFORE UPDATE ON "mem_accounts" FOR EACH ROW 62 | EXECUTE PROCEDURE revert_mem_account(); 63 | 64 | COMMIT; 65 | -------------------------------------------------------------------------------- /schema/peers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | headers: { 5 | id: 'peer.headers', 6 | type: 'object', 7 | properties: { 8 | port: { 9 | type: 'integer', 10 | minimum: 1, 11 | maximum: 65535 12 | }, 13 | os: { 14 | type: 'string', 15 | maxLength: 64 16 | }, 17 | nethash: { 18 | type: 'string', 19 | maxLength: 64 20 | }, 21 | height: { 22 | type: 'integer', 23 | minimum: 0 24 | }, 25 | version: { 26 | type: 'string', 27 | maxLength: 11 28 | }, 29 | blockheader: { 30 | type: 'object' 31 | } 32 | }, 33 | required: ['port', 'nethash', 'version'] 34 | }, 35 | updatePeersList: { 36 | peers: { 37 | id: 'peer.updatePeersList.peers', 38 | type: 'object', 39 | properties: { 40 | peers: { 41 | type: 'array', 42 | uniqueItems: true 43 | } 44 | }, 45 | required: ['peers'] 46 | }, 47 | peer: { 48 | id: 'peer.updatePeersList.peer', 49 | type: 'object', 50 | properties: { 51 | ip: { 52 | type: 'string', 53 | format: 'ip' 54 | }, 55 | port: { 56 | type: 'integer', 57 | minimum: 1, 58 | maximum: 65535 59 | }, 60 | state: { 61 | type: 'integer', 62 | minimum: 0, 63 | maximum: 3 64 | }, 65 | os: { 66 | type: 'string', 67 | maxLength: 64 68 | }, 69 | version: { 70 | type: 'string', 71 | maxLength: 11 72 | } 73 | }, 74 | required: ['ip', 'port'] 75 | } 76 | }, 77 | getPeers: { 78 | id: 'peer.getPeers', 79 | type: 'object', 80 | properties: { 81 | port: { 82 | type: 'integer', 83 | minimum: 1, 84 | maximum: 65535 85 | }, 86 | state: { 87 | type: 'integer', 88 | minimum: 0, 89 | maximum: 3 90 | }, 91 | os: { 92 | type: 'string', 93 | maxLength: 64 94 | }, 95 | version: { 96 | type: 'string', 97 | maxLength: 11 98 | }, 99 | orderBy: { 100 | type: 'string' 101 | }, 102 | limit: { 103 | type: 'integer', 104 | minimum: 0, 105 | maximum: 100 106 | }, 107 | offset: { 108 | type: 'integer', 109 | minimum: 0 110 | } 111 | } 112 | }, 113 | getPeer: { 114 | id: 'peer.getPeer', 115 | type: 'object', 116 | properties: { 117 | ip: { 118 | type: 'string', 119 | format: 'ip' 120 | }, 121 | port: { 122 | type: 'integer', 123 | minimum: 0, 124 | maximum: 65535 125 | } 126 | }, 127 | required: ['ip', 'port'] 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /sql/rounds.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RoundsSql = { 4 | 5 | saveActiveDelegates: function (activedelegates) { 6 | var values = activedelegates.map(function(ad){ 7 | return "('"+ad.publicKey+"', (${round})::bigint, ("+ad.vote+")::bigint)"; 8 | }).join(","); 9 | 10 | return 'DELETE FROM mem_delegates where round = (${round})::bigint; INSERT INTO mem_delegates ("publicKey", round, vote) VALUES ' + values; 11 | }, 12 | 13 | getActiveDelegates: 'SELECT * FROM mem_delegates WHERE round = (${round})::bigint ORDER BY vote DESC, "publicKey" ASC;', 14 | 15 | getRoundForgers: 'SELECT ENCODE("generatorPublicKey", \'hex\') as "publicKey" FROM blocks WHERE height > ${minheight} AND height < ${maxheight}+1 ORDER BY height desc;', 16 | 17 | updateActiveDelegatesStats: function (stats) { 18 | var statements = Object.keys(stats).map(function(pk){ 19 | var stat = stats[pk]; 20 | var statement = 'UPDATE mem_delegates SET '; 21 | statement += 'missedblocks = '+stat.missedblocks+','; 22 | statement += 'producedblocks = '+stat.producedblocks; 23 | statement += ' WHERE "publicKey" = \''+pk+'\' AND round = (${round})::bigint;'; 24 | if(stat.missedblocks > 0){ 25 | statement += 'UPDATE mem_accounts SET '; 26 | statement += 'missedblocks = missedblocks + ' + stat.missedblocks; 27 | statement += ' WHERE ENCODE("publicKey", \'hex\') = \''+pk+'\';'; 28 | } 29 | return statement; 30 | }); 31 | 32 | return statements.join(""); 33 | }, 34 | 35 | truncateBlocks: 'DELETE FROM blocks WHERE "height" > (${height})::bigint;', 36 | 37 | updateMissedBlocks: function (backwards) { 38 | return [ 39 | 'UPDATE mem_accounts SET "missedblocks" = "missedblocks"', 40 | (backwards ? '- 1' : '+ 1'), 41 | 'WHERE "address" IN ($1:csv);' 42 | ].join(' '); 43 | }, 44 | 45 | getTotalVotes: 'select ARRAY_AGG(a."accountId") as voters, SUM(b.balance) as vote FROM mem_accounts2delegates a, mem_accounts b where a."accountId" = b.address AND a."dependentId" = ${delegate};', 46 | 47 | updateVotes: 'UPDATE mem_accounts SET "vote" = "vote" + (${amount})::bigint WHERE "address" = ${address};', 48 | 49 | updateTotalVotes: 'UPDATE mem_accounts m SET vote = (SELECT COALESCE(SUM(b.balance), 0) as vote FROM mem_accounts2delegates a, mem_accounts b where a."accountId" = b.address AND a."dependentId" = encode(m."publicKey", \'hex\')) WHERE m."isDelegate" = 1;', 50 | 51 | updateBlockId: 'UPDATE mem_accounts SET "blockId" = ${newId} WHERE "blockId" = ${oldId};' 52 | 53 | }; 54 | 55 | module.exports = RoundsSql; 56 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var strftime = require('strftime').utc(); 4 | var fs = require('fs'); 5 | var util = require('util'); 6 | require('colors'); 7 | 8 | module.exports = function (config) { 9 | config = config || {}; 10 | var exports = {}; 11 | 12 | config.levels = config.levels || { 13 | trace: 0, 14 | debug: 1, 15 | log: 2, 16 | info: 3, 17 | warn: 4, 18 | error: 5, 19 | fatal: 6 20 | }; 21 | 22 | config.level_abbr = config.level_abbr || { 23 | trace: 'trc', 24 | debug: 'dbg', 25 | log: 'log', 26 | info: 'inf', 27 | warn: 'WRN', 28 | error: 'ERR', 29 | fatal: 'FTL' 30 | }; 31 | 32 | config.filename = config.filename || __dirname + '/logs.log'; 33 | 34 | config.errorLevel = config.errorLevel || 'log'; 35 | 36 | var log_file = fs.createWriteStream(config.filename, {flags: 'a'}); 37 | 38 | exports.setLevel = function (errorLevel) { 39 | config.errorLevel = errorLevel; 40 | }; 41 | 42 | function snipsecret (data) { 43 | for (var key in data) { 44 | if (key.search(/secret/i) > -1) { 45 | data[key] = 'XXXXXXXXXX'; 46 | } 47 | } 48 | return data; 49 | } 50 | 51 | Object.keys(config.levels).forEach(function (name) { 52 | function log (message, data) { 53 | var log = { 54 | level: name, 55 | timestamp: strftime('%F %T', new Date()) 56 | }; 57 | 58 | if (message instanceof Error) { 59 | log.message = message.stack; 60 | } else { 61 | 62 | if(message){ 63 | log.message = message.toString(); 64 | if(log.message.startsWith("# ")){ 65 | var head="#".repeat(message.length+2); 66 | log.message=head+"\n"; 67 | log.message+=message+" #\n"; 68 | log.message+=head; 69 | } 70 | } 71 | else { 72 | log.message = message; 73 | } 74 | } 75 | 76 | if (data && util.isObject(data)) { 77 | log.data = JSON.stringify(snipsecret(data)); 78 | } else { 79 | log.data = data; 80 | } 81 | 82 | log.symbol = config.level_abbr[log.level] ? config.level_abbr[log.level] : '???'; 83 | 84 | if (config.levels[config.errorLevel] <= config.levels[log.level]) { 85 | log.message.split("\n").forEach(function(m){ 86 | if (log.data) { 87 | log_file.write(util.format('[%s] %s | %s - %s\n', log.symbol, log.timestamp, m, log.data)); 88 | } else { 89 | log_file.write(util.format('[%s] %s | %s\n', log.symbol, log.timestamp, m)); 90 | } 91 | }); 92 | } 93 | 94 | if (config.echo && config.levels[config.echo] <= config.levels[log.level]) { 95 | log.message.split("\n").forEach(function(m){ 96 | if (log.data) { 97 | console.log('['+log.symbol.bgYellow.black+']', log.timestamp.grey, '|', m, '-', log.data); 98 | } else { 99 | console.log('['+log.symbol.bgYellow.black+']', log.timestamp.grey, '|', m); 100 | } 101 | }); 102 | } 103 | } 104 | 105 | exports[name] = log; 106 | }); 107 | 108 | return exports; 109 | }; 110 | -------------------------------------------------------------------------------- /schema/delegates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../helpers/constants.js'); 4 | 5 | module.exports = { 6 | enableForging: { 7 | id: 'delegates.enableForging', 8 | type: 'object', 9 | properties: { 10 | secret: { 11 | type: 'string', 12 | minLength: 1, 13 | maxLength: 100 14 | }, 15 | publicKey: { 16 | type: 'string', 17 | format: 'publicKey' 18 | } 19 | }, 20 | required: ['secret'] 21 | }, 22 | disableForging: { 23 | id: 'delegates.disableForging', 24 | type: 'object', 25 | properties: { 26 | secret: { 27 | type: 'string', 28 | minLength: 1, 29 | maxLength: 100 30 | }, 31 | publicKey: { 32 | type: 'string', 33 | format: 'publicKey' 34 | } 35 | }, 36 | required: ['secret'] 37 | }, 38 | forgingStatus: { 39 | id: 'delegates.forgingStatus', 40 | type: 'object', 41 | properties: { 42 | publicKey: { 43 | type: 'string', 44 | format: 'publicKey' 45 | } 46 | }, 47 | required: ['publicKey'] 48 | }, 49 | getDelegate: { 50 | id: 'delegates.getDelegate', 51 | type: 'object', 52 | properties: { 53 | publicKey: { 54 | type: 'string' 55 | }, 56 | username: { 57 | type: 'string' 58 | } 59 | } 60 | }, 61 | search: { 62 | id: 'delegates.search', 63 | type: 'object', 64 | properties: { 65 | q: { 66 | type: 'string', 67 | minLength: 1, 68 | maxLength: 20 69 | }, 70 | limit: { 71 | type: 'integer', 72 | minimum: 1, 73 | maximum: 100 74 | } 75 | }, 76 | required: ['q'] 77 | }, 78 | getVoters: { 79 | id: 'delegates.getVoters', 80 | type: 'object', 81 | properties: { 82 | publicKey: { 83 | type: 'string', 84 | format: 'publicKey' 85 | } 86 | }, 87 | required: ['publicKey'] 88 | }, 89 | getDelegates: { 90 | id: 'delegates.getDelegates', 91 | type: 'object', 92 | properties: { 93 | orderBy: { 94 | type: 'string' 95 | }, 96 | limit: { 97 | type: 'integer', 98 | minimum: 1, 99 | maximum: constants.activeDelegates 100 | }, 101 | offset: { 102 | type: 'integer', 103 | minimum: 0 104 | } 105 | } 106 | }, 107 | getForgedByAccount: { 108 | id: 'delegates.getForgedByAccount', 109 | type: 'object', 110 | properties: { 111 | generatorPublicKey: { 112 | type: 'string', 113 | format: 'publicKey' 114 | } 115 | }, 116 | required: ['generatorPublicKey'] 117 | }, 118 | addDelegate: { 119 | id: 'delegates.addDelegate', 120 | type: 'object', 121 | properties: { 122 | secret: { 123 | type: 'string', 124 | minLength: 1, 125 | maxLength: 100 126 | }, 127 | publicKey: { 128 | type: 'string', 129 | format: 'publicKey' 130 | }, 131 | secondSecret: { 132 | type: 'string', 133 | minLength: 1, 134 | maxLength: 100 135 | }, 136 | username: { 137 | type: 'string' 138 | } 139 | }, 140 | required: ['secret'] 141 | } 142 | }; 143 | -------------------------------------------------------------------------------- /test/api/peer.signatures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var node = require('./../node.js'); 4 | 5 | describe('GET /peer/signatures', function () { 6 | 7 | it('using incorrect nethash in headers should fail', function (done) { 8 | node.get('/peer/signatures') 9 | .set('nethash', 'incorrect') 10 | .end(function (err, res) { 11 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 12 | node.expect(res.body).to.have.property('success').to.be.not.ok; 13 | node.expect(res.body.expected).to.equal(node.config.nethash); 14 | done(); 15 | }); 16 | }); 17 | 18 | it('using valid headers should be ok', function (done) { 19 | node.get('/peer/signatures') 20 | .end(function (err, res) { 21 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 22 | node.expect(res.body).to.have.property('success').to.be.ok; 23 | node.expect(res.body).to.have.property('signatures').that.is.an('array'); 24 | done(); 25 | }); 26 | }); 27 | }); 28 | 29 | describe('POST /peer/signatures', function () { 30 | 31 | var validParams; 32 | 33 | var transaction = node.ark.transaction.createTransaction('AacRfTLtxAkR3Mind1XdPCddj1uDkHtwzD', 1, null, node.gAccount.password); 34 | 35 | beforeEach(function (done) { 36 | validParams = { 37 | signature: { 38 | signature: transaction.signature, 39 | transaction: transaction.id 40 | } 41 | }; 42 | done(); 43 | }); 44 | 45 | it('using incorrect nethash in headers should fail', function (done) { 46 | node.post('/peer/signatures') 47 | .set('nethash', 'incorrect') 48 | .end(function (err, res) { 49 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 50 | node.expect(res.body).to.have.property('success').to.be.not.ok; 51 | node.expect(res.body.expected).to.equal(node.config.nethash); 52 | done(); 53 | }); 54 | }); 55 | 56 | it('using invalid signature schema should fail', function (done) { 57 | delete validParams.signature.transaction; 58 | 59 | node.post('/peer/signatures', validParams) 60 | .end(function (err, res) { 61 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 62 | node.expect(res.body).to.have.property('success').to.be.not.ok; 63 | node.expect(res.body).to.have.property('error').to.equal('Signature validation failed'); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('using unprocessable signature should fail', function (done) { 69 | validParams.signature.transaction = 'invalidId'; 70 | 71 | node.post('/peer/signatures', validParams) 72 | .end(function (err, res) { 73 | node.debug('> Response:'.grey, JSON.stringify(res.body)); 74 | node.expect(res.body).to.have.property('success').to.be.not.ok; 75 | node.expect(res.body).to.have.property('error').to.equal('Error processing signature'); 76 | done(); 77 | }); 78 | }); 79 | 80 | it('using processable signature should be ok'); 81 | }); 82 | -------------------------------------------------------------------------------- /schema/transactions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../helpers/constants.js'); 4 | 5 | module.exports = { 6 | getTransactions: { 7 | id: 'transactions.getTransactions', 8 | type: 'object', 9 | properties: { 10 | blockId: { 11 | type: 'string' 12 | }, 13 | limit: { 14 | type: 'integer', 15 | minimum: 0, 16 | maximum: 100 17 | }, 18 | type: { 19 | type: 'integer', 20 | minimum: 0, 21 | maximum: 10 22 | }, 23 | orderBy: { 24 | type: 'string' 25 | }, 26 | offset: { 27 | type: 'integer', 28 | minimum: 0 29 | }, 30 | senderPublicKey: { 31 | type: 'string', 32 | format: 'publicKey' 33 | }, 34 | vendorField: { 35 | type: 'string', 36 | format: 'vendorField' 37 | }, 38 | ownerPublicKey: { 39 | type: 'string', 40 | format: 'publicKey' 41 | }, 42 | ownerAddress: { 43 | type: 'string' 44 | }, 45 | senderId: { 46 | type: 'string', 47 | format: 'address' 48 | }, 49 | recipientId: { 50 | type: 'string', 51 | format: 'address' 52 | }, 53 | amount: { 54 | type: 'integer', 55 | minimum: 0, 56 | maximum: constants.fixedPoint 57 | }, 58 | fee: { 59 | type: 'integer', 60 | minimum: 0, 61 | maximum: constants.fixedPoint 62 | } 63 | } 64 | }, 65 | getTransaction: { 66 | id: 'transactions.getTransaction', 67 | type: 'object', 68 | properties: { 69 | id: { 70 | type: 'string', 71 | minLength: 1 72 | } 73 | }, 74 | required: ['id'] 75 | }, 76 | getUnconfirmedTransaction: { 77 | id: 'transactions.getUnconfirmedTransaction', 78 | type: 'object', 79 | properties: { 80 | id: { 81 | type: 'string', 82 | minLength: 1 83 | } 84 | }, 85 | required: ['id'] 86 | }, 87 | getUnconfirmedTransactions: { 88 | id: 'transactions.getUnconfirmedTransactions', 89 | type: 'object', 90 | properties: { 91 | senderPublicKey: { 92 | type: 'string', 93 | format: 'publicKey' 94 | }, 95 | address: { 96 | type: 'string' 97 | } 98 | } 99 | }, 100 | addTransactions: { 101 | id: 'transactions.addTransactions', 102 | type: 'object', 103 | properties: { 104 | secret: { 105 | type: 'string', 106 | minLength: 1, 107 | maxLength: 100 108 | }, 109 | amount: { 110 | type: 'integer', 111 | minimum: 1, 112 | maximum: constants.totalAmount 113 | }, 114 | recipientId: { 115 | type: 'string', 116 | minLength: 1, 117 | format: 'address' 118 | }, 119 | vendorField: { 120 | type: 'string', 121 | format: 'vendorField' 122 | }, 123 | publicKey: { 124 | type: 'string', 125 | format: 'publicKey' 126 | }, 127 | secondSecret: { 128 | type: 'string', 129 | minLength: 1, 130 | maxLength: 100 131 | }, 132 | multisigAccountPublicKey: { 133 | type: 'string', 134 | format: 'publicKey' 135 | } 136 | }, 137 | required: ['secret', 'amount', 'recipientId'] 138 | } 139 | }; 140 | -------------------------------------------------------------------------------- /sql/migrations/20160724114255_createMemoryTables.sql: -------------------------------------------------------------------------------- 1 | /* Create Memory Tables 2 | * 3 | */ 4 | 5 | BEGIN; 6 | 7 | CREATE TABLE IF NOT EXISTS "mem_accounts"( 8 | "username" VARCHAR(20), 9 | "isDelegate" SMALLINT DEFAULT 0, 10 | "u_isDelegate" SMALLINT DEFAULT 0, 11 | "secondSignature" SMALLINT DEFAULT 0, 12 | "u_secondSignature" SMALLINT DEFAULT 0, 13 | "u_username" VARCHAR(20), 14 | "address" VARCHAR(36) NOT NULL UNIQUE PRIMARY KEY, 15 | "publicKey" BYTEA, 16 | "secondPublicKey" BYTEA, 17 | "balance" BIGINT DEFAULT 0, 18 | "u_balance" BIGINT DEFAULT 0, 19 | "vote" BIGINT DEFAULT 0, 20 | "rate" BIGINT DEFAULT 0, 21 | "delegates" TEXT, 22 | "u_delegates" TEXT, 23 | "multisignatures" TEXT, 24 | "u_multisignatures" TEXT, 25 | "multimin" SMALLINT DEFAULT 0, 26 | "u_multimin" SMALLINT DEFAULT 0, 27 | "multilifetime" SMALLINT DEFAULT 0, 28 | "u_multilifetime" SMALLINT DEFAULT 0, 29 | "blockId" VARCHAR(64), 30 | "nameexist" SMALLINT DEFAULT 0, 31 | "u_nameexist" SMALLINT DEFAULT 0, 32 | "producedblocks" int DEFAULT 0, 33 | "missedblocks" int DEFAULT 0, 34 | "fees" BIGINT DEFAULT 0, 35 | "rewards" BIGINT DEFAULT 0, 36 | "virgin" SMALLINT DEFAULT 1 37 | ); 38 | 39 | CREATE INDEX IF NOT EXISTS "mem_accounts_balance" ON "mem_accounts"("balance"); 40 | 41 | CREATE TABLE IF NOT EXISTS "mem_delegates"( 42 | "publicKey" VARCHAR(66) NOT NULL, 43 | "vote" BIGINT NOT NULL, 44 | "round" BIGINT NOT NULL, 45 | "producedblocks" int, 46 | "missedblocks" int 47 | ); 48 | 49 | CREATE TABLE IF NOT EXISTS "mem_accounts2delegates"( 50 | "accountId" VARCHAR(36) NOT NULL, 51 | "dependentId" VARCHAR(66) NOT NULL, 52 | FOREIGN KEY ("accountId") REFERENCES mem_accounts("address") ON DELETE CASCADE 53 | ); 54 | 55 | CREATE TABLE IF NOT EXISTS "mem_accounts2u_delegates"( 56 | "accountId" VARCHAR(36) NOT NULL, 57 | "dependentId" VARCHAR(66) NOT NULL, 58 | FOREIGN KEY ("accountId") REFERENCES mem_accounts("address") ON DELETE CASCADE 59 | ); 60 | 61 | CREATE TABLE IF NOT EXISTS "mem_accounts2multisignatures"( 62 | "accountId" VARCHAR(36) NOT NULL, 63 | "dependentId" VARCHAR(66) NOT NULL, 64 | FOREIGN KEY ("accountId") REFERENCES mem_accounts("address") ON DELETE CASCADE 65 | ); 66 | 67 | CREATE TABLE IF NOT EXISTS "mem_accounts2u_multisignatures"( 68 | "accountId" VARCHAR(36) NOT NULL, 69 | "dependentId" VARCHAR(66) NOT NULL, 70 | FOREIGN KEY ("accountId") REFERENCES mem_accounts("address") ON DELETE CASCADE 71 | ); 72 | 73 | 74 | CREATE INDEX IF NOT EXISTS "mem_delegates_vote" ON "mem_delegates"("vote"); 75 | 76 | CREATE INDEX IF NOT EXISTS "mem_delegates_round" ON "mem_delegates"("round"); 77 | 78 | CREATE INDEX IF NOT EXISTS "mem_accounts2delegates_accountId" ON "mem_accounts2delegates"("accountId"); 79 | 80 | CREATE INDEX IF NOT EXISTS "mem_accounts2u_delegates_accountId" ON "mem_accounts2u_delegates"("accountId"); 81 | 82 | CREATE INDEX IF NOT EXISTS "mem_accounts2multisignatures_accountId" ON "mem_accounts2multisignatures"("accountId"); 83 | 84 | CREATE INDEX IF NOT EXISTS "mem_accounts2u_multisignatures_accountId" ON "mem_accounts2u_multisignatures"("accountId"); 85 | 86 | COMMIT; 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ark-node", 3 | "version": "0.2.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app.js", 7 | "doc": "docco -t docs/template/arkio.jst -c docs/template/arkio.css modules/* app.js README.md", 8 | "start:testnet": "node app.js --genesis genesisBlock.testnet.json --config config.testnet.json", 9 | "start:devnet": "node app.js --genesis genesisBlock.devnet.json --config config.devnet.json", 10 | "start:mainnet": "node app.js --genesis genesisBlock.mainnet.json --config config.mainnet.json -i", 11 | "start:test": "node app.js --genesis test/genesisBlock.json --config test/config.json", 12 | "test": "./node_modules/.bin/mocha", 13 | "cov": "./node_modules/.bin/mocha --require blanket -R html-cov test/helpers test/logic > tmp/coverage.html" 14 | }, 15 | "author": "Boris Povod , Pavel Nekrasov , Oliver Beddows , FX Thoorens ", 16 | "dependencies": { 17 | "arkjs": "github:arkecosystem/ark-js#mainnet", 18 | "async": "=2.0.1", 19 | "bip39": "^2.2.0", 20 | "body-parser": "^1.18.3", 21 | "bytebuffer": "=5.0.1", 22 | "change-case": "=3.0.0", 23 | "colors": "=1.1.2", 24 | "commander": "=2.9.0", 25 | "compression": "^1.7.3", 26 | "cors": "=2.8.1", 27 | "ejs": "=2.5.5", 28 | "express": "^4.16.4", 29 | "express-domain-middleware": "=0.1.0", 30 | "express-query-int": "=1.0.1", 31 | "express-rate-limit": "=2.5.0", 32 | "extend": "=3.0.0", 33 | "ip": "=1.1.3", 34 | "json-schema": "=0.2.3", 35 | "json-sql": "LiskHQ/json-sql#27e1ed1", 36 | "lodash": "=4.17.5", 37 | "method-override": "=2.3.10", 38 | "pg-monitor": "=0.5.11", 39 | "pg-native": "=1.10.0", 40 | "pg-promise": "=5.3.3", 41 | "popsicle": "=8.2.0", 42 | "pug": "^2.0.0-beta11", 43 | "randomstring": "=1.1.5", 44 | "request-ip": "^2.1.1", 45 | "rimraf": "=2.5.4", 46 | "socket.io": "=1.4.8", 47 | "strftime": "=0.9.2", 48 | "valid-url": "=1.0.9", 49 | "validator": "=5.7.0", 50 | "validator.js": "=2.0.3", 51 | "vorpal": "^1.11.4", 52 | "z-schema": "=3.18.0" 53 | }, 54 | "devDependencies": { 55 | "bitcore-mnemonic": "^1.5.0", 56 | "blanket": "=1.2.3", 57 | "browserify-bignum": "=1.3.0-2", 58 | "buffer": "=4.9.1", 59 | "chai": "=3.5.0", 60 | "chai-bignumber": "=2.0.0", 61 | "crypto-browserify": "=3.11.0", 62 | "csv": "=1.1.0", 63 | "faker": "=3.1.0", 64 | "grunt": "=0.4.5", 65 | "grunt-cli": "=1.2.0", 66 | "grunt-contrib-compress": "=1.3.0", 67 | "grunt-contrib-jshint": "=1.0.0", 68 | "grunt-exec": "=1.0.1", 69 | "grunt-jsdox": "=0.1.7", 70 | "grunt-obfuscator": "=0.1.0", 71 | "jsdox": "=0.4.10", 72 | "jshint": "^2.9.6", 73 | "mocha": "=3.1.0", 74 | "moment": "=2.22.2", 75 | "supertest": "=2.0.0" 76 | }, 77 | "config": { 78 | "blanket": { 79 | "pattern": [ 80 | "helpers", 81 | "logic" 82 | ], 83 | "data-cover-never": [ 84 | "node_modules", 85 | "test", 86 | "tmp" 87 | ] 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /logic/blockReward.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../helpers/constants.js'); 4 | 5 | // Private fields 6 | var __private = {}; 7 | 8 | // Constructor 9 | function BlockReward () { 10 | // Array of milestones 11 | this.milestones = constants.rewards.milestones; 12 | 13 | // Distance between each milestone 14 | this.distance = Math.floor(constants.rewards.distance); 15 | 16 | // Start rewards at block (n) 17 | this.rewardOffset = Math.floor(constants.rewards.offset); 18 | } 19 | 20 | // Private methods 21 | __private.parseHeight = function (height) { 22 | if (isNaN(height)) { 23 | throw 'Invalid block height'; 24 | } else { 25 | return Math.abs(height); 26 | } 27 | }; 28 | 29 | // Public methods 30 | // 31 | //__API__ `calcMilestone` 32 | 33 | // 34 | BlockReward.prototype.calcMilestone = function (height) { 35 | var location = Math.trunc((__private.parseHeight(height) - this.rewardOffset) / this.distance); 36 | var lastMile = this.milestones[this.milestones.length - 1]; 37 | 38 | if (location > (this.milestones.length - 1)) { 39 | return this.milestones.lastIndexOf(lastMile); 40 | } else { 41 | return location; 42 | } 43 | }; 44 | 45 | // 46 | //__API__ `calcReward` 47 | 48 | // 49 | BlockReward.prototype.calcReward = function (height) { 50 | height = __private.parseHeight(height); 51 | 52 | if (height < this.rewardOffset) { 53 | return 0; 54 | } else { 55 | return this.milestones[this.calcMilestone(height)]; 56 | } 57 | }; 58 | 59 | // 60 | //__API__ `calcSupply` 61 | 62 | // 63 | BlockReward.prototype.calcSupply = function (height) { 64 | height = __private.parseHeight(height); 65 | var milestone = this.calcMilestone(height); 66 | var supply = constants.totalAmount / Math.pow(10,8); 67 | var rewards = []; 68 | 69 | var amount = 0, multiplier = 0; 70 | 71 | for (var i = 0; i < this.milestones.length; i++) { 72 | if (milestone >= i) { 73 | multiplier = (this.milestones[i] / Math.pow(10,8)); 74 | 75 | if (height < this.rewardOffset) { 76 | break; // Rewards not started yet 77 | } else if (height < this.distance) { 78 | amount = height % this.distance; // Measure this.distance thus far 79 | } else { 80 | amount = this.distance; // Assign completed milestone 81 | height -= this.distance; // Deduct from total height 82 | 83 | // After last milestone 84 | if (height > 0 && i === this.milestones.length - 1) { 85 | var postHeight = this.rewardOffset - 1; 86 | 87 | if (height >= postHeight) { 88 | amount += (height - postHeight); 89 | } else { 90 | amount += (postHeight - height); 91 | } 92 | } 93 | } 94 | 95 | rewards.push([amount, multiplier]); 96 | } else { 97 | break; // Milestone out of bounds 98 | } 99 | } 100 | 101 | for (i = 0; i < rewards.length; i++) { 102 | var reward = rewards[i]; 103 | supply += reward[0] * reward[1]; 104 | } 105 | 106 | return supply * Math.pow(10,8); 107 | }; 108 | 109 | // Export 110 | module.exports = BlockReward; 111 | -------------------------------------------------------------------------------- /helpers/z_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ip = require('ip'); 4 | var bs58check = require('bs58check'); 5 | 6 | function schema(network){ 7 | this.z_schema = require('z-schema'); 8 | 9 | this.z_schema.registerFormat('hex', function (str) { 10 | try { 11 | new Buffer(str, 'hex'); 12 | } catch (e) { 13 | return false; 14 | } 15 | 16 | return true; 17 | }); 18 | 19 | this.z_schema.registerFormat('publicKey', function (str) { 20 | if (str.length === 0) { 21 | return true; 22 | } 23 | 24 | try { 25 | var publicKey = new Buffer(str, 'hex'); 26 | return publicKey.length === 33; 27 | } catch (e) { 28 | return false; 29 | } 30 | }); 31 | 32 | this.z_schema.registerFormat('address', function (str) { 33 | if (str.length === 0) { 34 | return true; 35 | } 36 | 37 | var version = network.pubKeyHash; 38 | try { 39 | var decode = bs58check.decode(str); 40 | return decode[0] == version; 41 | } catch(e){ 42 | return false; 43 | } 44 | }); 45 | 46 | 47 | this.z_schema.registerFormat('vendorField', function (str) { 48 | if (str.length === 0) { 49 | return true; 50 | } 51 | 52 | try { 53 | var vendorField = new Buffer(str); 54 | 55 | return vendorField.length < 65; 56 | } catch (e) { 57 | return false; 58 | } 59 | }); 60 | 61 | this.z_schema.registerFormat('csv', function (str) { 62 | try { 63 | var a = str.split(','); 64 | if (a.length > 0 && a.length <= 1000) { 65 | return true; 66 | } else { 67 | return false; 68 | } 69 | } catch (e) { 70 | return false; 71 | } 72 | }); 73 | 74 | this.z_schema.registerFormat('signature', function (str) { 75 | if (str.length === 0) { 76 | return true; 77 | } 78 | 79 | try { 80 | var signature = new Buffer(str, 'hex'); 81 | return signature.length < 73; 82 | } catch (e) { 83 | return false; 84 | } 85 | }); 86 | 87 | this.z_schema.registerFormat('voteString', function (str) { 88 | //Excluding capital hex characters 89 | //(mainnet database could contain mixed case vote strings?) 90 | return /^[-+]0[23][0-9a-fA-F]{64}$/.test(str); 91 | }); 92 | 93 | this.z_schema.registerFormat('queryList', function (obj) { 94 | obj.limit = 100; 95 | return true; 96 | }); 97 | 98 | this.z_schema.registerFormat('delegatesList', function (obj) { 99 | obj.limit = 51; 100 | return true; 101 | }); 102 | 103 | this.z_schema.registerFormat('parsedInt', function (value) { 104 | /*jslint eqeq: true*/ 105 | if (isNaN(value) || parseInt(value) != value || isNaN(parseInt(value, 10))) { 106 | return false; 107 | } 108 | 109 | value = parseInt(value); 110 | return true; 111 | }); 112 | 113 | this.z_schema.registerFormat('ip', function (str) { 114 | return ip.isV4Format(str); 115 | }); 116 | } 117 | 118 | 119 | 120 | // var registeredFormats = z_schema.getRegisteredFormats(); 121 | // console.log(registeredFormats); 122 | 123 | // Exports 124 | module.exports = schema; 125 | -------------------------------------------------------------------------------- /config.mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4001, 3 | "address": "0.0.0.0", 4 | "version": "1.3.3", 5 | "fileLogLevel": "info", 6 | "logFileName": "logs/ark.log", 7 | "consoleLogLevel": "info", 8 | "trustProxy": false, 9 | "minimumVersion": "1.3.1", 10 | "db": { 11 | "host": "localhost", 12 | "port": 5432, 13 | "database": "ark_mainnet", 14 | "user": null, 15 | "password": "password", 16 | "poolSize": 20, 17 | "poolIdleTimeout": 30000, 18 | "reapIntervalMillis": 1000, 19 | "logEvents": [ 20 | "error" 21 | ] 22 | }, 23 | "api": { 24 | "mount": true, 25 | "access": { 26 | "whiteList": [] 27 | }, 28 | "options": { 29 | "limits": { 30 | "max": 0, 31 | "delayMs": 0, 32 | "delayAfter": 0, 33 | "windowMs": 60000 34 | } 35 | } 36 | }, 37 | "peers": { 38 | "queryReach": 20, 39 | "minimumNetworkReach": 20, 40 | "list": [ 41 | {"ip":"5.39.9.240", "port":4001}, 42 | {"ip":"5.39.9.241", "port":4001}, 43 | {"ip":"5.39.9.242", "port":4001}, 44 | {"ip":"5.39.9.243", "port":4001}, 45 | {"ip":"5.39.9.244", "port":4001}, 46 | {"ip":"5.39.9.245", "port":4001}, 47 | {"ip":"5.39.9.246", "port":4001}, 48 | {"ip":"5.39.9.247", "port":4001}, 49 | {"ip":"5.39.9.248", "port":4001}, 50 | {"ip":"5.39.9.249", "port":4001}, 51 | {"ip":"5.39.9.250", "port":4001}, 52 | {"ip":"5.39.9.251", "port":4001}, 53 | {"ip":"5.39.9.252", "port":4001}, 54 | {"ip":"5.39.9.253", "port":4001}, 55 | {"ip":"5.39.9.254", "port":4001}, 56 | {"ip":"5.39.9.255", "port":4001}, 57 | {"ip":"54.38.48.160", "port":4001}, 58 | {"ip":"54.38.48.161", "port":4001}, 59 | {"ip":"54.38.48.162", "port":4001}, 60 | {"ip":"54.38.48.163", "port":4001}, 61 | {"ip":"54.38.48.164", "port":4001}, 62 | {"ip":"54.38.48.165", "port":4001}, 63 | {"ip":"54.38.48.166", "port":4001}, 64 | {"ip":"54.38.48.167", "port":4001}, 65 | {"ip":"54.38.48.168", "port":4001}, 66 | {"ip":"54.38.48.169", "port":4001}, 67 | {"ip":"54.38.48.170", "port":4001}, 68 | {"ip":"54.38.48.171", "port":4001}, 69 | {"ip":"54.38.48.172", "port":4001}, 70 | {"ip":"54.38.48.173", "port":4001}, 71 | {"ip":"54.38.48.174", "port":4001}, 72 | {"ip":"54.38.48.175", "port":4001} 73 | ], 74 | "blackList": [], 75 | "options": { 76 | "limits": { 77 | "max": 0, 78 | "delayMs": 0, 79 | "delayAfter": 0, 80 | "windowMs": 60000 81 | }, 82 | "maxUpdatePeers": 20, 83 | "timeout": 5000 84 | } 85 | }, 86 | "forging": { 87 | "coldstart": 30, 88 | "force": true, 89 | "secret": [], 90 | "access": { 91 | "whiteList": [ 92 | "127.0.0.1" 93 | ] 94 | } 95 | }, 96 | "loading": { 97 | "verifyOnLoading": false, 98 | "loadPerIteration": 5000 99 | }, 100 | "ssl": { 101 | "enabled": false, 102 | "options": { 103 | "port": 443, 104 | "address": "0.0.0.0", 105 | "key": "./ssl/ark.key", 106 | "cert": "./ssl/ark.crt" 107 | } 108 | }, 109 | "network":"ark", 110 | "nethash": "6e84d08bd299ed97c212c886c98a57e36545c8f5d645ca7eeae63a8bd62d8988" 111 | } 112 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var moment = require('moment'); 4 | var util = require('util'); 5 | 6 | module.exports = function (grunt) { 7 | var files = [ 8 | 'logger.js', 9 | 'helpers/**/*.js', 10 | 'modules/*.js', 11 | 'logic/*.js', 12 | 'schema/**/*.js', 13 | 'sql/**/*.js', 14 | 'app.js' 15 | ]; 16 | 17 | var today = moment().format('HH:mm:ss DD/MM/YYYY'); 18 | 19 | var config = require('./config.json'); 20 | 21 | var release_dir = __dirname + '/release/', 22 | version_dir = release_dir + config.version; 23 | 24 | grunt.initConfig({ 25 | obfuscator: { 26 | files: files, 27 | entry: 'app.js', 28 | out: 'release/app.js', 29 | strings: true, 30 | root: __dirname 31 | }, 32 | 33 | exec: { 34 | package: { 35 | command: function () { 36 | return [ 37 | util.format('mkdir -p %s', version_dir), 38 | util.format('mkdir -p %s/logs', version_dir), 39 | util.format('mkdir -p %s/pids', version_dir), 40 | util.format('cp %s/app.js %s', release_dir, version_dir), 41 | util.format('cp %s/config.json %s', __dirname, version_dir), 42 | util.format('cp %s/package.json %s', __dirname, version_dir), 43 | util.format('cp %s/genesisBlock.json %s', __dirname, version_dir), 44 | util.format('mkdir -p %s/sql/migrations', version_dir), 45 | util.format('cp %s/sql/*.sql %s/sql/', __dirname, version_dir), 46 | util.format('cp %s/sql/migrations/*.sql %s/sql/migrations/', __dirname, version_dir) 47 | ].join(' && '); 48 | } 49 | }, 50 | folder: { 51 | command: 'mkdir -p ' + release_dir 52 | }, 53 | build: { 54 | command: 'cd ' + version_dir + '/ && touch build && echo "v' + today + '" > build' 55 | } 56 | }, 57 | 58 | compress: { 59 | main: { 60 | options: { 61 | archive: version_dir + '.tar.gz', 62 | mode: 'tgz', 63 | level: 6 64 | }, 65 | files: [ 66 | { expand: true, cwd: release_dir, src: [config.version + '/**'], dest: './' } 67 | ] 68 | } 69 | }, 70 | 71 | jsdox: { 72 | generate: { 73 | src: [ 74 | 'helpers/*.js' 75 | // './modules/*.js' 76 | ], 77 | dest: 'tmp/docs', 78 | options: { 79 | templateDir: 'var/jsdox' 80 | } 81 | } 82 | }, 83 | 84 | jshint: { 85 | options: { 86 | jshintrc: true 87 | }, 88 | all: [ 89 | '*.js', 90 | 'helpers/**/*.js', 91 | 'modules/**/*.js', 92 | 'logic/**/*.js', 93 | 'schema/**/*.js', 94 | 'sql/**/*.js', 95 | 'tasks/**/*.js', 96 | 'test/*.js', 97 | 'test/api/**/*.js', 98 | 'test/unit/**/*.js' 99 | ] 100 | }, 101 | 102 | mochaTest: { 103 | test: { 104 | options: { 105 | reporter: 'spec', 106 | quiet: false, 107 | clearRequireCache: false, 108 | noFail: false, 109 | timeout: '250s' 110 | }, 111 | src: ['test'] 112 | } 113 | } 114 | }); 115 | 116 | grunt.loadTasks('tasks'); 117 | 118 | grunt.loadNpmTasks('grunt-obfuscator'); 119 | grunt.loadNpmTasks('grunt-jsdox'); 120 | grunt.loadNpmTasks('grunt-exec'); 121 | grunt.loadNpmTasks('grunt-contrib-compress'); 122 | grunt.loadNpmTasks('grunt-contrib-jshint'); 123 | grunt.loadNpmTasks('grunt-mocha-test'); 124 | 125 | grunt.registerTask('default', ['release']); 126 | grunt.registerTask('release', ['exec:folder', 'obfuscator', 'exec:package', 'exec:build', 'compress']); 127 | grunt.registerTask('travis', ['jshint', 'mochaTest']); 128 | }; 129 | -------------------------------------------------------------------------------- /test/api/peers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var node = require('./../node.js'); 4 | 5 | describe('GET /api/peers/version', function () { 6 | 7 | it('should be ok', function (done) { 8 | node.get('/api/peers/version', function (err, res) { 9 | node.expect(res.body).to.have.property('success').to.be.ok; 10 | node.expect(res.body).to.have.property('build').to.be.a('string'); 11 | node.expect(res.body).to.have.property('version').to.be.a('string'); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | 17 | describe('GET /api/peers', function () { 18 | 19 | it('using empty parameters should fail', function (done) { 20 | var params = [ 21 | 'state=', 22 | 'os=', 23 | 'shared=', 24 | 'version=', 25 | 'limit=', 26 | 'offset=', 27 | 'orderBy=' 28 | ]; 29 | 30 | node.get('/api/peers?' + params.join('&'), function (err, res) { 31 | node.expect(res.body).to.have.property('success').to.be.not.ok; 32 | node.expect(res.body).to.have.property('error'); 33 | done(); 34 | }); 35 | }); 36 | 37 | 38 | it('using limit > 100 should fail', function (done) { 39 | var limit = 101; 40 | var params = 'limit=' + limit; 41 | 42 | node.get('/api/peers?' + params, function (err, res) { 43 | node.expect(res.body).to.have.property('success').to.be.not.ok; 44 | node.expect(res.body).to.have.property('error'); 45 | done(); 46 | }); 47 | }); 48 | 49 | it('using invalid parameters should fail', function (done) { 50 | var params = [ 51 | 'state=invalid', 52 | 'os=invalid', 53 | 'shared=invalid', 54 | 'version=invalid', 55 | 'limit=invalid', 56 | 'offset=invalid', 57 | 'orderBy=invalid' 58 | ]; 59 | 60 | node.get('/api/peers?' + params.join('&'), function (err, res) { 61 | node.expect(res.body).to.have.property('success').to.be.not.ok; 62 | node.expect(res.body).to.have.property('error'); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('GET /api/peers/get', function () { 69 | 70 | var validParams; 71 | 72 | before(function (done) { 73 | node.addPeers(1, function (err, headers) { 74 | validParams = headers; 75 | done(); 76 | }); 77 | }); 78 | 79 | it('using known ip address with no port should fail', function (done) { 80 | node.get('/api/peers/get?ip=127.0.0.1', function (err, res) { 81 | node.expect(res.body).to.have.property('success').to.be.not.ok; 82 | node.expect(res.body).to.have.property('error').to.equal('Missing required property: port'); 83 | done(); 84 | }); 85 | }); 86 | 87 | it('using valid port with no ip address should fail', function (done) { 88 | node.get('/api/peers/get?port=' + validParams.port, function (err, res) { 89 | node.expect(res.body).to.have.property('success').to.be.not.ok; 90 | node.expect(res.body).to.have.property('error').to.equal('Missing required property: ip'); 91 | done(); 92 | }); 93 | }); 94 | 95 | it('using known ip address and port should be ok', function (done) { 96 | node.get('/api/peers/get?ip=127.0.0.1&port=4000', function (err, res) { 97 | node.expect(res.body).to.have.property('success').to.be.ok; 98 | node.expect(res.body).to.have.property('peer').to.be.an('object'); 99 | done(); 100 | }); 101 | }); 102 | 103 | it('using unknown ip address and port should fail', function (done) { 104 | node.get('/api/peers/get?ip=0.0.0.0&port=' + validParams.port, function (err, res) { 105 | node.expect(res.body).to.have.property('success').to.be.not.ok; 106 | node.expect(res.body).to.have.property('error').to.equal('Peer not found'); 107 | done(); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /sql/migrations/20171123154400_addIndexOnVote.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Fix slow query execution times by adding an index on mem_accounts2delegates ("dependentId") 3 | Before index fix: 4 | ``` 5 | ark_mainnet=> EXPLAIN (ANALYZE) UPDATE mem_accounts m 6 | SET vote = (SELECT COALESCE(SUM(b.balance), 0) AS vote 7 | FROM mem_accounts2delegates a, mem_accounts b 8 | WHERE a."accountId" = b.address AND a."dependentId" = encode(m."publicKey", 'hex')) 9 | WHERE m."isDelegate" = 1; 10 | Update on mem_accounts m (cost=0.00..571465.84 rows=601 width=345) (actual time=2801.314..2801.314 rows=0 loops=1) 11 | -> Seq Scan on mem_accounts m (cost=0.00..571465.84 rows=601 width=345) (actual time=8.935..2778.252 rows=594 loops=1) 12 | Filter: ("isDelegate" = 1) 13 | Rows Removed by Filter: 36990 14 | SubPlan 1 15 | -> Aggregate (cost=947.02..947.03 rows=1 width=8) (actual time=4.657..4.657 rows=1 loops=594) 16 | -> Nested Loop (cost=0.41..946.87 rows=61 width=8) (actual time=3.052..4.650 rows=23 loops=594) 17 | -> Seq Scan on mem_accounts2delegates a (cost=0.00..447.88 rows=61 width=35) (actual time=3.043..4.478 rows=23 loops=594) 18 | Filter: (("dependentId")::text = encode(m."publicKey", 'hex'::text)) 19 | Rows Removed by Filter: 13392 20 | -> Index Scan using mem_accounts_pkey on mem_accounts b (cost=0.41..8.17 rows=1 width=43) (actual time=0.007..0.007 rows=1 loops=13415) 21 | Index Cond: ((address)::text = (a."accountId")::text) 22 | Planning time: 0.639 ms 23 | Trigger protect_mem_accounts: time=13.702 calls=594 24 | Execution time: 2801.446 ms 25 | ``` 26 | After creating index fix: 27 | ``` 28 | ark_mainnet=> EXPLAIN (ANALYZE) UPDATE mem_accounts m 29 | SET vote = (SELECT COALESCE(SUM(b.balance), 0) AS vote 30 | FROM mem_accounts2delegates a, mem_accounts b 31 | WHERE a."accountId" = b.address AND a."dependentId" = encode(m."publicKey", 'hex')) 32 | WHERE m."isDelegate" = 1; 33 | Update on mem_accounts m (cost=0.00..387200.62 rows=601 width=345) (actual time=120.801..120.801 rows=0 loops=1) 34 | -> Seq Scan on mem_accounts m (cost=0.00..387200.62 rows=601 width=345) (actual time=0.100..106.916 rows=594 loops=1) 35 | Filter: ("isDelegate" = 1) 36 | Rows Removed by Filter: 36990 37 | SubPlan 1 38 | -> Aggregate (cost=640.42..640.43 rows=1 width=8) (actual time=0.164..0.164 rows=1 loops=594) 39 | -> Nested Loop (cost=5.29..640.27 rows=60 width=8) (actual time=0.017..0.160 rows=23 loops=594) 40 | -> Bitmap Heap Scan on mem_accounts2delegates a (cost=4.88..145.72 rows=60 width=35) (actual time=0.013..0.024 rows=23 loops=594) 41 | Recheck Cond: (("dependentId")::text = encode(m."publicKey", 'hex'::text)) 42 | Heap Blocks: exact=4125 43 | -> Bitmap Index Scan on "mem_accounts2delegates_dependentId_idx" (cost=0.00..4.86 rows=60 width=0) (actual time=0.011..0.011 rows=23 loops=594) 44 | Index Cond: (("dependentId")::text = encode(m."publicKey", 'hex'::text)) 45 | -> Index Scan using mem_accounts_pkey on mem_accounts b (cost=0.41..8.23 rows=1 width=43) (actual time=0.005..0.005 rows=1 loops=13415) 46 | Index Cond: ((address)::text = (a."accountId")::text) 47 | Planning time: 0.517 ms 48 | Trigger protect_mem_accounts: time=7.787 calls=594 49 | Execution time: 120.902 ms 50 | ``` 51 | */ 52 | 53 | BEGIN; 54 | 55 | CREATE INDEX IF NOT EXISTS "mem_accounts2delegates_dependentId" ON mem_accounts2delegates ("dependentId"); 56 | 57 | COMMIT; -------------------------------------------------------------------------------- /logic/transfer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../helpers/constants.js'); 4 | 5 | // Private fields 6 | var modules, library; 7 | 8 | // Constructor 9 | function Transfer () {} 10 | 11 | // Public methods 12 | // 13 | //__API__ `bind` 14 | 15 | // 16 | Transfer.prototype.bind = function (scope) { 17 | modules = scope.modules; 18 | library = scope.library; 19 | }; 20 | 21 | // 22 | //__API__ `create` 23 | 24 | // 25 | Transfer.prototype.create = function (data, trs) { 26 | trs.recipientId = data.recipientId; 27 | trs.amount = data.amount; 28 | 29 | return trs; 30 | }; 31 | 32 | // 33 | //__API__ `calculateFee` 34 | 35 | // 36 | Transfer.prototype.calculateFee = function (trs) { 37 | return constants.fees.send; 38 | }; 39 | 40 | // 41 | //__API__ `verify` 42 | 43 | // 44 | Transfer.prototype.verify = function (trs, sender, cb) { 45 | var isAddress = /^[1-9A-Za-z]{1,35}$/g; 46 | if (!trs.recipientId || !isAddress.test(trs.recipientId)) { 47 | return cb('Invalid recipient'); 48 | } 49 | 50 | if (trs.amount <= 0) { 51 | return cb('Invalid transaction amount'); 52 | } 53 | 54 | return cb(null, trs); 55 | }; 56 | 57 | // 58 | //__API__ `process` 59 | 60 | // 61 | Transfer.prototype.process = function (trs, sender, cb) { 62 | return cb(null, trs); 63 | }; 64 | 65 | // 66 | //__API__ `getBytes` 67 | 68 | // 69 | Transfer.prototype.getBytes = function (trs) { 70 | return null; 71 | }; 72 | 73 | // 74 | //__API__ `apply` 75 | 76 | // 77 | Transfer.prototype.apply = function (trs, block, sender, cb) { 78 | modules.accounts.setAccountAndGet({address: trs.recipientId}, function (err, recipient) { 79 | if (err) { 80 | return cb(err); 81 | } 82 | 83 | modules.accounts.mergeAccountAndGet({ 84 | address: trs.recipientId, 85 | balance: trs.amount, 86 | u_balance: trs.amount, 87 | blockId: block.id, 88 | round: modules.rounds.getRoundFromHeight(block.height) 89 | }, cb); 90 | }); 91 | }; 92 | 93 | // 94 | //__API__ `undo` 95 | 96 | // 97 | Transfer.prototype.undo = function (trs, block, sender, cb) { 98 | modules.accounts.setAccountAndGet({address: trs.recipientId}, function (err, recipient) { 99 | if (err) { 100 | return cb(err); 101 | } 102 | 103 | modules.accounts.mergeAccountAndGet({ 104 | address: trs.recipientId, 105 | balance: -trs.amount, 106 | u_balance: -trs.amount, 107 | blockId: block.id, 108 | round: modules.rounds.getRoundFromHeight(block.height) 109 | }, cb); 110 | }); 111 | }; 112 | 113 | // 114 | //__API__ `applyUnconfirmed` 115 | 116 | // 117 | Transfer.prototype.applyUnconfirmed = function (trs, sender, cb) { 118 | return cb(null, trs); 119 | }; 120 | 121 | // 122 | //__API__ `undoUnconfirmed` 123 | 124 | // 125 | Transfer.prototype.undoUnconfirmed = function (trs, sender, cb) { 126 | return cb(null, trs); 127 | }; 128 | 129 | // 130 | //__API__ `objectNormalize` 131 | 132 | // 133 | Transfer.prototype.objectNormalize = function (trs) { 134 | delete trs.blockId; 135 | return trs; 136 | }; 137 | 138 | // 139 | //__API__ `dbRead` 140 | 141 | // 142 | Transfer.prototype.dbRead = function (raw) { 143 | return null; 144 | }; 145 | 146 | // 147 | //__API__ `dbSave` 148 | 149 | // 150 | Transfer.prototype.dbSave = function (trs) { 151 | return null; 152 | }; 153 | 154 | // 155 | //__API__ `ready` 156 | 157 | // 158 | Transfer.prototype.ready = function (trs, sender) { 159 | if (Array.isArray(sender.multisignatures) && sender.multisignatures.length) { 160 | if (!Array.isArray(trs.signatures)) { 161 | return false; 162 | } 163 | return trs.signatures.length >= sender.multimin; 164 | } else { 165 | return true; 166 | } 167 | }; 168 | 169 | // Export 170 | module.exports = Transfer; 171 | -------------------------------------------------------------------------------- /sql/migrations/20160723182901_createViews.sql: -------------------------------------------------------------------------------- 1 | /* Create Views 2 | * 3 | */ 4 | 5 | BEGIN; 6 | 7 | DROP VIEW IF EXISTS blocks_list; 8 | 9 | CREATE VIEW blocks_list AS 10 | 11 | SELECT b."id" AS "b_id", 12 | b."version" AS "b_version", 13 | b."timestamp" AS "b_timestamp", 14 | b."height" AS "b_height", 15 | b."previousBlock" AS "b_previousBlock", 16 | b."numberOfTransactions" AS "b_numberOfTransactions", 17 | b."totalAmount" AS "b_totalAmount", 18 | b."totalFee" AS "b_totalFee", 19 | b."reward" AS "b_reward", 20 | b."payloadLength" AS "b_payloadLength", 21 | ENCODE(b."payloadHash", 'hex') AS "b_payloadHash", 22 | ENCODE(b."generatorPublicKey", 'hex') AS "b_generatorPublicKey", 23 | ENCODE(b."blockSignature", 'hex') AS "b_blockSignature", 24 | (SELECT MAX("height") + 1 FROM blocks) - b."height" AS "b_confirmations" 25 | 26 | FROM blocks b; 27 | 28 | DROP VIEW IF EXISTS full_blocks_list; 29 | 30 | CREATE VIEW full_blocks_list AS 31 | 32 | SELECT b."id" AS "b_id", 33 | b."version" AS "b_version", 34 | b."timestamp" AS "b_timestamp", 35 | b."height" AS "b_height", 36 | b."previousBlock" AS "b_previousBlock", 37 | b."numberOfTransactions" AS "b_numberOfTransactions", 38 | (b."totalAmount")::bigint AS "b_totalAmount", 39 | (b."totalFee")::bigint AS "b_totalFee", 40 | (b."reward")::bigint AS "b_reward", 41 | b."payloadLength" AS "b_payloadLength", 42 | ENCODE(b."payloadHash", 'hex') AS "b_payloadHash", 43 | ENCODE(b."generatorPublicKey", 'hex') AS "b_generatorPublicKey", 44 | ENCODE(b."blockSignature", 'hex') AS "b_blockSignature", 45 | t."id" AS "t_id", 46 | t."rowId" AS "t_rowId", 47 | t."type" AS "t_type", 48 | t."timestamp" AS "t_timestamp", 49 | ENCODE(t."senderPublicKey", 'hex') AS "t_senderPublicKey", 50 | t."senderId" AS "t_senderId", 51 | t."recipientId" AS "t_recipientId", 52 | t."vendorField" AS "t_vendorField", 53 | (t."amount")::bigint AS "t_amount", 54 | (t."fee")::bigint AS "t_fee", 55 | ENCODE(t."signature", 'hex') AS "t_signature", 56 | ENCODE(t."signSignature", 'hex') AS "t_signSignature", 57 | ENCODE(s."publicKey", 'hex') AS "s_publicKey", 58 | d."username" AS "d_username", 59 | v."votes" AS "v_votes", 60 | m."min" AS "m_min", 61 | m."lifetime" AS "m_lifetime", 62 | m."keysgroup" AS "m_keysgroup", 63 | ENCODE(t."requesterPublicKey", 'hex') AS "t_requesterPublicKey", 64 | t."signatures" AS "t_signatures" 65 | 66 | FROM blocks b 67 | 68 | LEFT OUTER JOIN transactions AS t ON t."blockId" = b."id" 69 | LEFT OUTER JOIN delegates AS d ON d."transactionId" = t."id" 70 | LEFT OUTER JOIN votes AS v ON v."transactionId" = t."id" 71 | LEFT OUTER JOIN signatures AS s ON s."transactionId" = t."id" 72 | LEFT OUTER JOIN multisignatures AS m ON m."transactionId" = t."id"; 73 | 74 | DROP VIEW IF EXISTS trs_list; 75 | 76 | CREATE VIEW trs_list AS 77 | 78 | SELECT t."id" AS "t_id", 79 | b."height" AS "b_height", 80 | t."blockId" AS "t_blockId", 81 | t."type" AS "t_type", 82 | t."timestamp" AS "t_timestamp", 83 | ENCODE(t."senderPublicKey", 'hex') AS "t_senderPublicKey", 84 | t."senderId" AS "t_senderId", 85 | t."recipientId" AS "t_recipientId", 86 | t."amount" AS "t_amount", 87 | t."fee" AS "t_fee", 88 | t."vendorField" AS "t_vendorField", 89 | ENCODE(t."signature", 'hex') AS "t_signature", 90 | ENCODE(t."signSignature", 'hex') AS "t_SignSignature", 91 | t."signatures" AS "t_signatures", 92 | (SELECT MAX("height") + 1 FROM blocks) - b."height" AS "confirmations" 93 | 94 | FROM transactions t 95 | 96 | INNER JOIN blocks b ON t."blockId" = b."id"; 97 | 98 | COMMIT; 99 | -------------------------------------------------------------------------------- /test/api/peer.transactions.stress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var node = require('./../node.js'); 4 | 5 | function postTransaction (transaction, done) { 6 | node.post('/peer/transactions', { 7 | transactions: [transaction] 8 | }, done); 9 | 10 | 11 | } 12 | 13 | function postTransactions (transactions, done) { 14 | node.post('/peer/transactions', { 15 | transactions: transactions 16 | }, function(err, res){ 17 | node.onNewBlock(function (err) { 18 | return done(err, res); 19 | }); 20 | }); 21 | } 22 | 23 | describe('POST /peer/transactions', function () { 24 | 25 | // describe('sending 1000 bundled transfers to random addresses', function () { 26 | // 27 | // var transactions = []; 28 | // var maximum = 1000; 29 | // var count = 1; 30 | // 31 | // before(function (done) { 32 | // node.async.doUntil(function (next) { 33 | // var bundled = []; 34 | // 35 | // for (var i = 0; i < 100; i++) { 36 | // var transaction = node.ark.transaction.createTransaction( 37 | // node.randomAccount().address, 38 | // node.randomNumber(100000000, 1000000000), 39 | // "stress test", 40 | // node.gAccount.password 41 | // ); 42 | // 43 | // transactions.push(transaction); 44 | // bundled.push(transaction); 45 | // count++; 46 | // } 47 | // 48 | // postTransactions(bundled, function (err, res) { 49 | // node.expect(res.body).to.have.property('success').to.be.ok; 50 | // next(); 51 | // }); 52 | // }, function () { 53 | // return (count >= maximum); 54 | // }, function (err) { 55 | // done(err); 56 | // }); 57 | // }); 58 | // 59 | // it('should confirm all transactions', function (done) { 60 | // var blocksToWait = maximum / node.constants.maxTxsPerBlock + 1; 61 | // node.waitForBlocks(blocksToWait, function (err) { 62 | // node.async.eachSeries(transactions, function (transaction, eachSeriesCb) { 63 | // node.get('/api/transactions/get?id=' + transaction.id, function (err, res) { 64 | // node.expect(res.body).to.have.property('success').to.be.ok; 65 | // node.expect(res.body).to.have.property('transaction').that.is.an('object'); 66 | // return setImmediate(eachSeriesCb); 67 | // }); 68 | // }, done); 69 | // }); 70 | // }).timeout(500000); 71 | // }); 72 | 73 | describe('sending 1000 single transfers to random addresses', function () { 74 | 75 | var transactions = []; 76 | var maximum = 1000; 77 | var count = 1; 78 | 79 | before(function (done) { 80 | node.async.doUntil(function (next) { 81 | var transaction = node.ark.transaction.createTransaction( 82 | node.randomAccount().address, 83 | node.randomNumber(100000000, 1000000000), 84 | "stress test", 85 | node.gAccount.password 86 | ); 87 | 88 | postTransaction(transaction, function (err, res) { 89 | node.expect(res.body).to.have.property('success').to.be.ok; 90 | node.expect(res.body).to.have.property('transactionId').to.equal(transaction.id); 91 | transactions.push(transaction); 92 | count++; 93 | next(); 94 | }); 95 | }, function () { 96 | return (count >= maximum); 97 | }, function (err) { 98 | done(err); 99 | }); 100 | }); 101 | 102 | it('should confirm all transactions', function (done) { 103 | var blocksToWait = maximum / node.constants.maxTxsPerBlock + 1; 104 | node.waitForBlocks(blocksToWait, function (err) { 105 | node.async.eachSeries(transactions, function (transaction, eachSeriesCb) { 106 | node.get('/api/transactions/get?id=' + transaction.id, function (err, res) { 107 | node.expect(res.body).to.have.property('success').to.be.ok; 108 | node.expect(res.body).to.have.property('transaction').that.is.an('object'); 109 | return setImmediate(eachSeriesCb); 110 | }); 111 | }, done); 112 | }); 113 | }).timeout(500000); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /sql/blocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BlocksSql = { 4 | sortFields: [ 5 | 'id', 6 | 'timestamp', 7 | 'height', 8 | 'previousBlock', 9 | 'totalAmount', 10 | 'totalFee', 11 | 'reward', 12 | 'numberOfTransactions', 13 | 'generatorPublicKey' 14 | ], 15 | 16 | getGenesisBlockId: 'SELECT "id" FROM blocks WHERE "id" = ${id}', 17 | 18 | deleteBlock: 'DELETE FROM blocks WHERE "id" = ${id};', 19 | 20 | countList: function (params) { 21 | return [ 22 | 'SELECT COUNT("b_id")::int FROM blocks_list', 23 | (params.where.length ? 'WHERE ' + params.where.join(' AND ') : '') 24 | ].filter(Boolean).join(' '); 25 | }, 26 | 27 | list: function (params) { 28 | return [ 29 | 'SELECT * FROM blocks_list', 30 | (params.where.length ? 'WHERE ' + params.where.join(' AND ') : ''), 31 | (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') : ''), 32 | 'LIMIT ${limit} OFFSET ${offset}' 33 | ].filter(Boolean).join(' '); 34 | }, 35 | 36 | getById: 'SELECT * FROM blocks_list WHERE "b_id" = ${id}', 37 | 38 | getIdSequence: 'SELECT (ARRAY_AGG("id" ORDER BY "height" ASC))[1] AS "id", MIN("height") AS "height", CAST("height" / ${delegates} AS INTEGER) + (CASE WHEN "height" % ${activeDelegates} > 0 THEN 1 ELSE 0 END) AS "round" FROM blocks WHERE "height" <= ${height} GROUP BY "round" ORDER BY "height" DESC LIMIT ${limit}', 39 | 40 | getCommonBlock: function (params) { 41 | return [ 42 | 'SELECT COUNT("id")::int FROM blocks WHERE "id" = ${id}', 43 | (params.previousBlock ? 'AND "previousBlock" = ${previousBlock}' : ''), 44 | 'AND "height" = ${height}' 45 | ].filter(Boolean).join(' '); 46 | }, 47 | 48 | countByRowId: 'SELECT COUNT("rowId")::int FROM blocks', 49 | 50 | getHeightByLastId: 'SELECT "height" FROM blocks WHERE "id" = ${lastId}', 51 | 52 | loadBlocksData: function (params) { 53 | var limitPart; 54 | 55 | if (!params.id && !params.lastId) { 56 | limitPart = 'WHERE height < ${limit}'; 57 | } 58 | 59 | return [ 60 | 'SELECT id, version, height, timestamp, "previousBlock", "numberOfTransactions" ,"totalAmount", "totalFee", reward, "payloadLength", encode("payloadHash", \'hex\') as "payloadHash", encode("generatorPublicKey", \'hex\') as "generatorPublicKey", encode("blockSignature", \'hex\') as "blockSignature", rawtxs::json as transactions from blocks', 61 | limitPart, 62 | (params.id || params.lastId ? 'WHERE' : ''), 63 | (params.id ? 'id = ${id}' : ''), 64 | (params.id && params.lastId ? ' AND ' : ''), 65 | (params.lastId ? 'height > ${height} AND height < ${limit}' : ''), 66 | 'ORDER BY height' 67 | ].filter(Boolean).join(' '); 68 | }, 69 | 70 | loadBlocksOffset: 'SELECT id, version, height, timestamp, "previousBlock", "numberOfTransactions" ,"totalAmount", "totalFee", reward, "payloadLength", encode("payloadHash", \'hex\') as "payloadHash", encode("generatorPublicKey", \'hex\') as "generatorPublicKey", encode("blockSignature", \'hex\') as "blockSignature", rawtxs::json as transactions from blocks WHERE height >= ${offset} AND height < ${limit} ORDER BY height', 71 | 72 | loadLastBlock: 'SELECT id, version, height, timestamp, "previousBlock", "numberOfTransactions" ,"totalAmount", "totalFee", reward, "payloadLength", encode("payloadHash", \'hex\') as "payloadHash", encode("generatorPublicKey", \'hex\') as "generatorPublicKey", encode("blockSignature", \'hex\') as "blockSignature", rawtxs::json as transactions from blocks WHERE height = (SELECT MAX("height") FROM blocks)', 73 | 74 | getBlockId: 'SELECT "id" FROM blocks WHERE "id" = ${id}', 75 | 76 | getBlockById: 'SELECT id, version, height, timestamp, "previousBlock", "numberOfTransactions" ,"totalAmount", "totalFee", reward, "payloadLength", encode("payloadHash", \'hex\') as "payloadHash", encode("generatorPublicKey", \'hex\') as "generatorPublicKey", encode("blockSignature", \'hex\') as "blockSignature", rawtxs::json as transactions from blocks WHERE id = ${id}', 77 | 78 | getTransactionId: 'SELECT "id" FROM transactions WHERE "id" = ${id}', 79 | 80 | simpleDeleteAfterBlock: 'DELETE FROM blocks WHERE "height" >= (SELECT "height" FROM blocks WHERE "id" = ${id});' 81 | }; 82 | 83 | module.exports = BlocksSql; 84 | -------------------------------------------------------------------------------- /helpers/database.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var bignum = require('./bignum'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | // var isWin = /^win/.test(process.platform); 9 | // var isMac = /^darwin/.test(process.platform); 10 | 11 | function Migrator (pgp, db) { 12 | this.checkMigrations = function (waterCb) { 13 | db.one('SELECT to_regclass(\'migrations\')').then(function (row) { 14 | return waterCb(null, Boolean(row.to_regclass)); 15 | }).catch(function (err) { 16 | return waterCb(err); 17 | }); 18 | }; 19 | 20 | this.getLastMigration = function (hasMigrations, waterCb) { 21 | if (!hasMigrations) { 22 | return waterCb(null, null); 23 | } 24 | db.query('SELECT * FROM migrations ORDER BY "id" DESC LIMIT 1').then(function (rows) { 25 | if (rows[0]) { 26 | rows[0].id = bignum(rows[0].id); 27 | } 28 | return waterCb(null, rows[0]); 29 | }).catch(function (err) { 30 | return waterCb(err); 31 | }); 32 | }; 33 | 34 | this.readPendingMigrations = function (lastMigration, waterCb) { 35 | var migrationsPath = path.join(__dirname + '/../sql/migrations'); 36 | var pendingMigrations = []; 37 | 38 | function matchMigrationName (file) { 39 | var name = file.match(/_.+\.sql$/); 40 | 41 | return Array.isArray(name) ? name[0].replace(/_/, '').replace(/\.sql$/, '') : null; 42 | } 43 | 44 | function matchMigrationId (file) { 45 | var id = file.match(/^[0-9]+/); 46 | 47 | return Array.isArray(id) ? bignum(id[0]): null; 48 | } 49 | 50 | fs.readdir(migrationsPath, function (err, files) { 51 | if (err) { 52 | return waterCb(err); 53 | } 54 | 55 | files.map(function (file) { 56 | return { 57 | id: matchMigrationId(file), 58 | name: matchMigrationName(file), 59 | path: path.join(migrationsPath, file) 60 | }; 61 | }).filter(function (file) { 62 | return ( 63 | (file.id && file.name) && fs.statSync(file.path).isFile() && /\.sql$/.test(file.path) 64 | ); 65 | }).forEach(function (file) { 66 | if (!lastMigration || file.id.greaterThan(lastMigration.id)) { 67 | pendingMigrations.push(file); 68 | } 69 | }); 70 | 71 | return waterCb(null, pendingMigrations); 72 | }); 73 | }; 74 | 75 | this.applyPendingMigrations = function (pendingMigrations, waterCb) { 76 | var appliedMigrations = []; 77 | 78 | async.eachSeries(pendingMigrations, function (file, eachCb) { 79 | var sql = new pgp.QueryFile(file.path, {minify: true}); 80 | 81 | db.query(sql).then(function () { 82 | appliedMigrations.push(file); 83 | return eachCb(); 84 | }).catch(function (err) { 85 | return eachCb(err); 86 | }); 87 | }, function (err) { 88 | return waterCb(err, appliedMigrations); 89 | }); 90 | }; 91 | 92 | this.insertAppliedMigrations = function (appliedMigrations, waterCb) { 93 | async.eachSeries(appliedMigrations, function (file, eachCb) { 94 | db.query('INSERT INTO migrations(id, name) VALUES($1, $2) ON CONFLICT DO NOTHING', [file.id.toString(), file.name]).then(function () { 95 | return eachCb(); 96 | }).catch(function (err) { 97 | return eachCb(err); 98 | }); 99 | }, function (err) { 100 | return waterCb(err); 101 | }); 102 | }; 103 | 104 | this.applyRuntimeQueryFile = function (waterCb) { 105 | var sql = new pgp.QueryFile(path.join(__dirname + '/../sql/runtime.sql'), {minify: true}); 106 | 107 | db.query(sql).then(function () { 108 | return waterCb(); 109 | }).catch(function (err) { 110 | return waterCb(err); 111 | }); 112 | }; 113 | } 114 | 115 | module.exports.connect = function (config, logger, cb) { 116 | var pgOptions = { 117 | pgNative: true 118 | }; 119 | 120 | var pgp = require('pg-promise')(pgOptions); 121 | var monitor = require('pg-monitor'); 122 | 123 | monitor.attach(pgOptions, config.logEvents); 124 | monitor.setTheme('matrix'); 125 | 126 | monitor.log = function(msg, info){ 127 | logger.log(info.event, info.text); 128 | info.display = false; 129 | }; 130 | 131 | config.user = config.user || process.env.USER; 132 | 133 | var db = pgp(config); 134 | var migrator = new Migrator(pgp, db); 135 | 136 | async.waterfall([ 137 | migrator.checkMigrations, 138 | migrator.getLastMigration, 139 | migrator.readPendingMigrations, 140 | migrator.applyPendingMigrations, 141 | migrator.insertAppliedMigrations, 142 | migrator.applyRuntimeQueryFile 143 | ], function (err) { 144 | return cb(err, db); 145 | }); 146 | }; 147 | -------------------------------------------------------------------------------- /test/unit/helpers/request-limiter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var chai = require('chai'); 4 | var expect = require('chai').expect; 5 | var express = require('express'); 6 | 7 | var RequestLimiter = require('../../../helpers/request-limiter.js'); 8 | 9 | describe('RequestLimiter', function () { 10 | 11 | var app; 12 | 13 | beforeEach(function () { 14 | app = express(); 15 | }); 16 | 17 | describe('when config.trustProxy is undefined', function () { 18 | 19 | it('should not enable trust proxy', function () { 20 | RequestLimiter(app, {}); 21 | expect(app.enabled('trust proxy')).to.be.false; 22 | }); 23 | }); 24 | 25 | describe('when config.trustProxy is == false', function () { 26 | 27 | it('should not enable trust proxy', function () { 28 | RequestLimiter(app, { trustProxy: false }); 29 | expect(app.enabled('trust proxy')).to.be.false; 30 | }); 31 | }); 32 | 33 | describe('when config.trustProxy is == true', function () { 34 | 35 | it('should enable trust proxy', function () { 36 | RequestLimiter(app, { trustProxy: true }); 37 | expect(app.enabled('trust proxy')).to.be.true; 38 | }); 39 | }); 40 | 41 | describe('when limits are undefined', function () { 42 | 43 | var limiter; 44 | 45 | beforeEach(function () { 46 | limiter = RequestLimiter(app, {}); 47 | }); 48 | 49 | it('should return the default client limits', function () { 50 | expect(limiter).to.be.a('object').that.has.property('client').that.is.a('object'); 51 | expect(limiter.client).to.have.property('delayAfter').to.eql(0); 52 | expect(limiter.client).to.have.property('delayAfter').to.eql(0); 53 | expect(limiter.client).to.have.property('delayMs').to.eql(0); 54 | expect(limiter.client).to.have.property('max').to.eql(0); 55 | expect(limiter.client).to.have.property('windowMs').to.eql(60000); 56 | }); 57 | 58 | it('should return the default peer limits', function () { 59 | expect(limiter).to.be.a('object').that.has.property('peer').that.is.a('object'); 60 | expect(limiter.peer).to.have.property('delayAfter').to.eql(0); 61 | expect(limiter.peer).to.have.property('delayAfter').to.eql(0); 62 | expect(limiter.peer).to.have.property('delayMs').to.eql(0); 63 | expect(limiter.peer).to.have.property('max').to.eql(0); 64 | expect(limiter.peer).to.have.property('windowMs').to.eql(60000); 65 | }); 66 | 67 | it('should enable the client middleware', function () { 68 | expect(limiter).to.be.a('object').that.has.property('middleware').that.is.a('object'); 69 | expect(limiter.middleware).to.have.property('client').that.is.a('function'); 70 | }); 71 | 72 | it('should enable the peer middleware', function () { 73 | expect(limiter).to.be.a('object').that.has.property('middleware').that.is.a('object'); 74 | expect(limiter.middleware).to.have.property('peer').that.is.a('function'); 75 | }); 76 | }); 77 | 78 | describe('when limits are defined', function () { 79 | 80 | var limits, options, limiter; 81 | 82 | beforeEach(function () { 83 | limits = { 84 | max: 1, 85 | delayMs: 2, 86 | delayAfter: 3, 87 | windowMs: 4 88 | }; 89 | options = { options: { limits: limits } }; 90 | limiter = RequestLimiter(app, { api: options, peers: options }); 91 | }); 92 | 93 | it('should return the defined client limits', function () { 94 | expect(limiter).to.be.a('object').that.has.property('client').that.is.a('object'); 95 | expect(limiter.client).to.have.property('max').to.eql(1); 96 | expect(limiter.client).to.have.property('delayMs').to.eql(2); 97 | expect(limiter.client).to.have.property('delayAfter').to.eql(3); 98 | expect(limiter.client).to.have.property('windowMs').to.eql(4); 99 | }); 100 | 101 | it('should return the defined peer limits', function () { 102 | expect(limiter).to.be.a('object').that.has.property('peer').that.is.a('object'); 103 | expect(limiter.peer).to.have.property('max').to.eql(1); 104 | expect(limiter.peer).to.have.property('delayMs').to.eql(2); 105 | expect(limiter.peer).to.have.property('delayAfter').to.eql(3); 106 | expect(limiter.peer).to.have.property('windowMs').to.eql(4); 107 | }); 108 | 109 | it('should enable the client middleware', function () { 110 | expect(limiter).to.be.a('object').that.has.property('middleware').that.is.a('object'); 111 | expect(limiter.middleware).to.have.property('client').that.is.a('function'); 112 | }); 113 | 114 | it('should enable the peer middleware', function () { 115 | expect(limiter).to.be.a('object').that.has.property('middleware').that.is.a('object'); 116 | expect(limiter.middleware).to.have.property('peer').that.is.a('function'); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /sql/migrations/20160723182900_createSchema.sql: -------------------------------------------------------------------------------- 1 | /* Create Schema 2 | * 3 | */ 4 | 5 | BEGIN; 6 | 7 | /* Tables */ 8 | CREATE TABLE IF NOT EXISTS "migrations"( 9 | "id" VARCHAR(22) NOT NULL PRIMARY KEY, 10 | "name" TEXT NOT NULL 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS "blocks"( 14 | "id" VARCHAR(64) PRIMARY KEY, 15 | "rowId" SERIAL NOT NULL, 16 | "version" INT NOT NULL, 17 | "timestamp" INT NOT NULL, 18 | "height" INT NOT NULL, 19 | "previousBlock" VARCHAR(64), 20 | "numberOfTransactions" INT NOT NULL, 21 | "totalAmount" BIGINT NOT NULL, 22 | "totalFee" BIGINT NOT NULL, 23 | "reward" BIGINT NOT NULL, 24 | "payloadLength" INT NOT NULL, 25 | "payloadHash" bytea NOT NULL, 26 | "generatorPublicKey" bytea NOT NULL, 27 | "blockSignature" bytea NOT NULL, 28 | "rawtxs" TEXT NOT NULL, 29 | FOREIGN KEY("previousBlock") 30 | REFERENCES "blocks"("id") ON DELETE SET NULL 31 | ); 32 | 33 | CREATE TABLE IF NOT EXISTS "transactions"( 34 | "id" VARCHAR(64) PRIMARY KEY, 35 | "rowId" SERIAL NOT NULL, 36 | "blockId" VARCHAR(20) NOT NULL, 37 | "type" SMALLINT NOT NULL, 38 | "timestamp" INT NOT NULL, 39 | "senderPublicKey" bytea NOT NULL, 40 | "senderId" VARCHAR(36) NOT NULL, 41 | "recipientId" VARCHAR(36), 42 | "amount" BIGINT NOT NULL, 43 | "fee" BIGINT NOT NULL, 44 | "signature" bytea NOT NULL, 45 | "signSignature" bytea, 46 | "requesterPublicKey" bytea, 47 | "vendorField" VARCHAR(64), 48 | "signatures" TEXT, 49 | "rawasset" TEXT, 50 | FOREIGN KEY("blockId") REFERENCES "blocks"("id") ON DELETE CASCADE 51 | ); 52 | 53 | CREATE TABLE IF NOT EXISTS "signatures"( 54 | "transactionId" VARCHAR(64) NOT NULL PRIMARY KEY, 55 | "publicKey" bytea NOT NULL, 56 | FOREIGN KEY("transactionId") REFERENCES transactions(id) ON DELETE CASCADE 57 | ); 58 | 59 | CREATE TABLE IF NOT EXISTS "delegates"( 60 | "username" VARCHAR(20) NOT NULL, 61 | "transactionId" VARCHAR(64) NOT NULL, 62 | FOREIGN KEY("transactionId") REFERENCES "transactions"("id") ON DELETE CASCADE 63 | ); 64 | 65 | CREATE TABLE IF NOT EXISTS "votes"( 66 | "votes" TEXT, 67 | "transactionId" VARCHAR(64) NOT NULL, 68 | FOREIGN KEY("transactionId") REFERENCES "transactions"("id") ON DELETE CASCADE 69 | ); 70 | 71 | CREATE TABLE IF NOT EXISTS "forks_stat"( 72 | "delegatePublicKey" bytea NOT NULL, 73 | "blockTimestamp" INT NOT NULL, 74 | "blockId" VARCHAR(64) NOT NULL, 75 | "blockHeight" INT NOT NULL, 76 | "previousBlock" VARCHAR(64) NOT NULL, 77 | "cause" INT NOT NULL 78 | ); 79 | 80 | CREATE TABLE IF NOT EXISTS "multisignatures"( 81 | "min" INT NOT NULL, 82 | "lifetime" INT NOT NULL, 83 | "keysgroup" TEXT NOT NULL, 84 | "transactionId" VARCHAR(64) NOT NULL, 85 | FOREIGN KEY("transactionId") REFERENCES "transactions"("id") ON DELETE CASCADE 86 | ); 87 | 88 | CREATE TABLE IF NOT EXISTS "peers"( 89 | "id" SERIAL NOT NULL PRIMARY KEY, 90 | "ip" INET NOT NULL, 91 | "port" SMALLINT NOT NULL, 92 | "state" SMALLINT NOT NULL, 93 | "os" VARCHAR(64), 94 | "version" VARCHAR(11), 95 | "clock" BIGINT 96 | ); 97 | 98 | /* Unique Indexes */ 99 | CREATE UNIQUE INDEX IF NOT EXISTS "blocks_height" ON "blocks"("height"); 100 | CREATE UNIQUE INDEX IF NOT EXISTS "blocks_previousBlock" ON "blocks"("previousBlock"); 101 | CREATE UNIQUE INDEX IF NOT EXISTS "peers_unique" ON "peers"("ip", "port"); 102 | 103 | /* Indexes */ 104 | CREATE INDEX IF NOT EXISTS "blocks_rowId" ON "blocks"("rowId"); 105 | CREATE INDEX IF NOT EXISTS "blocks_generator_public_key" ON "blocks"("generatorPublicKey"); 106 | CREATE INDEX IF NOT EXISTS "blocks_reward" ON "blocks"("reward"); 107 | CREATE INDEX IF NOT EXISTS "blocks_totalFee" ON "blocks"("totalFee"); 108 | CREATE INDEX IF NOT EXISTS "blocks_totalAmount" ON "blocks"("totalAmount"); 109 | CREATE INDEX IF NOT EXISTS "blocks_numberOfTransactions" ON "blocks"("numberOfTransactions"); 110 | CREATE INDEX IF NOT EXISTS "blocks_timestamp" ON "blocks"("timestamp"); 111 | CREATE INDEX IF NOT EXISTS "transactions_rowId" ON "transactions"("rowId"); 112 | CREATE INDEX IF NOT EXISTS "transactions_block_id" ON "transactions"("blockId"); 113 | CREATE INDEX IF NOT EXISTS "transactions_sender_id" ON "transactions"("senderId"); 114 | CREATE INDEX IF NOT EXISTS "transactions_recipient_id" ON "transactions"("recipientId"); 115 | CREATE INDEX IF NOT EXISTS "transactions_senderPublicKey" ON "transactions"("senderPublicKey"); 116 | CREATE INDEX IF NOT EXISTS "transactions_type" ON "transactions"("type"); 117 | CREATE INDEX IF NOT EXISTS "transactions_timestamp" ON "transactions"("timestamp"); 118 | CREATE INDEX IF NOT EXISTS "signatures_transactions_id" ON "signatures"("transactionId"); 119 | CREATE INDEX IF NOT EXISTS "votes_transactions_id" ON "votes"("transactionId"); 120 | CREATE INDEX IF NOT EXISTS "delegates_transactions_id" ON "delegates"("transactionId"); 121 | CREATE INDEX IF NOT EXISTS "multisignatures_transactions_id" ON "multisignatures"("transactionId"); 122 | 123 | COMMIT; 124 | -------------------------------------------------------------------------------- /helpers/validator/field.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Field; 4 | 5 | /** 6 | * 7 | * @param {Validator} validator Validator instance 8 | * @param {string} path Validation field path 9 | * @param {*} value Validated value 10 | * @param {object} rules Set of rules 11 | * @param {*} thisArg Value used as this reference within rule callback calls. 12 | * @constructor 13 | */ 14 | function Field (validator, path, value, rules, thisArg) { 15 | this.isAsync = false; 16 | this.hasError = false; 17 | this.rules = rules; 18 | this.value = value; 19 | this.report = []; 20 | this.path = path||[]; 21 | this.thisArg = thisArg||null; 22 | this._stack = Object.keys(rules); 23 | this.validator = validator; 24 | this.inProgress = false; 25 | } 26 | 27 | /** 28 | * Create child field. 29 | * @param {string} path Validation field path 30 | * @param {*} value Validated value 31 | * @param {object} rules Set of rules 32 | * @param {*} thisArg Value used as this reference within rule callback calls. 33 | * @returns {Validator.Field} 34 | */ 35 | // 36 | //__API__ `child` 37 | 38 | // 39 | Field.prototype.child = function (path, value, rules, thisArg) { 40 | var field = this.validator.createField(this.path.concat(path), value, rules, thisArg); 41 | field.report = this.report; 42 | return field; 43 | }; 44 | 45 | /** 46 | * Validate field value and trigger callback on result 47 | * @param callback 48 | */ 49 | // 50 | //__API__ `validate` 51 | 52 | // 53 | Field.prototype.validate = function(callback) { 54 | var stack = this._stack; 55 | // TODO copy value 56 | var report = this.report; 57 | var thisArg = this.thisArg; 58 | this.inProgress = true; 59 | 60 | if (typeof callback === 'function') { 61 | this.callback = callback; 62 | this.hasCallback = true; 63 | } else { 64 | this.callback = null; 65 | this.hasCallback = false; 66 | } 67 | 68 | var descriptor, result, accept, value; 69 | while (stack.length) { 70 | var rule = stack.shift(); 71 | value = this.value; 72 | accept = this.rules[rule]; 73 | 74 | try { 75 | if (!this.validator.hasRule(rule) && !this.validator.skipMissed) { 76 | throw new Error('Rule "' + rule + '" not found for "' + this.path.join('.') + '".'); 77 | } 78 | 79 | descriptor = this.validator.getRule(rule); 80 | 81 | if (this.validator.execRules && typeof accept === 'function') { 82 | accept = accept.call(thisArg, value); 83 | } 84 | 85 | if (descriptor.accept) { 86 | accept = descriptor.accept.call(thisArg, accept, value, this); 87 | } 88 | 89 | if (descriptor.filter) { 90 | value = this.value = descriptor.filter.call(thisArg, accept, value, this); 91 | } 92 | 93 | if (descriptor.validate) { 94 | result = descriptor.validate.call(thisArg, accept, value, this); 95 | } 96 | 97 | if (this.isAsync) { return; } 98 | 99 | if (result === false) { 100 | report.push({ 101 | path : this.path, 102 | rule : rule, 103 | accept : accept 104 | }); 105 | 106 | this.hasError = true; 107 | stack.length = 0; 108 | } 109 | } catch (err) { 110 | if (!err.field) { 111 | Object.defineProperty(err, 'field', { 112 | enumerable : false, 113 | value : this 114 | }); 115 | } 116 | this.validator.onError(this, err); 117 | this.end(err, report, value); 118 | return; 119 | } 120 | } 121 | 122 | this.inProgress = false; 123 | 124 | if (!stack.length) { 125 | this.end(null, report, value); 126 | } 127 | }; 128 | 129 | /** 130 | * End validation. Drop validation stack. 131 | * @param {Error} err Report and error if passed. Optional 132 | */ 133 | // 134 | //__API__ `end` 135 | 136 | // 137 | Field.prototype.end = function(err) { 138 | this._stack = []; 139 | 140 | if (this.hasError) { 141 | this.validator.onInvalid(this); 142 | } else { 143 | this.validator.onValid(this); 144 | } 145 | 146 | if (this.hasCallback) { 147 | this.callback(err, this.report, this.value); 148 | this.callback = null; 149 | } 150 | }; 151 | 152 | /** 153 | * Create validation async. Callback get done function to emit validation end. 154 | * @param {function(done:function)} callback 155 | */ 156 | // 157 | //__API__ `async` 158 | 159 | // 160 | Field.prototype.async = function(callback) { 161 | this.isAsync = true; 162 | var self = this; 163 | callback(function(err){ 164 | if (arguments.length > 1) { 165 | self.value = arguments[1]; 166 | } 167 | 168 | self.isAsync = false; 169 | 170 | if (err) { 171 | if (!err.hasOwnProperty('field')) { 172 | Object.defineProperty(err, 'field', { 173 | enumerable : false, 174 | value : self 175 | }); 176 | self.validator.onError(self, err); 177 | } 178 | self.end(err); 179 | } else if (!self.inProgress) { 180 | self.validate(self.callback); 181 | } 182 | }); 183 | }; 184 | 185 | /** 186 | * Report an invalid validation result 187 | * @param {{}} report Validation report object 188 | */ 189 | // 190 | //__API__ `issue` 191 | 192 | // 193 | Field.prototype.issue = function(report){ 194 | this.hasError = true; 195 | report.path = this.path.concat(report.path); 196 | this.report.push(report); 197 | }; 198 | -------------------------------------------------------------------------------- /test/api/peer.transactions.signatures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var crypto = require('crypto'); 4 | var node = require('./../node.js'); 5 | 6 | var account = node.randomAccount(); 7 | var account2 = node.randomAccount(); 8 | var account3 = node.randomAccount(); 9 | 10 | function postTransaction (transaction, done) { 11 | node.post('/peer/transactions', { 12 | transactions: [transaction] 13 | }, function (err, res) { 14 | done(err, res); 15 | }); 16 | } 17 | 18 | function sendArk (params, done) { 19 | var transaction = node.ark.transaction.createTransaction(params.recipientId, params.amount, null, params.secret); 20 | 21 | postTransaction(transaction, function (err, res) { 22 | node.expect(res.body).to.have.property('success').to.be.ok; 23 | node.onNewBlock(function (err) { 24 | done(err, res); 25 | }); 26 | }); 27 | } 28 | 29 | describe('POST /peer/transactions', function () { 30 | 31 | 32 | describe('enabling second signature', function () { 33 | 34 | it('using undefined transaction', function (done) { 35 | postTransaction(undefined, function (err, res) { 36 | node.expect(res.body).to.have.property('success').to.be.not.ok; 37 | node.expect(res.body).to.have.property('error').to.equal("TypeError: Cannot read property 'type' of null"); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('using undefined transaction.asset', function (done) { 43 | var transaction = node.ark.signature.createSignature(node.randomPassword(), node.randomPassword()); 44 | 45 | delete transaction.asset; 46 | 47 | postTransaction(transaction, function (err, res) { 48 | node.expect(res.body).to.have.property('success').to.be.not.ok; 49 | node.expect(res.body).to.have.property('error').to.equal("TypeError: Cannot read property 'signature' of undefined"); 50 | done(); 51 | }); 52 | }); 53 | 54 | describe('when account has no funds', function () { 55 | 56 | it('should fail', function (done) { 57 | var transaction = node.ark.signature.createSignature(node.randomPassword(), node.randomPassword()); 58 | 59 | postTransaction(transaction, function (err, res) { 60 | node.expect(res.body).to.have.property('success').to.be.not.ok; 61 | node.expect(res.body).to.have.property('error').to.match(/Account does not have enough ARK: [a-zA-Z0-9]+ balance: 0/); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('when account has funds', function () { 68 | 69 | before(function (done) { 70 | sendArk({ 71 | secret: node.gAccount.password, 72 | amount: node.fees.secondPasswordFee + 100000000, 73 | recipientId: account.address 74 | }, done); 75 | }); 76 | 77 | it('should be ok', function (done) { 78 | var transaction = node.ark.signature.createSignature(account.password, account.secondPassword); 79 | transaction.fee = node.fees.secondPasswordFee; 80 | 81 | postTransaction(transaction, function (err, res) { 82 | node.expect(res.body).to.have.property('success').to.be.ok; 83 | node.expect(res.body).to.have.property('transactionIds'); 84 | node.expect(res.body.transactionIds[0]).to.equal(transaction.id); 85 | done(); 86 | }); 87 | }); 88 | }); 89 | }); 90 | 91 | describe('using second signature', function () { 92 | 93 | var testaccount = node.randomAccount(); 94 | 95 | before(function (done) { 96 | node.onNewBlock(function (err) { 97 | done(); 98 | }); 99 | }); 100 | 101 | it('when account does not have one should fail', function (done) { 102 | var transaction = node.ark.transaction.createTransaction(testaccount.address, 1, null, node.gAccount.password, account.secondPassword); 103 | 104 | postTransaction(transaction, function (err, res) { 105 | node.expect(res.body).to.have.property('success').to.be.not.ok; 106 | done(); 107 | }); 108 | }); 109 | 110 | it('using blank second passphrase should fail', function (done) { 111 | var transaction = node.ark.transaction.createTransaction(testaccount.address, 1, null, account.password, ''); 112 | 113 | postTransaction(transaction, function (err, res) { 114 | node.expect(res.body).to.have.property('success').to.be.not.ok; 115 | done(); 116 | }); 117 | }); 118 | 119 | it('using fake second passphrase should fail', function (done) { 120 | var transaction = node.ark.transaction.createTransaction(testaccount.address, 1, null, account.password, account2.secondPassword); 121 | transaction.signSignature = crypto.randomBytes(64).toString('hex'); 122 | transaction.id = node.ark.crypto.getId(transaction); 123 | 124 | postTransaction(transaction, function (err, res) { 125 | node.expect(res.body).to.have.property('success').to.be.not.ok; 126 | done(); 127 | }); 128 | }); 129 | 130 | it('using valid second passphrase should be ok', function (done) { 131 | var transaction = node.ark.transaction.createTransaction(testaccount.address, 1, null, account.password, account.secondPassword); 132 | 133 | postTransaction(transaction, function (err, res) { 134 | node.expect(res.body).to.have.property('success').to.be.ok; 135 | node.expect(res.body).to.have.property('transactionIds'); 136 | node.expect(res.body.transactionIds[0]).to.equal(transaction.id); 137 | done(); 138 | }); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/api/signatures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var node = require('./../node.js'); 4 | 5 | var account = node.randomTxAccount(); 6 | var account2 = node.randomTxAccount(); 7 | var account3 = node.randomTxAccount(); 8 | 9 | function putSignature (params, done) { 10 | node.put('/api/signatures', params, done); 11 | } 12 | 13 | function putTransaction (params, done) { 14 | node.put('/api/transactions', params, done); 15 | } 16 | 17 | function putDelegate (params, done) { 18 | node.put('/api/delegates', params, done); 19 | } 20 | 21 | function sendArk (account, done) { 22 | var randomArk = node.randomArk(); 23 | var expectedFee = node.expectedFee(randomArk); 24 | 25 | putTransaction({ 26 | secret: node.gAccount.password, 27 | amount: randomArk, 28 | recipientId: account.address 29 | }, function (err, res) { 30 | node.expect(res.body).to.have.property('success').to.be.ok; 31 | done(err, res); 32 | }); 33 | } 34 | 35 | before(function (done) { 36 | setTimeout(function () { 37 | sendArk(account, done); 38 | }, 2000); 39 | }); 40 | 41 | before(function (done) { 42 | setTimeout(function () { 43 | sendArk(account2, done); 44 | }, 2000); 45 | }); 46 | 47 | describe('PUT /api/signatures', function () { 48 | 49 | before(function (done) { 50 | node.onNewBlock(done); 51 | }); 52 | 53 | var validParams; 54 | 55 | beforeEach(function (done) { 56 | validParams = { 57 | secret: account.password, 58 | secondSecret: account.secondPassword 59 | }; 60 | done(); 61 | }); 62 | 63 | it('when account has no funds should fail', function (done) { 64 | validParams.secret = account3.password; 65 | validParams.secondSecret = account3.password; 66 | 67 | putSignature(validParams, function (err, res) { 68 | node.expect(res.body).to.have.property('success').to.be.not.ok; 69 | node.expect(res.body).to.have.property('error').to.match(/Account does not have enough ARK: [a-zA-Z0-9]+ balance: 0/); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('using invalid passphrase should fail', function (done) { 75 | validParams.secret = 'invalid'; 76 | 77 | putSignature(validParams, function (err, res) { 78 | node.expect(res.body).to.have.property('success').to.be.not.ok; 79 | node.expect(res.body).to.have.property('error').to.match(/Account does not have enough ARK: [a-zA-Z0-9]+ balance: 0/); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('using no second passphrase should fail', function (done) { 85 | delete validParams.secondSecret; 86 | 87 | putSignature(validParams, function (err, res) { 88 | node.expect(res.body).to.have.property('success').to.be.not.ok; 89 | node.expect(res.body).to.have.property('error'); 90 | done(); 91 | }); 92 | }); 93 | 94 | it('using valid parameters should be ok', function (done) { 95 | putSignature(validParams, function (err, res) { 96 | node.expect(res.body).to.have.property('success').to.be.ok; 97 | node.expect(res.body).to.have.property('transaction').that.is.an('object'); 98 | node.expect(res.body.transaction).to.have.property('type').to.equal(node.txTypes.SIGNATURE); 99 | node.expect(res.body.transaction).to.have.property('senderPublicKey').to.equal(account.publicKey); 100 | node.expect(res.body.transaction).to.have.property('senderId').to.equal(account.address); 101 | node.expect(res.body.transaction).to.have.property('fee').to.equal(node.fees.secondPasswordFee); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | 107 | describe('PUT /api/transactions from account with second signature enabled', function () { 108 | 109 | before(function (done) { 110 | node.onNewBlock(done); 111 | }); 112 | 113 | var validParams; 114 | 115 | beforeEach(function (done) { 116 | validParams = { 117 | secret: account.password, 118 | secondSecret: account.password, 119 | recipientId: account2.address, 120 | amount: 100000000 121 | }; 122 | done(); 123 | }); 124 | 125 | it('using no second passphase should fail', function (done) { 126 | delete validParams.secondSecret; 127 | 128 | putTransaction(validParams, function (err, res) { 129 | node.expect(res.body).to.have.property('success').to.be.not.ok; 130 | node.expect(res.body).to.have.property('error'); 131 | done(); 132 | }); 133 | }); 134 | 135 | it('using second passphase but no primary passphase should fail', function (done) { 136 | delete validParams.secret; 137 | 138 | putTransaction(validParams, function (err, res) { 139 | node.expect(res.body).to.have.property('success').to.be.not.ok; 140 | node.expect(res.body).to.have.property('error'); 141 | done(); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('PUT /api/delegates from account with second signature enabled', function () { 147 | 148 | var validParams; 149 | 150 | beforeEach(function (done) { 151 | validParams = { 152 | secret: account.password, 153 | secondSecret: account.password, 154 | username: account.delegateName 155 | }; 156 | done(); 157 | }); 158 | 159 | it('using no second passphase should fail', function (done) { 160 | delete validParams.secondSecret; 161 | 162 | putDelegate(validParams, function (err, res) { 163 | node.expect(res.body).to.have.property('success').to.be.not.ok; 164 | node.expect(res.body).to.have.property('error'); 165 | done(); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /logic/signature.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ByteBuffer = require('bytebuffer'); 4 | var constants = require('../helpers/constants.js'); 5 | 6 | // Private fields 7 | var modules, library; 8 | 9 | // Constructor 10 | function Signature () {} 11 | 12 | // 13 | //__API__ `bind` 14 | 15 | // 16 | Signature.prototype.bind = function (scope) { 17 | modules = scope.modules; 18 | library = scope.library; 19 | }; 20 | 21 | // 22 | //__API__ `create` 23 | 24 | // 25 | Signature.prototype.create = function (data, trs) { 26 | trs.recipientId = null; 27 | trs.amount = 0; 28 | trs.asset.signature = { 29 | publicKey: data.secondKeypair.publicKey 30 | }; 31 | 32 | return trs; 33 | }; 34 | 35 | // 36 | //__API__ `calculateFee` 37 | 38 | // 39 | Signature.prototype.calculateFee = function (trs) { 40 | return constants.fees.secondsignature; 41 | }; 42 | 43 | // 44 | //__API__ `verify` 45 | 46 | // 47 | Signature.prototype.verify = function (trs, sender, cb) { 48 | if (!trs.asset || !trs.asset.signature) { 49 | return cb('Invalid transaction asset'); 50 | } 51 | 52 | if (trs.amount !== 0) { 53 | return cb('Invalid transaction amount'); 54 | } 55 | 56 | try { 57 | if (!trs.asset.signature.publicKey || new Buffer(trs.asset.signature.publicKey, 'hex').length !== 33) { 58 | return cb('Invalid public key'); 59 | } 60 | } catch (e) { 61 | library.logger.error("stack", e.stack); 62 | return cb('Invalid public key'); 63 | } 64 | 65 | return cb(null, trs); 66 | }; 67 | 68 | // 69 | //__API__ `process` 70 | 71 | // 72 | Signature.prototype.process = function (trs, sender, cb) { 73 | return cb(null, trs); 74 | }; 75 | 76 | // 77 | //__API__ `getBytes` 78 | 79 | // 80 | Signature.prototype.getBytes = function (trs) { 81 | var bb; 82 | 83 | try { 84 | bb = new ByteBuffer(33, true); 85 | var publicKeyBuffer = new Buffer(trs.asset.signature.publicKey, 'hex'); 86 | 87 | for (var i = 0; i < publicKeyBuffer.length; i++) { 88 | bb.writeByte(publicKeyBuffer[i]); 89 | } 90 | 91 | bb.flip(); 92 | } catch (e) { 93 | throw e; 94 | } 95 | return bb.toBuffer(); 96 | }; 97 | 98 | // 99 | //__API__ `apply` 100 | 101 | // 102 | Signature.prototype.apply = function (trs, block, sender, cb) { 103 | modules.accounts.setAccountAndGet({ 104 | address: sender.address, 105 | secondSignature: 1, 106 | u_secondSignature: 0, 107 | secondPublicKey: trs.asset.signature.publicKey 108 | }, cb); 109 | }; 110 | 111 | // 112 | //__API__ `undo` 113 | 114 | // 115 | Signature.prototype.undo = function (trs, block, sender, cb) { 116 | modules.accounts.setAccountAndGet({ 117 | address: sender.address, 118 | secondSignature: 0, 119 | u_secondSignature: 1, 120 | secondPublicKey: null 121 | }, cb); 122 | }; 123 | 124 | // 125 | //__API__ `applyUnconfirmed` 126 | 127 | // 128 | Signature.prototype.applyUnconfirmed = function (trs, sender, cb) { 129 | if (sender.u_secondSignature || sender.secondSignature) { 130 | return cb('Failed second signature: ' + trs.id); 131 | } 132 | 133 | modules.accounts.setAccountAndGet({address: sender.address, u_secondSignature: 1}, cb); 134 | }; 135 | 136 | // 137 | //__API__ `undoUnconfirmed` 138 | 139 | // 140 | Signature.prototype.undoUnconfirmed = function (trs, sender, cb) { 141 | modules.accounts.setAccountAndGet({address: sender.address, u_secondSignature: 0}, cb); 142 | }; 143 | 144 | Signature.prototype.schema = { 145 | id: 'Signature', 146 | object: true, 147 | properties: { 148 | publicKey: { 149 | type: 'string', 150 | format: 'publicKey' 151 | } 152 | }, 153 | required: ['publicKey'] 154 | }; 155 | 156 | // 157 | //__API__ `objectNormalize` 158 | 159 | // 160 | Signature.prototype.objectNormalize = function (trs) { 161 | var report = library.schema.validate(trs.asset.signature, Signature.prototype.schema); 162 | 163 | if (!report) { 164 | throw 'Failed to validate signature schema: ' + this.scope.schema.getLastErrors().map(function (err) { 165 | return err.message; 166 | }).join(', '); 167 | } 168 | 169 | return trs; 170 | }; 171 | 172 | // 173 | //__API__ `dbRead` 174 | 175 | // 176 | Signature.prototype.dbRead = function (raw) { 177 | if (!raw.s_publicKey) { 178 | return null; 179 | } else { 180 | var signature = { 181 | transactionId: raw.t_id, 182 | publicKey: raw.s_publicKey 183 | }; 184 | 185 | return {signature: signature}; 186 | } 187 | }; 188 | 189 | Signature.prototype.dbTable = 'signatures'; 190 | 191 | Signature.prototype.dbFields = [ 192 | 'transactionId', 193 | 'publicKey' 194 | ]; 195 | 196 | // 197 | //__API__ `dbSave` 198 | 199 | // 200 | Signature.prototype.dbSave = function (trs) { 201 | var publicKey; 202 | 203 | try { 204 | publicKey = new Buffer(trs.asset.signature.publicKey, 'hex'); 205 | } catch (e) { 206 | throw e; 207 | } 208 | 209 | return { 210 | table: this.dbTable, 211 | fields: this.dbFields, 212 | values: { 213 | transactionId: trs.id, 214 | publicKey: publicKey 215 | } 216 | }; 217 | }; 218 | 219 | // 220 | //__API__ `ready` 221 | 222 | // 223 | Signature.prototype.ready = function (trs, sender) { 224 | if (Array.isArray(sender.multisignatures) && sender.multisignatures.length) { 225 | if (!Array.isArray(trs.signatures)) { 226 | return false; 227 | } 228 | return trs.signatures.length >= sender.multimin; 229 | } else { 230 | return true; 231 | } 232 | }; 233 | 234 | // Export 235 | module.exports = Signature; 236 | -------------------------------------------------------------------------------- /test/api/peer.transactions.delegates.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var crypto = require('crypto'); 4 | var node = require('./../node.js'); 5 | 6 | var account = node.randomAccount(); 7 | var account2 = node.randomAccount(); 8 | 9 | function postTransaction (transaction, done) { 10 | node.post('/peer/transactions', { 11 | transactions: [transaction] 12 | }, function (err, res) { 13 | done(err, res); 14 | }); 15 | } 16 | 17 | function sendArk (params, done) { 18 | var transaction = node.ark.transaction.createTransaction(params.recipientId, params.amount, null, params.secret); 19 | 20 | postTransaction(transaction, function (err, res) { 21 | node.expect(res.body).to.have.property('success').to.be.ok; 22 | node.onNewBlock(function (err) { 23 | done(err, res); 24 | }); 25 | }); 26 | } 27 | 28 | describe('POST /peer/transactions', function () { 29 | 30 | describe('registering a delegate', function () { 31 | 32 | it('using undefined transaction', function (done) { 33 | postTransaction(undefined, function (err, res) { 34 | node.expect(res.body).to.have.property('success').to.be.not.ok; 35 | node.expect(res.body).to.have.property('error').to.equal("TypeError: Cannot read property 'type' of null"); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('using undefined transaction.asset', function (done) { 41 | var transaction = node.ark.delegate.createDelegate(node.randomPassword(), node.randomDelegateName().toLowerCase()); 42 | transaction.fee = node.fees.delegateRegistrationFee; 43 | 44 | delete transaction.asset; 45 | 46 | postTransaction(transaction, function (err, res) { 47 | node.expect(res.body).to.have.property('success').to.be.not.ok; 48 | node.expect(res.body).to.have.property('message').to.equal("Invalid transaction detected"); 49 | done(); 50 | }); 51 | }); 52 | 53 | describe('when account has no funds', function () { 54 | 55 | it('should fail', function (done) { 56 | var transaction = node.ark.delegate.createDelegate(node.randomPassword(), node.randomDelegateName().toLowerCase()); 57 | transaction.fee = node.fees.delegateRegistrationFee; 58 | 59 | postTransaction(transaction, function (err, res) { 60 | node.expect(res.body).to.have.property('success').to.be.not.ok; 61 | node.expect(res.body).to.have.property('error').to.match(/Account does not have enough ARK: [a-zA-Z0-9]+ balance: 0/); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('when account has funds', function () { 68 | 69 | before(function (done) { 70 | sendArk({ 71 | secret: node.gAccount.password, 72 | amount: node.fees.delegateRegistrationFee, 73 | recipientId: account.address 74 | }, done); 75 | }); 76 | 77 | it('using invalid username should fail', function (done) { 78 | var transaction = node.ark.delegate.createDelegate(account.password, crypto.randomBytes(64).toString('hex')); 79 | transaction.fee = node.fees.delegateRegistrationFee; 80 | 81 | postTransaction(transaction, function (err, res) { 82 | node.expect(res.body).to.have.property('success').to.be.not.ok; 83 | done(); 84 | }); 85 | }); 86 | 87 | it('using uppercase username should fail', function (done) { 88 | account.username = node.randomDelegateName().toUpperCase(); 89 | var transaction = node.ark.delegate.createDelegate(account.password, account.username); 90 | 91 | postTransaction(transaction, function (err, res) { 92 | node.expect(res.body).to.have.property('success').to.be.not.ok; 93 | done(); 94 | }); 95 | }); 96 | 97 | describe('when lowercased username already registered', function () { 98 | it('using uppercase username should fail', function (done) { 99 | var transaction = node.ark.delegate.createDelegate(account2.password, account.username.toUpperCase()); 100 | 101 | postTransaction(transaction, function (err, res) { 102 | node.expect(res.body).to.have.property('success').to.be.not.ok; 103 | done(); 104 | }); 105 | }); 106 | }); 107 | 108 | it('using lowercase username should be ok', function (done) { 109 | account.username = node.randomDelegateName().toLowerCase(); 110 | var transaction = node.ark.delegate.createDelegate(account.password, account.username); 111 | 112 | postTransaction(transaction, function (err, res) { 113 | node.expect(res.body).to.have.property('success').to.be.ok; 114 | node.expect(res.body).to.have.property('transactionIds'); 115 | node.expect(res.body.transactionIds[0]).to.equal(transaction.id); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('twice within the same block', function () { 122 | 123 | before(function (done) { 124 | sendArk({ 125 | secret: node.gAccount.password, 126 | amount: (node.fees.delegateRegistrationFee * 2), 127 | recipientId: account2.address 128 | }, done); 129 | }); 130 | 131 | it('should fail', function (done) { 132 | account2.username = node.randomDelegateName().toLowerCase(); 133 | var transaction = node.ark.delegate.createDelegate(account2.password, account2.username); 134 | 135 | account2.username = node.randomDelegateName().toLowerCase(); 136 | var transaction2 = node.ark.delegate.createDelegate(account2.password, account2.username); 137 | 138 | postTransaction(transaction, function (err, res) { 139 | node.expect(res.body).to.have.property('success').to.be.ok; 140 | 141 | postTransaction(transaction2, function (err, res) { 142 | node.expect(res.body).to.have.property('success').to.be.not.ok; 143 | done(); 144 | }); 145 | }); 146 | }); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /views/example.pug: -------------------------------------------------------------------------------- 1 | 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 6 | meta(name='viewport', content='width=device-width, initial-scale=1') 7 | 8 | meta(name='description', content='') 9 | meta(name='author', content='') 10 | 11 | link(rel="icon", href="https://ark.io/wp-content/uploads/2016/10/welcome-ark-1.png", sizes="32x32") 12 | title Ark Node 13 | // Bootstrap core CSS 14 | link(href='https://cdn.jsdelivr.net/bootstrap/3.3.7/css/bootstrap.min.css', rel='stylesheet') 15 | link(href='https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css', rel='stylesheet') 16 | link(href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css", rel="stylesheet") 17 | // HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries 18 | //if lt IE 9 19 | script(src='https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js') 20 | script(src='https://oss.maxcdn.com/respond/1.4.2/respond.min.js') 21 | 22 | body(style='background-image:url(https://ark.io/wp-content/uploads/2016/10/ark-planet.jpg); background-size:cover') 23 | block content 24 | nav.navbar.navbar-default.navbar-fixed-top 25 | .container 26 | .navbar-header 27 | button.navbar-toggle.collapsed(type='button', data-toggle='collapse', data-target='#navbar', aria-expanded='false', aria-controls='navbar') 28 | span.sr-only Toggle navigation 29 | span.icon-bar 30 | span.icon-bar 31 | span.icon-bar 32 | a.navbar-brand(href='/app/') Ark Node 33 | #navbar.navbar-collapse.collapse 34 | ul.nav.navbar-nav 35 | li(class=(title == 'Home') ? 'active' : null) 36 | a(href='/app/') Home 37 | li(class=(title == 'About') ? 'active' : null) 38 | a(href='/app/about') About 39 | ul.nav.navbar-nav.navbar-right 40 | if user 41 | li.dropdown 42 | a.navbar-avatar.dropdown-toggle(href='#', data-toggle='dropdown') 43 | span #{user.name || user.email}   44 | i.caret 45 | ul.dropdown-menu 46 | li 47 | a(href='profile') Profile 48 | li.divider(role='separator') 49 | li 50 | a(href='logout') Logout 51 | else 52 | li(class=(title == 'Log in') ? 'active' : null) 53 | a(href='/app/login') Log-in 54 | li(class=(title == 'Register') ? 'active' : null) 55 | a(href='/app/register') Register 56 | // /.nav-collapse 57 | .container(style='margin-top:50px;padding-top:15px;') 58 | .jumbotron(style='opacity:0.8;') 59 | h1 60 | img(src="https://ark.io/wp-content/uploads/2016/10/logo-red.png", alt="alt") 61 | | ARK node 62 | h3 Congratulations you just installed ark-node sucessfully 63 | ul 64 | li nethash 65 | pre #{nethash} 66 | .row 67 | .col-md-6 68 | .well(style='opacity:0.8;') 69 | h3 Transaction pool 70 | #tpoolchart 71 | 72 | .col-md-6 73 | .well(style='opacity:0.8;') 74 | h3 Node height 75 | #heightchart 76 | 77 | .col-md-6 78 | .well(style='opacity:0.8;') 79 | h3 Transactions per block 80 | #txchart 81 | 82 | .col-md-6 83 | .well(style='opacity:0.8;') 84 | h3 TPS 85 | #tpschart 86 | 87 | 88 | script(src='https://cdn.jsdelivr.net/jquery/3.1.1/jquery.min.js') 89 | script(src='https://cdn.jsdelivr.net/bootstrap/3.3.7/js/bootstrap.min.js') 90 | script(src='https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js') 91 | script(src='https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js') 92 | script. 93 | var node = {}; 94 | var updateTime = 4; //update every 4s 95 | 96 | function updateChart(type, value){ 97 | node[type].data.push({ 98 | date:new Date().getTime(), 99 | value:value 100 | }); 101 | if(node[type].data.length>50){ 102 | node[type].data.shift(); 103 | } 104 | node[type].chart.setData(node[type].data); 105 | } 106 | 107 | function createChart(type, label, path){ 108 | node[type] = { 109 | data: [], 110 | path: path, 111 | label: label 112 | }; 113 | node[type].chart = new Morris.Line({ 114 | element: type+'chart', 115 | data: node[type].data, 116 | pointSize:0, 117 | xkey: 'date', 118 | ykeys: ['value'], 119 | ymin: 'auto', 120 | smooth: false, 121 | labels: [label] 122 | }); 123 | } 124 | 125 | var resolve = function(path, obj) { 126 | return path.split('.').reduce(function(prev, curr) { 127 | return prev ? prev[curr] : undefined 128 | }, obj || self) 129 | } 130 | 131 | var lastTpool; 132 | function updateData(){ 133 | $.get("/app/getStats", function(res){ 134 | res.tps=(res.transactionPool - lastTpool)/updateTime; 135 | if(!res.tps || res.tps<0) res.tps=0; 136 | lastTpool=res.transactionPool; 137 | for(var type in node){ 138 | updateChart(type, resolve(node[type].path, res)); 139 | } 140 | 141 | }); 142 | }; 143 | 144 | createChart('height', 'Height', 'lastBlock.height'); 145 | createChart('tpool', 'Transaction pool', 'transactionPool'); 146 | createChart('tx', 'Transactions', 'lastBlock.numberOfTransactions'); 147 | createChart('tps', 'TPS', 'tps'); 148 | 149 | updateData(); 150 | setInterval(updateData, updateTime*1000); 151 | -------------------------------------------------------------------------------- /test/api/accounts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /*jslint mocha:true, expr:true */ 2 | 3 | var node = require('./../node.js'); 4 | 5 | var account = node.randomAccount(); 6 | 7 | describe('GET /api/accounts/getBalance?address=', function () { 8 | 9 | function getBalance (address, done) { 10 | node.get('/api/accounts/getBalance?address=' + address, done); 11 | } 12 | 13 | it('using known address should be ok', function (done) { 14 | getBalance(node.gAccount.address, function (err, res) { 15 | node.expect(res.body).to.have.property('success').to.be.ok; 16 | node.expect(res.body).to.have.property('balance').that.is.a('string'); 17 | node.expect(res.body).to.have.property('unconfirmedBalance').that.is.a('string'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('using unknown address should be ok', function (done) { 23 | getBalance(account.address, function (err, res) { 24 | node.expect(res.body).to.have.property('success').to.be.ok; 25 | node.expect(res.body).to.have.property('balance').that.is.a('string'); 26 | node.expect(res.body).to.have.property('unconfirmedBalance').that.is.a('string'); 27 | done(); 28 | }); 29 | }); 30 | 31 | it('using invalid address should fail', function (done) { 32 | getBalance('éthisIsNOTAArkAddress', function (err, res) { 33 | node.expect(res.body).to.have.property('success').to.be.not.ok; 34 | node.expect(res.body).to.have.property('error').to.contain('Object didn\'t pass validation for format address'); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('using empty address should fail', function (done) { 40 | getBalance('', function (err, res) { 41 | node.expect(res.body).to.have.property('success').to.be.not.ok; 42 | node.expect(res.body).to.have.property('error'); 43 | node.expect(res.body.error).to.contain('String is too short (0 chars), minimum 1'); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('GET /api/accounts/getPublicKey?address=', function () { 50 | 51 | function getPublicKey (address, done) { 52 | node.get('/api/accounts/getPublicKey?address=' + address, done); 53 | } 54 | 55 | it('using known address should be ok', function (done) { 56 | getPublicKey(node.gAccount.address, function (err, res) { 57 | node.expect(res.body).to.have.property('success').to.be.ok; 58 | node.expect(res.body).to.have.property('publicKey').to.equal(node.gAccount.publicKey); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('using unknown address should be ok', function (done) { 64 | getPublicKey(account.address, function (err, res) { 65 | node.expect(res.body).to.have.property('success').to.be.not.ok; 66 | node.expect(res.body).to.have.property('error').to.contain('Account not found'); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('using invalid address should fail', function (done) { 72 | getPublicKey('éthisIsNOTAArkAddress', function (err, res) { 73 | node.expect(res.body).to.have.property('success').to.be.not.ok; 74 | node.expect(res.body).to.have.property('error').to.contain('Object didn\'t pass validation for format address'); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('using empty address should fail', function (done) { 80 | getPublicKey('', function (err, res) { 81 | node.expect(res.body).to.have.property('success').to.be.not.ok; 82 | node.expect(res.body).to.have.property('error'); 83 | node.expect(res.body.error).to.contain('String is too short (0 chars), minimum 1'); 84 | done(); 85 | }); 86 | }); 87 | }); 88 | 89 | describe('GET /accounts?address=', function () { 90 | 91 | function getAccounts (address, done) { 92 | node.get('/api/accounts?address=' + address, done); 93 | } 94 | 95 | it('using known address should be ok', function (done) { 96 | getAccounts(node.gAccount.address, function (err, res) { 97 | node.expect(res.body).to.have.property('success').to.be.ok; 98 | node.expect(res.body).to.have.property('account').that.is.an('object'); 99 | node.expect(res.body.account).to.have.property('address').to.equal(node.gAccount.address); 100 | node.expect(res.body.account).to.have.property('unconfirmedBalance').that.is.a('string'); 101 | node.expect(res.body.account).to.have.property('balance').that.is.a('string'); 102 | node.expect(res.body.account).to.have.property('publicKey').to.equal(node.gAccount.publicKey); 103 | node.expect(res.body.account).to.have.property('unconfirmedSignature').to.equal(0); 104 | node.expect(res.body.account).to.have.property('secondSignature').to.equal(0); 105 | node.expect(res.body.account).to.have.property('secondPublicKey').to.equal(null); 106 | node.expect(res.body.account).to.have.property('multisignatures').to.a('array'); 107 | node.expect(res.body.account).to.have.property('u_multisignatures').to.a('array'); 108 | done(); 109 | }); 110 | }); 111 | 112 | it('using known lowercase address should not be ok', function (done) { 113 | getAccounts(node.gAccount.address.toLowerCase(), function (err, res) { 114 | node.expect(res.body).to.have.property('success').to.be.not.ok; 115 | done(); 116 | }); 117 | }); 118 | 119 | it('using unknown address should fail', function (done) { 120 | getAccounts(account.address, function (err, res) { 121 | node.expect(res.body).to.have.property('success').to.be.not.ok; 122 | node.expect(res.body).to.have.property('error').to.eql('Account not found'); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('using invalid address should fail', function (done) { 128 | getAccounts('éthisIsNOTAValidArkAddress', function (err, res) { 129 | node.expect(res.body).to.have.property('success').to.be.not.ok; 130 | node.expect(res.body).to.have.property('error'); 131 | node.expect(res.body.error).to.contain('Object didn\'t pass validation for format address'); 132 | done(); 133 | }); 134 | }); 135 | 136 | it('using empty address should fail', function (done) { 137 | getAccounts('', function (err, res) { 138 | node.expect(res.body).to.have.property('success').to.be.not.ok; 139 | node.expect(res.body).to.have.property('error'); 140 | node.expect(res.body.error).to.contain('String is too short (0 chars), minimum 1'); 141 | done(); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /modules/signatures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var constants = require('../helpers/constants.js'); 5 | var crypto = require('crypto'); 6 | var MilestoneBlocks = require('../helpers/milestoneBlocks.js'); 7 | var Router = require('../helpers/router.js'); 8 | var schema = require('../schema/signatures.js'); 9 | var slots = require('../helpers/slots.js'); 10 | var transactionTypes = require('../helpers/transactionTypes.js'); 11 | 12 | // Private fields 13 | var modules, library, self, __private = {}, shared = {}; 14 | 15 | __private.assetTypes = {}; 16 | 17 | // Constructor 18 | function Signatures (cb, scope) { 19 | library = scope; 20 | self = this; 21 | 22 | var Signature = require('../logic/signature.js'); 23 | __private.assetTypes[transactionTypes.SIGNATURE] = library.logic.transaction.attachAssetType( 24 | transactionTypes.SIGNATURE, new Signature() 25 | ); 26 | 27 | return cb(null, self); 28 | } 29 | 30 | // Private methods 31 | __private.attachApi = function () { 32 | var router = new Router(); 33 | 34 | router.use(function (req, res, next) { 35 | if (modules) { return next(); } 36 | res.status(500).send({success: false, error: 'Blockchain is loading'}); 37 | }); 38 | 39 | router.map(shared, { 40 | 'get /fee': 'getFee', 41 | 'put /': 'addSignature' 42 | }); 43 | 44 | router.use(function (req, res, next) { 45 | res.status(500).send({success: false, error: 'API endpoint not found'}); 46 | }); 47 | 48 | library.network.app.use('/api/signatures', router); 49 | library.network.app.use(function (err, req, res, next) { 50 | if (!err) { return next(); } 51 | library.logger.error('API error ' + req.url, err); 52 | res.status(500).send({success: false, error: 'API error: ' + err.message}); 53 | }); 54 | }; 55 | 56 | // Public methods 57 | 58 | // Events 59 | // 60 | //__EVENT__ `onBind` 61 | 62 | // 63 | Signatures.prototype.onBind = function (scope) { 64 | modules = scope; 65 | 66 | __private.assetTypes[transactionTypes.SIGNATURE].bind({ 67 | modules: modules, library: library 68 | }); 69 | }; 70 | 71 | 72 | // 73 | //__EVENT__ `onAttachPublicApi` 74 | 75 | // 76 | Signatures.prototype.onAttachPublicApi = function () { 77 | __private.attachApi(); 78 | }; 79 | 80 | // Shared 81 | shared.getFee = function (req, cb) { 82 | var fee = null; 83 | 84 | fee = constants.fees.secondsignature; 85 | 86 | return cb(null, {fee: fee}); 87 | }; 88 | 89 | shared.addSignature = function (req, cb) { 90 | library.schema.validate(req.body, schema.addSignature, function (err) { 91 | if (err) { 92 | return cb(err[0].message); 93 | } 94 | 95 | var keypair = library.crypto.makeKeypair(req.body.secret); 96 | 97 | if (req.body.publicKey) { 98 | if (keypair.publicKey.toString('hex') !== req.body.publicKey) { 99 | return cb('Invalid passphrase'); 100 | } 101 | } 102 | 103 | library.balancesSequence.add(function (cb) { 104 | if (req.body.multisigAccountPublicKey && req.body.multisigAccountPublicKey !== keypair.publicKey.toString('hex')) { 105 | modules.accounts.getAccount({publicKey: req.body.multisigAccountPublicKey}, function (err, account) { 106 | if (err) { 107 | return cb(err); 108 | } 109 | 110 | if (!account || !account.publicKey) { 111 | return cb('Multisignature account not found'); 112 | } 113 | 114 | if (!account.multisignatures || !account.multisignatures) { 115 | return cb('Account does not have multisignatures enabled'); 116 | } 117 | 118 | if (account.multisignatures.indexOf(keypair.publicKey.toString('hex')) < 0) { 119 | return cb('Account does not belong to multisignature group'); 120 | } 121 | 122 | if (account.secondSignature || account.u_secondSignature) { 123 | return cb('Account already has a second passphrase'); 124 | } 125 | 126 | modules.accounts.getAccount({publicKey: keypair.publicKey}, function (err, requester) { 127 | if (err) { 128 | return cb(err); 129 | } 130 | 131 | if (!requester || !requester.publicKey) { 132 | return cb('Requester not found'); 133 | } 134 | 135 | if (requester.secondSignature && !req.body.secondSecret) { 136 | return cb('Missing requester second passphrase'); 137 | } 138 | 139 | if (requester.publicKey === account.publicKey) { 140 | return cb('Invalid requester public key'); 141 | } 142 | 143 | var secondKeypair = library.crypto.makeKeypair(req.body.secondSecret); 144 | var transaction; 145 | 146 | try { 147 | transaction = library.logic.transaction.create({ 148 | type: transactionTypes.SIGNATURE, 149 | sender: account, 150 | keypair: keypair, 151 | requester: keypair, 152 | secondKeypair: secondKeypair, 153 | 154 | }); 155 | } catch (e) { 156 | return cb(e.toString()); 157 | } 158 | 159 | library.bus.message("transactionsReceived", [transaction], "api", cb); 160 | }); 161 | }); 162 | } else { 163 | modules.accounts.setAccountAndGet({publicKey: keypair.publicKey.toString('hex')}, function (err, account) { 164 | if (err) { 165 | return cb(err); 166 | } 167 | 168 | if (!account || !account.publicKey) { 169 | return cb('Account not found'); 170 | } 171 | 172 | if (account.secondSignature || account.u_secondSignature) { 173 | return cb('Account already has a second passphrase'); 174 | } 175 | 176 | var secondKeypair = library.crypto.makeKeypair(req.body.secondSecret); 177 | var transaction; 178 | 179 | try { 180 | transaction = library.logic.transaction.create({ 181 | type: transactionTypes.SIGNATURE, 182 | sender: account, 183 | keypair: keypair, 184 | secondKeypair: secondKeypair 185 | }); 186 | } catch (e) { 187 | return cb(e.toString()); 188 | } 189 | 190 | library.bus.message("transactionsReceived", [transaction], "api", cb); 191 | }); 192 | } 193 | 194 | }, function (err, transaction) { 195 | if (err) { 196 | return cb(err); 197 | } 198 | return cb(null, {transaction: transaction[0]}); 199 | }); 200 | 201 | }); 202 | }; 203 | 204 | // Export 205 | module.exports = Signatures; 206 | -------------------------------------------------------------------------------- /config.testnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4000, 3 | "address": "0.0.0.0", 4 | "version": "1.0.0", 5 | "fileLogLevel": "info", 6 | "logFileName": "logs/ark.log", 7 | "consoleLogLevel": "debug", 8 | "trustProxy": false, 9 | "db": { 10 | "host": "localhost", 11 | "port": 5432, 12 | "database": "ark_testnet", 13 | "user": null, 14 | "password": "password", 15 | "poolSize": 20, 16 | "poolIdleTimeout": 30000, 17 | "reapIntervalMillis": 1000, 18 | "logEvents": [ 19 | "error" 20 | ] 21 | }, 22 | "api": { 23 | "mount": true, 24 | "access": { 25 | "whiteList": [] 26 | }, 27 | "options": { 28 | "limits": { 29 | "max": 0, 30 | "delayMs": 0, 31 | "delayAfter": 0, 32 | "windowMs": 60000 33 | } 34 | } 35 | }, 36 | "peers": { 37 | "minimumNetworkReach": 1, 38 | "list": [ 39 | { 40 | "ip": "127.0.0.1", 41 | "port": 4000 42 | } 43 | ], 44 | "blackList": [], 45 | "options": { 46 | "limits": { 47 | "max": 0, 48 | "delayMs": 0, 49 | "delayAfter": 0, 50 | "windowMs": 60000 51 | }, 52 | "maxUpdatePeers": 20, 53 | "timeout": 5000 54 | } 55 | }, 56 | "forging": { 57 | "coldstart": 6, 58 | "force": true, 59 | "secret": [ 60 | "height dance bottom plastic circle scrap will creek invest fever degree oven", 61 | "runway home silent mutual blade nose spin marriage fancy main smooth champion", 62 | "elder alter stock acoustic jazz divide toward whip decorate summer wire length", 63 | "deputy shoe pizza eyebrow choice fury emotion morning naive that error opera", 64 | "define end worry style typical basket fresh chapter cloud enroll kidney juice", 65 | "gap open romance sadness north system jump vault debris correct spend exist", 66 | "history hollow pencil marine sentence chat plastic sniff focus final impulse toilet", 67 | "disorder exit caution joke kitchen broccoli response decorate machine outside this again", 68 | "paddle ridge young goddess tackle struggle radio door wink engage inhale pear", 69 | "false man grocery depth thought hammer vital jaguar seminar old tobacco field", 70 | "nerve finger use balcony capable soul mixed shallow hurt bench video below", 71 | "observe champion change history very soft bleak twenty cigar such penalty bunker", 72 | "nurse island tragic margin absorb danger naive whale blouse owner rifle average", 73 | "other nut young essay endorse talent sick flavor panel under brand cancel", 74 | "globe shine sand lonely problem tilt laugh garbage clutch submit distance arrive", 75 | "expose law earn crucial what bridge guard split delay average isolate home", 76 | "shoot they acid whip vicious injury future siren chuckle twin bless wrist", 77 | "property machine actor forest rigid dilemma such globe cross citizen across enemy", 78 | "survey parent uphold shine metal draw normal stove potato melt huge master", 79 | "office sting divorce fragile limit creek maple aware hour clutch code avocado", 80 | "debris city onion achieve screen village orange labor rice item someone much", 81 | "limit shaft canoe sorry domain exclude danger enter invite just meat price", 82 | "lock auto ankle want whip lottery picnic wolf permit start essay wasp", 83 | "dwarf capital weasel insane sauce cake ice toy suspect eye horn ecology", 84 | "gossip wreck cruise fault tortoise convince sauce meat bulk two bicycle host", 85 | "direct joke weapon other acoustic pulse blue word team flush boil supply", 86 | "unit swap skull magic flock bread sudden problem cushion intact able olympic", 87 | "hedgehog notable grant person riot vague recycle local naive debate rib gap", 88 | "into sauce sleep setup super element squeeze employ try decline brass educate", 89 | "wave drop air picnic vicious oil wheat stereo pride artefact food like", 90 | "devote kiss protect cook where welcome unaware express climb catalog wool little", 91 | "inner enforce have caution lawn proof because similar clarify dinner pig sadness", 92 | "hood welcome room pencil pen find must trend cat warfare hip fine", 93 | "deliver one turkey pelican derive sport oval minute poem myth also color", 94 | "secret pepper antique flip airport stadium angry talent act blur bulb hammer", 95 | "solve novel model upset demise thunder select number sun club region become", 96 | "scrub lawsuit dynamic matrix unhappy reason first taste bind dial lonely pair", 97 | "cousin negative reward elevator mosquito remember pizza slab park pulp pencil lock", 98 | "bicycle dignity burger public miss target oblige dragon ready wood velvet spatial", 99 | "fragile vicious movie field dentist sheriff soda sing gloom gold horse antenna", 100 | "dress jeans obscure update wisdom enact person little sting make napkin rural", 101 | "behind choose rally brand worth time coast decide used celery ribbon adapt", 102 | "solve property flash genius subway verb access portion orphan say taste gorilla", 103 | "sick bike dilemma ten room donkey fossil innocent limit learn inhale globe", 104 | "shoot oppose garment dial replace smoke razor loud cube exile story envelope", 105 | "assume harbor must knee shoulder file already apart today october target range", 106 | "mix judge volcano hidden road stumble phrase behave car setup inhale border", 107 | "jazz erase bargain city there thank vintage erase excuse wrong chronic lend", 108 | "client when person chuckle pen balance rescue measure price toy around soft", 109 | "narrow vague cannon home churn want sick rail above letter tourist switch", 110 | "general sock juice birth express dress harbor tobacco limb drive chimney tree" 111 | ], 112 | "access": { 113 | "whiteList": [ 114 | "127.0.0.1" 115 | ] 116 | } 117 | }, 118 | "loading": { 119 | "verifyOnLoading": false, 120 | "loadPerIteration": 5000 121 | }, 122 | "ssl": { 123 | "enabled": false, 124 | "options": { 125 | "port": 443, 126 | "address": "0.0.0.0", 127 | "key": "./ssl/ark.key", 128 | "cert": "./ssl/ark.crt" 129 | } 130 | }, 131 | "network": "testnet", 132 | "nethash": "d1c7fa0dcf7584add7a535acd674d3b13fa794345d83c3789cc2436d6ecd80d1" 133 | } -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure("2") do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://atlas.hashicorp.com/search. 15 | config.vm.box = "ubuntu/trusty64" 16 | config.vm.network "forwarded_port", guest: 4000, host: 4000 17 | config.vm.provision :shell, privileged: false, inline: <<-SHELL 18 | 19 | ########################################## 20 | # # 21 | # Checking and installing prerequisites # 22 | # # 23 | ########################################## 24 | 25 | # Variables and arrays declarations 26 | log="ark-install.log" 27 | 28 | sudo apt-get update 29 | echo -e "Installing tools... " 30 | sudo apt-get install -yyq build-essential wget python git curl jq htop nmon iftop 31 | 32 | if [ $(dpkg-query -W -f='${Status}' postgresql 2>/dev/null | grep -c "ok installed") -eq 0 ]; 33 | then 34 | echo -e "Installing postgresql... " 35 | sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' 36 | wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | sudo apt-key add - 37 | sudo apt-get update 38 | sudo apt-get install -yyq postgresql postgresql-contrib libpq-dev 39 | else 40 | echo -e "Postgresql is already installed." 41 | fi 42 | 43 | if ! sudo pgrep -x "ntpd" > /dev/null; then 44 | echo -e "No NTP found. Installing... " 45 | sudo apt-get install ntp -yyq &>> $log 46 | sudo service ntp stop &>> $log 47 | sudo ntpd -gq &>> $log 48 | sudo service ntp start &>> $log 49 | if ! sudo pgrep -x "ntpd" > /dev/null; then 50 | echo -e "NTP failed to start! It should be installed and running for ARK.\n Check /etc/ntp.conf for any issues and correct them first! \n Exiting." 51 | exit 1 52 | fi 53 | echo -e "NTP was successfuly installed and started with PID:" `grep -x "ntpd"` 54 | else echo "NTP is up and running with PID:" `pgrep -x "ntpd"` 55 | 56 | fi # if sudo pgrep 57 | 58 | echo "-------------------------------------------------------------------" 59 | 60 | # Installing node 61 | curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 62 | sudo apt-get install nodejs 63 | sudo npm install -g n 64 | 65 | sudo n 6.9.1 66 | sudo npm install forever -g 67 | 68 | echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 69 | 70 | # Creating DB and user 71 | sudo -u postgres psql -c "CREATE USER $USER WITH PASSWORD 'password';" 72 | #sudo -u postgres createuser --createdb --password $USER 73 | sudo -u postgres createdb -O $USER ark_testnet 74 | sudo service postgresql start 75 | 76 | git clone https://github.com/arkecosystem/ark-node.git 77 | 78 | cd /home/vagrant/ark-node 79 | #rm -fr node_modules 80 | npm install grunt-cli 81 | npm install 82 | forever app.js --genesis genesisBlock.testnet.json --config config.testnet.json 83 | SHELL 84 | 85 | config.vm.provider "virtualbox" do |v| 86 | v.memory = 1024 87 | v.customize ["modifyvm", :id, "--cpuexecutioncap", "50"] 88 | v.name = "ark_node_vm" 89 | end 90 | 91 | # Disable automatic box update checking. If you disable this, then 92 | # boxes will only be checked for updates when the user runs 93 | # `vagrant box outdated`. This is not recommended. 94 | # config.vm.box_check_update = false 95 | 96 | # Create a forwarded port mapping which allows access to a specific port 97 | # within the machine from a port on the host machine. In the example below, 98 | # accessing "localhost:8080" will access port 80 on the guest machine. 99 | # config.vm.network "forwarded_port", guest: 80, host: 8080 100 | 101 | # Create a private network, which allows host-only access to the machine 102 | # using a specific IP. 103 | # config.vm.network "private_network", ip: "192.168.33.10" 104 | 105 | # Create a public network, which generally matched to bridged network. 106 | # Bridged networks make the machine appear as another physical device on 107 | # your network. 108 | # config.vm.network "public_network" 109 | 110 | # Share an additional folder to the guest VM. The first argument is 111 | # the path on the host to the actual folder. The second argument is 112 | # the path on the guest to mount the folder. And the optional third 113 | # argument is a set of non-required options. 114 | # config.vm.synced_folder "../data", "/vagrant_data" 115 | 116 | # Provider-specific configuration so you can fine-tune various 117 | # backing providers for Vagrant. These expose provider-specific options. 118 | # Example for VirtualBox: 119 | # 120 | # config.vm.provider "virtualbox" do |vb| 121 | # # Display the VirtualBox GUI when booting the machine 122 | # vb.gui = true 123 | # 124 | # # Customize the amount of memory on the VM: 125 | # vb.memory = "1024" 126 | # end 127 | # 128 | # View the documentation for the provider you are using for more 129 | # information on available options. 130 | 131 | # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies 132 | # such as FTP and Heroku are also available. See the documentation at 133 | # https://docs.vagrantup.com/v2/push/atlas.html for more information. 134 | # config.push.define "atlas" do |push| 135 | # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" 136 | # end 137 | 138 | # Enable provisioning with a shell script. Additional provisioners such as 139 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 140 | # documentation for more information about their specific syntax and use. 141 | # config.vm.provision "shell", inline: <<-SHELL 142 | # apt-get update 143 | # apt-get install -y apache2 144 | # SHELL 145 | end 146 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4000, 3 | "address": "0.0.0.0", 4 | "version": "0.3.0", 5 | "fileLogLevel": "info", 6 | "logFileName": "logs/ark.log", 7 | "consoleLogLevel": "debug", 8 | "trustProxy": false, 9 | "db": { 10 | "host": "localhost", 11 | "port": 5432, 12 | "database": "ark_test", 13 | "user": null, 14 | "password": "password", 15 | "poolSize": 20, 16 | "poolIdleTimeout": 30000, 17 | "reapIntervalMillis": 1000, 18 | "logEvents": [ 19 | "error" 20 | ] 21 | }, 22 | "api": { 23 | "mount":true, 24 | "access": { 25 | "whiteList": [] 26 | }, 27 | "options": { 28 | "limits": { 29 | "max": 0, 30 | "delayMs": 0, 31 | "delayAfter": 0, 32 | "windowMs": 60000 33 | } 34 | } 35 | }, 36 | "peers": { 37 | "minimumNetworkReach": 1, 38 | "list": [ 39 | { 40 | "ip": "127.0.0.1", 41 | "port": 4000 42 | } 43 | ], 44 | "blackList": [], 45 | "options": { 46 | "limits": { 47 | "max": 0, 48 | "delayMs": 0, 49 | "delayAfter": 0, 50 | "windowMs": 60000 51 | }, 52 | "maxUpdatePeers": 20, 53 | "timeout": 5000 54 | } 55 | }, 56 | "forging": { 57 | "coldstart": 6, 58 | "force": true, 59 | "secret": [ 60 | "clay harbor enemy utility margin pretty hub comic piece aerobic umbrella acquire", 61 | "venue below waste gather spin cruise title still boost mother flash tuna", 62 | "craft imitate step mixture patch forest volcano business charge around girl confirm", 63 | "fatal hat sail asset chase barrel pluck bag approve coral slab bright", 64 | "flash thank strike stove grain remove match reflect excess present beyond matrix", 65 | "various present shine domain outdoor neck soup diesel limit express genuine tuna", 66 | "hurdle pulse sheriff anchor two hope income pattern hazard bacon book night", 67 | "glow boss party require silk interest pyramid marriage try wisdom snow grab", 68 | "direct palace screen shuffle world fit produce rubber jelly gather river ordinary", 69 | "wall ketchup shed word twist flip knock liar merge rural ill pond", 70 | "measure blue volcano month orphan only cupboard found laugh peasant drama monitor", 71 | "scissors sort pause medal target diesel reveal stock maze party gauge vacant", 72 | "hand anchor hip pyramid taxi vote celery clap tribe damage shrimp brave", 73 | "merge thunder detect stove else bottom favorite doll learn festival basic basic", 74 | "educate attitude rely combine treat balcony west reopen coil west grab depth", 75 | "advance silver advance squeeze load stone middle garden perfect invest field lounge", 76 | "prison tobacco acquire stone dignity palace note decade they current lesson robot", 77 | "team impact stadium year security steak harsh vacant fire pelican until olympic", 78 | "walk intact ice prevent fit trial frog glory monkey once grunt gentle", 79 | "same lens parrot suspect just sunset frown exercise lemon two mistake robust", 80 | "skill insect issue crazy erase okay govern upgrade bounce dress motor athlete", 81 | "peasant alert hard deposit naive follow page fiscal normal awful wedding history", 82 | "resemble abandon same total oppose noise dune order fatal rhythm pink science", 83 | "wide mesh ketchup acquire bright day mountain final below hamster scout drive", 84 | "half weasel poet better rocket fan help left blade soda argue system", 85 | "target sort neutral address language spike measure jaguar glance strong drop zone", 86 | "race total stage trap wool believe twin pudding claim claim eternal miss", 87 | "parade isolate wing vague magic husband acid skin skate path fence rib", 88 | "neither fine dry priority example obtain bread reopen afford coyote milk minor", 89 | "token atom lemon game charge area goose hotel excess endless spice oblige", 90 | "pledge buffalo finish pipe mule popular bind clinic draft salon swamp purpose", 91 | "west hat hold stand unique panther cable extend spell shaft injury reopen", 92 | "van impulse pole install profit excuse give auction expire remain skate input", 93 | "wrist maze potato april survey burden bamboo knee foot carry speak prison", 94 | "three toddler copy owner pencil minimum doctor orange bottom ice detail design", 95 | "ceiling warrior person thing whisper jeans black cricket drift ahead tornado typical", 96 | "obvious mutual tone usual valve credit soccer mention also clown main box", 97 | "valve slot soft green scale menu anxiety live drill legend upgrade chimney", 98 | "twist comfort mule weather print oven cabin seek punch rival prepare sphere", 99 | "say tumble glass argue aware service force caution until grocery hammer fetch", 100 | "idea illegal empty frozen canvas arctic number poet rely track size obscure", 101 | "chalk try large tower shed warfare blade clerk fame second charge tobacco", 102 | "category nice verb fox start able brass climb boss luggage voice whale", 103 | "favorite emotion trumpet visual welcome spend fine lock image review garage opera", 104 | "waste axis humor auction next salmon much margin useful glimpse insect rotate", 105 | "remember rose genuine police guard old flavor parent gain cross twelve first", 106 | "coil tray elder mask circle crush anger electric harbor onion grab will", 107 | "shove airport bus gather radio derive below horse canvas crime tribe adjust", 108 | "retire lend burden cricket able sheriff output grocery empty scorpion flat inquiry", 109 | "agree grain record shift fossil summer hunt mutual net vast behind pilot", 110 | "decide rhythm oyster lady they merry betray jelly coyote solve episode then" 111 | ], 112 | "access": { 113 | "whiteList": [ 114 | "127.0.0.1" 115 | ] 116 | } 117 | }, 118 | "loading": { 119 | "verifyOnLoading": false, 120 | "loadPerIteration": 5000 121 | }, 122 | "modules": { 123 | "example":"./optional/example.js" 124 | }, 125 | "ssl": { 126 | "enabled": false, 127 | "options": { 128 | "port": 443, 129 | "address": "0.0.0.0", 130 | "key": "./ssl/ark.key", 131 | "cert": "./ssl/ark.crt" 132 | } 133 | }, 134 | "nethash": "d1c7fa0dcf7584add7a535acd674d3b13fa794345d83c3789cc2436d6ecd80d1" 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ark 2 | 3 | **:warning: DEPRECATED IN FAVOR OF https://github.com/ArkEcosystem/core :warning:*** 4 | 5 | Ark is a next generation crypto-currency and decentralized application platform, written entirely in JavaScript. For more information please refer to our website: https://ark.io/. 6 | 7 | The Token Exchange Campaign is up at https://tec.ark.io 8 | 9 | This version is still alpha, use at your own risks 10 | 11 | ## Install, Upgrade etc... 12 | You need to provision a linux (ubuntu tested) server (digital ocean, vultur or other). 13 | 14 | Then use the excellent ark-commander script 15 | ``` 16 | cd 17 | wget https://ark.io/ARKcommander.sh 18 | bash ARKcommander.sh 19 | ``` 20 | 21 | For developers, please read below in section "Developer Installation" 22 | 23 | ## Details 24 | 25 | This is a fork from Lisk with the following features: 26 | - Removed sidechains (deprecated in favor of smartbridge) 27 | - Removed custom node version 28 | - Removed UI for stability and security reasons 29 | - Changed some constants (block rewards, blocktime etc...) 30 | - Added simple PBFT before forging new block 31 | - Ditch addresses from the protocol in favor of bitcoin like system, enabling HD Wallet as for BIP32 32 | - Completely rewritten node management using a single NodeManager and messaging system 33 | - Completely rewritten round management (removed mem_round, reward block fees to forger) 34 | - Added 64 bytes vendorField as first iteration of smart bridge 35 | - Made peers management entirely in-memory for efficiency 36 | - Strengthened the transaction management and broadcast (reject often, reject soon) 37 | - Rearchitect with relay nodes and forging nodes 38 | - Nodes broadcast only block headers. 39 | 40 | ### Planned features: 41 | - Simple blockchain validation for SPV and use in lite clients 42 | - Add IPFS as first class citizen (using smartbridge addressing) 43 | - Protocol improvements (uncle forging, voting weights). 44 | - Remove unsecured API 45 | - Routing tables 46 | 47 | ### Performance 48 | - stable on testnet at 5tx/s 49 | - pushed to 10tx/s on devnet 50 | 51 | 52 | ## Developer Installation 53 | 54 | Install essentials: 55 | 56 | ``` 57 | sudo apt-get update 58 | sudo apt-get install -y curl build-essential python git 59 | ``` 60 | 61 | Install PostgreSQL (min version: 9.5.2) 62 | 63 | ``` 64 | sudo apt-get install -y postgresql postgresql-contrib 65 | sudo -u postgres createuser --createdb --password $USER 66 | createdb ark_test 67 | ``` 68 | 69 | Install Node.js (tested with version 6.9.2, but any recent LTS release should do): 70 | 71 | ``` 72 | sudo apt-get install -y nodejs 73 | sudo npm install -g n 74 | sudo n 6.9.2 75 | ``` 76 | 77 | Install grunt-cli (globally): 78 | 79 | ``` 80 | sudo npm install grunt-cli -g 81 | ``` 82 | 83 | Clone this repository 84 | ``` 85 | git clone https://github.com/arkecosytem/ark-node.git 86 | cd ark-node 87 | ``` 88 | 89 | Install node modules: 90 | ``` 91 | npm install libpq secp256k1 92 | npm install 93 | ``` 94 | 95 | ## Launch 96 | To launch Ark on testnet: 97 | ``` 98 | createdb ark_testnet 99 | node run start:testnet 100 | ``` 101 | 102 | To launch Ark on devtnet: 103 | ``` 104 | createdb ark_devnet 105 | node run start:devnet 106 | ``` 107 | 108 | To launch Ark on mainnet (when launched): 109 | ``` 110 | createdb ark_mainnet 111 | node run start:mainnet 112 | ``` 113 | 114 | **NOTE:** The **port**, **address**, **genesis block** and **config-path** can be overridden by providing the relevant command switch: 115 | ``` 116 | node app.js -p [port] -a [address] -c [config-path] -g [genesisBlock-path] 117 | ``` 118 | This allow you to run several different networks, or your own private chain 119 | 120 | 121 | ## Launch your own private or public chain 122 | Generate a genesisBlock.json + a default config.json containing all passphrases of genesis delegates 123 | ``` 124 | node tasks/createGenesisBlock.js 125 | ``` 126 | You can find generated files in tasks/ 127 | - genesisBlock.json 128 | - config.json 129 | - delegatesPassphrases.json (containing details about the genesis delegates) 130 | - genesisPassphrase.json (containing the details of account having all premined arks) 131 | 132 | Obviously you can hack away tasks/createGenesisBlock.js for your own custom use. 133 | 134 | You can the start with your own chain on a single node (all delegates will forge on your single node) using: 135 | ``` 136 | createdb ark_newtest 137 | npm run start:newtest 138 | ``` 139 | 140 | Then you can distribute the config.json (without the delegates secrets inside, and with custom peers settings) to peers to let them join your chain 141 | 142 | 143 | ## Tests 144 | Load git submodule [ark-js](https://github.com/arkecosystem/ark-js): 145 | ``` 146 | git submodule init 147 | git submodule update 148 | ``` 149 | 150 | You should run using test configurations 151 | 152 | ``` 153 | npm run start:test 154 | ``` 155 | 156 | Run the test suite: 157 | 158 | ``` 159 | npm test 160 | ``` 161 | 162 | Run individual tests: 163 | 164 | ``` 165 | npm test -- test/api/accounts.js 166 | npm test -- test/api/transactions.js 167 | ``` 168 | 169 | **NOTE:** The master passphrase for this test genesis block is as follows: 170 | 171 | ``` 172 | peace vanish bleak box tuna woman rally manage undo royal lucky since 173 | ``` 174 | 175 | 176 | ## Authors 177 | - FX Thoorens 178 | - Boris Povod 179 | - Pavel Nekrasov 180 | - Sebastian Stupurac 181 | - Oliver Beddows 182 | 183 | ## License 184 | 185 | The MIT License (MIT) 186 | 187 | Copyright (c) 2016 Ark 188 | Copyright (c) 2016 Lisk 189 | Copyright (c) 2014-2015 Crypti 190 | 191 | 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: 192 | 193 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 194 | 195 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 196 | -------------------------------------------------------------------------------- /logic/vote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var constants = require('../helpers/constants.js'); 5 | var exceptions = require('../helpers/exceptions.js'); 6 | var Diff = require('../helpers/diff.js'); 7 | 8 | // Private fields 9 | var modules, library, self; 10 | 11 | // Constructor 12 | function Vote () { 13 | self = this; 14 | } 15 | 16 | 17 | // Public methods 18 | // 19 | //__API__ `bind` 20 | 21 | // 22 | Vote.prototype.bind = function (scope) { 23 | modules = scope.modules; 24 | library = scope.library; 25 | }; 26 | 27 | // 28 | //__API__ `create` 29 | 30 | // 31 | Vote.prototype.create = function (data, trs) { 32 | trs.recipientId = data.sender.address; 33 | trs.asset.votes = data.votes; 34 | 35 | return trs; 36 | }; 37 | 38 | // 39 | //__API__ `calculateFee` 40 | 41 | // 42 | Vote.prototype.calculateFee = function (trs) { 43 | return constants.fees.vote; 44 | }; 45 | 46 | // 47 | //__API__ `verify` 48 | 49 | // 50 | Vote.prototype.verify = function (trs, sender, cb) { 51 | if (trs.recipientId !== trs.senderId) { 52 | return cb('Invalid recipient'); 53 | } 54 | 55 | if (!trs.asset || !trs.asset.votes) { 56 | return cb('Invalid transaction asset'); 57 | } 58 | 59 | if (!Array.isArray(trs.asset.votes)) { 60 | return cb('Invalid votes. Must be an array'); 61 | } 62 | 63 | if (!trs.asset.votes.length) { 64 | return cb('Invalid votes. Must not be empty'); 65 | } 66 | 67 | if (trs.asset.votes && trs.asset.votes.length > constants.maximumVotes) { 68 | return cb('Voting limit exceeded. Maximum is '+constants.maximumVotes+' vote per transaction'); 69 | } 70 | 71 | modules.delegates.checkConfirmedDelegates(trs.senderPublicKey, trs.asset.votes, function (err) { 72 | if (err && exceptions.votes.indexOf(trs.id) > -1) { 73 | library.logger.debug(err); 74 | library.logger.debug(JSON.stringify(trs)); 75 | err = null; 76 | } 77 | return cb(err, trs); 78 | }); 79 | }; 80 | 81 | // 82 | //__API__ `process` 83 | 84 | // 85 | Vote.prototype.process = function (trs, sender, cb) { 86 | return cb(null, trs); 87 | }; 88 | 89 | // 90 | //__API__ `getBytes` 91 | 92 | // 93 | Vote.prototype.getBytes = function (trs) { 94 | var buf; 95 | 96 | try { 97 | buf = trs.asset.votes ? new Buffer(trs.asset.votes.join(''), 'utf8') : null; 98 | } catch (e) { 99 | throw e; 100 | } 101 | 102 | return buf; 103 | }; 104 | 105 | 106 | // 107 | //__API__ `checkConfirmedDelegates` 108 | 109 | // 110 | Vote.prototype.checkConfirmedDelegates = function (trs, cb) { 111 | modules.delegates.checkConfirmedDelegates(trs.senderPublicKey, trs.asset.votes, function (err) { 112 | if (err && exceptions.votes.indexOf(trs.id) > -1) { 113 | library.logger.debug(err); 114 | library.logger.debug(JSON.stringify(trs)); 115 | err = null; 116 | } 117 | 118 | return cb(err); 119 | }); 120 | }; 121 | 122 | 123 | // 124 | //__API__ `checkUnconfirmedDelegates` 125 | 126 | // 127 | Vote.prototype.checkUnconfirmedDelegates = function (trs, cb) { 128 | modules.delegates.checkUnconfirmedDelegates(trs.senderPublicKey, trs.asset.votes, function (err) { 129 | if (err && exceptions.votes.indexOf(trs.id) > -1) { 130 | library.logger.debug(err); 131 | library.logger.debug(JSON.stringify(trs)); 132 | err = null; 133 | } 134 | 135 | return cb(err); 136 | }); 137 | }; 138 | 139 | // 140 | //__API__ `apply` 141 | 142 | // 143 | Vote.prototype.apply = function (trs, block, sender, cb) { 144 | var parent = this; 145 | 146 | async.series([ 147 | function (seriesCb) { 148 | self.checkConfirmedDelegates(trs, seriesCb); 149 | }, 150 | function (seriesCb) { 151 | parent.scope.account.merge(sender.address, { 152 | delegates: trs.asset.votes, 153 | blockId: block.id, 154 | round: modules.rounds.getRoundFromHeight(block.height) 155 | }, seriesCb); 156 | } 157 | ], cb); 158 | }; 159 | 160 | 161 | // 162 | //__API__ `undo` 163 | 164 | // 165 | Vote.prototype.undo = function (trs, block, sender, cb) { 166 | if (trs.asset.votes === null) { return cb(); } 167 | 168 | var votesInvert = Diff.reverse(trs.asset.votes); 169 | 170 | this.scope.account.merge(sender.address, { 171 | delegates: votesInvert, 172 | blockId: block.id, 173 | round: modules.rounds.getRoundFromHeight(block.height) 174 | }, cb); 175 | }; 176 | 177 | // 178 | //__API__ `applyUnconfirmed` 179 | 180 | // 181 | Vote.prototype.applyUnconfirmed = function (trs, sender, cb) { 182 | var parent = this; 183 | 184 | async.series([ 185 | function (seriesCb) { 186 | self.checkUnconfirmedDelegates(trs, seriesCb); 187 | }, 188 | function (seriesCb) { 189 | parent.scope.account.merge(sender.address, { 190 | u_delegates: trs.asset.votes 191 | }, seriesCb); 192 | } 193 | ], cb); 194 | }; 195 | 196 | // 197 | //__API__ `undoUnconfirmed` 198 | 199 | // 200 | Vote.prototype.undoUnconfirmed = function (trs, sender, cb) { 201 | if (trs.asset.votes === null) { return cb(); } 202 | 203 | var votesInvert = Diff.reverse(trs.asset.votes); 204 | this.scope.account.merge(sender.address, {u_delegates: votesInvert}, cb); 205 | }; 206 | 207 | Vote.prototype.schema = { 208 | id: 'Vote', 209 | type: 'object', 210 | properties: { 211 | votes: { 212 | type: 'array', 213 | items: { 214 | type: 'string', 215 | format: 'voteString', 216 | }, 217 | minLength: 1, 218 | maxLength: constants.maximumVotes, 219 | uniqueItems: true 220 | } 221 | }, 222 | required: ['votes'] 223 | }; 224 | 225 | // 226 | //__API__ `objectNormalize` 227 | 228 | // 229 | Vote.prototype.objectNormalize = function (trs) { 230 | var report = library.schema.validate(trs.asset, Vote.prototype.schema); 231 | 232 | if (!report) { 233 | throw 'Failed to validate vote schema: ' + this.scope.schema.getLastErrors().map(function (err) { 234 | return err.message; 235 | }).join(', '); 236 | } 237 | 238 | return trs; 239 | }; 240 | 241 | // 242 | //__API__ `dbRead` 243 | 244 | // 245 | Vote.prototype.dbRead = function (raw) { 246 | // console.log(raw.v_votes); 247 | 248 | if (!raw.v_votes) { 249 | return null; 250 | } else { 251 | var votes = raw.v_votes.split(','); 252 | 253 | return {votes: votes}; 254 | } 255 | }; 256 | 257 | Vote.prototype.dbTable = 'votes'; 258 | 259 | Vote.prototype.dbFields = [ 260 | 'votes', 261 | 'transactionId' 262 | ]; 263 | 264 | // 265 | //__API__ `dbSave` 266 | 267 | // 268 | Vote.prototype.dbSave = function (trs) { 269 | return { 270 | table: this.dbTable, 271 | fields: this.dbFields, 272 | values: { 273 | votes: Array.isArray(trs.asset.votes) ? trs.asset.votes.join(',') : null, 274 | transactionId: trs.id 275 | } 276 | }; 277 | }; 278 | 279 | // 280 | //__API__ `ready` 281 | 282 | // 283 | Vote.prototype.ready = function (trs, sender) { 284 | if (Array.isArray(sender.multisignatures) && sender.multisignatures.length) { 285 | if (!Array.isArray(trs.signatures)) { 286 | return false; 287 | } 288 | return trs.signatures.length >= sender.multimin; 289 | } else { 290 | return true; 291 | } 292 | }; 293 | 294 | // Export 295 | module.exports = Vote; 296 | -------------------------------------------------------------------------------- /logic/delegate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var constants = require('../helpers/constants.js'); 4 | 5 | // Private fields 6 | var modules, library; 7 | 8 | // Constructor 9 | function Delegate () {} 10 | 11 | // Public methods 12 | // 13 | //__API__ `bind` 14 | 15 | // 16 | Delegate.prototype.bind = function (scope) { 17 | modules = scope.modules; 18 | library = scope.library; 19 | }; 20 | 21 | // 22 | //__API__ `create` 23 | 24 | // 25 | Delegate.prototype.create = function (data, trs) { 26 | trs.recipientId = null; 27 | trs.amount = 0; 28 | trs.asset.delegate = { 29 | username: data.username, 30 | publicKey: data.sender.publicKey 31 | }; 32 | 33 | if (trs.asset.delegate.username) { 34 | trs.asset.delegate.username = trs.asset.delegate.username.toLowerCase().trim(); 35 | } 36 | 37 | return trs; 38 | }; 39 | 40 | // 41 | //__API__ `calculateFee` 42 | 43 | // 44 | Delegate.prototype.calculateFee = function (trs) { 45 | return constants.fees.delegate; 46 | }; 47 | 48 | // 49 | //__API__ `verify` 50 | 51 | // 52 | Delegate.prototype.verify = function (trs, sender, cb) { 53 | if (trs.recipientId) { 54 | return cb('Invalid recipient'); 55 | } 56 | 57 | if (trs.amount !== 0) { 58 | return cb('Invalid transaction amount'); 59 | } 60 | 61 | if (sender.isDelegate) { 62 | return cb('Account is already a delegate'); 63 | } 64 | 65 | if (!trs.asset || !trs.asset.delegate) { 66 | return cb('Invalid transaction asset'); 67 | } 68 | 69 | if (!trs.asset.delegate.username) { 70 | return cb('Username is undefined'); 71 | } 72 | 73 | if (trs.asset.delegate.username !== trs.asset.delegate.username.toLowerCase()) { 74 | return cb('Username must be lowercase'); 75 | } 76 | 77 | var isAddress = /^[1-9A-Za-z]{1,35}$/g; 78 | var allowSymbols = /^[a-z0-9!@$&_.]+$/g; 79 | 80 | var username = String(trs.asset.delegate.username).toLowerCase().trim(); 81 | 82 | if (username === '') { 83 | return cb('Empty username'); 84 | } 85 | 86 | if (username.length > 20) { 87 | return cb('Username is too long. Maximum is 20 characters'); 88 | } 89 | 90 | // Not relevant anymore 91 | // if (isAddress.test(username)) { 92 | // return cb('Username can not be a potential address'); 93 | // } 94 | 95 | if (!allowSymbols.test(username)) { 96 | return cb('Username can only contain alphanumeric characters with the exception of !@$&_.'); 97 | } 98 | 99 | modules.accounts.getAccount({ 100 | username: username 101 | }, function (err, account) { 102 | if (err) { 103 | return cb(err); 104 | } 105 | 106 | if (account) { 107 | return cb('Username already exists'); 108 | } 109 | 110 | return cb(null, trs); 111 | }); 112 | }; 113 | 114 | // 115 | //__API__ `process` 116 | 117 | // 118 | Delegate.prototype.process = function (trs, sender, cb) { 119 | return cb(null, trs); 120 | }; 121 | 122 | // 123 | //__API__ `getBytes` 124 | 125 | // 126 | Delegate.prototype.getBytes = function (trs) { 127 | if (!trs.asset.delegate.username) { 128 | return null; 129 | } 130 | 131 | var buf; 132 | 133 | try { 134 | buf = new Buffer(trs.asset.delegate.username, 'utf8'); 135 | } catch (e) { 136 | throw e; 137 | } 138 | 139 | return buf; 140 | }; 141 | 142 | // 143 | //__API__ `apply` 144 | 145 | // 146 | Delegate.prototype.apply = function (trs, block, sender, cb) { 147 | var data = { 148 | address: sender.address, 149 | u_isDelegate: 0, 150 | isDelegate: 1, 151 | vote: 0 152 | }; 153 | 154 | if (trs.asset.delegate.username) { 155 | data.u_username = null; 156 | data.username = trs.asset.delegate.username; 157 | } 158 | 159 | modules.accounts.setAccountAndGet(data, cb); 160 | }; 161 | 162 | // 163 | //__API__ `undo` 164 | 165 | // 166 | Delegate.prototype.undo = function (trs, block, sender, cb) { 167 | var data = { 168 | address: sender.address, 169 | u_isDelegate: 1, 170 | isDelegate: 0, 171 | vote: 0 172 | }; 173 | 174 | if (!sender.nameexist && trs.asset.delegate.username) { 175 | data.username = null; 176 | data.u_username = trs.asset.delegate.username; 177 | } 178 | 179 | modules.accounts.setAccountAndGet(data, cb); 180 | }; 181 | 182 | // 183 | //__API__ `applyUnconfirmed` 184 | 185 | // 186 | Delegate.prototype.applyUnconfirmed = function (trs, sender, cb) { 187 | if (sender.u_isDelegate) { 188 | return cb('Account is already a delegate'); 189 | } 190 | 191 | function done () { 192 | var data = { 193 | address: sender.address, 194 | u_isDelegate: 1, 195 | isDelegate: 0 196 | }; 197 | 198 | if (trs.asset.delegate.username) { 199 | data.username = null; 200 | data.u_username = trs.asset.delegate.username; 201 | } 202 | 203 | modules.accounts.setAccountAndGet(data, cb); 204 | } 205 | 206 | modules.accounts.getAccount({ 207 | u_username: trs.asset.delegate.username 208 | }, function (err, account) { 209 | if (err) { 210 | return cb(err); 211 | } 212 | 213 | if (account) { 214 | return cb('Username already exists'); 215 | } 216 | 217 | done(); 218 | }); 219 | }; 220 | 221 | // 222 | //__API__ `undoUnconfirmed` 223 | 224 | // 225 | Delegate.prototype.undoUnconfirmed = function (trs, sender, cb) { 226 | var data = { 227 | address: sender.address, 228 | u_isDelegate: 0, 229 | isDelegate: 0 230 | }; 231 | 232 | if (trs.asset.delegate.username) { 233 | data.username = null; 234 | data.u_username = null; 235 | } 236 | 237 | modules.accounts.setAccountAndGet(data, cb); 238 | }; 239 | 240 | Delegate.prototype.schema = { 241 | id: 'Delegate', 242 | type: 'object', 243 | properties: { 244 | publicKey: { 245 | type: 'string', 246 | format: 'publicKey' 247 | } 248 | }, 249 | required: ['publicKey'] 250 | }; 251 | 252 | // 253 | //__API__ `objectNormalize` 254 | 255 | // 256 | Delegate.prototype.objectNormalize = function (trs) { 257 | var report = library.schema.validate(trs.asset.delegate, Delegate.prototype.schema); 258 | 259 | if (!report) { 260 | throw 'Failed to validate delegate schema: ' + this.scope.schema.getLastErrors().map(function (err) { 261 | return err.message; 262 | }).join(', '); 263 | } 264 | 265 | return trs; 266 | }; 267 | 268 | // 269 | //__API__ `dbRead` 270 | 271 | // 272 | Delegate.prototype.dbRead = function (raw) { 273 | if (!raw.d_username) { 274 | return null; 275 | } else { 276 | var delegate = { 277 | username: raw.d_username, 278 | publicKey: raw.t_senderPublicKey, 279 | address: raw.t_senderId 280 | }; 281 | 282 | return {delegate: delegate}; 283 | } 284 | }; 285 | 286 | Delegate.prototype.dbTable = 'delegates'; 287 | 288 | Delegate.prototype.dbFields = [ 289 | 'username', 290 | 'transactionId' 291 | ]; 292 | 293 | // 294 | //__API__ `dbSave` 295 | 296 | // 297 | Delegate.prototype.dbSave = function (trs) { 298 | return { 299 | table: this.dbTable, 300 | fields: this.dbFields, 301 | values: { 302 | username: trs.asset.delegate.username, 303 | transactionId: trs.id 304 | } 305 | }; 306 | }; 307 | 308 | // 309 | //__API__ `ready` 310 | 311 | // 312 | Delegate.prototype.ready = function (trs, sender) { 313 | if (Array.isArray(sender.multisignatures) && sender.multisignatures.length) { 314 | if (!Array.isArray(trs.signatures)) { 315 | return false; 316 | } 317 | return trs.signatures.length >= sender.multimin; 318 | } else { 319 | return true; 320 | } 321 | }; 322 | 323 | // Export 324 | module.exports = Delegate; 325 | --------------------------------------------------------------------------------