diff --git a/api/http/states.js b/api/http/states.js new file mode 100644 index 00000000..ed010c1d --- /dev/null +++ b/api/http/states.js @@ -0,0 +1,38 @@ +'use strict'; + +var Router = require('../../helpers/router'); +var httpApi = require('../../helpers/httpApi'); + + +/** + * Binds api with modules and creates common url. + * - End point: `/api/states` + * - Private API: + * - post /normalize + * - post /finalize + * + * - Sanitized + * - get /get + * @memberof module:states + * @requires helpers/Router + * @requires helpers/httpApi + * @constructor + * @param {Object} statesModule - Module storing options state + * @param {scope} app - Network app. + */ +// Constructor +function StatesHttpApi (statesModule, app) { + + var router = new Router(); + + router.map(statesModule.internal, { + 'get /get': 'getTransactions', + 'post /normalize': 'normalize', + 'post /store': 'store' + }); + + + httpApi.registerEndpoint('/api/states', app, router, statesModule.isLoaded); +} + +module.exports = StatesHttpApi; diff --git a/app.js b/app.js index 05b5768a..32c4588b 100644 --- a/app.js +++ b/app.js @@ -138,6 +138,7 @@ var config = { multisignatures: './modules/multisignatures.js', dapps: './modules/dapps.js', chats: './modules/chats.js', + states: './modules/states.js', crypto: './modules/crypto.js', sql: './modules/sql.js', cache: './modules/cache.js' @@ -147,6 +148,7 @@ var config = { blocks: { http: './api/http/blocks.js' }, dapps: { http: './api/http/dapps.js' }, chats: { http: './api/http/chats.js' }, + states: { http: './api/http/states.js' }, delegates: { http: './api/http/delegates.js' }, loader: { http: './api/http/loader.js' }, multisignatures: { http: './api/http/multisignatures.js' }, @@ -462,6 +464,7 @@ d.run(function () { logic: ['db', 'bus', 'schema', 'genesisblock', function (scope, cb) { var Transaction = require('./logic/transaction.js'); var Chat = require('./logic/chat.js'); + var State = require('./logic/state.js'); var Block = require('./logic/block.js'); var Account = require('./logic/account.js'); var Peers = require('./logic/peers.js'); @@ -495,6 +498,9 @@ d.run(function () { }], chat: ['db', 'bus', 'ed', 'schema', 'account', 'logger', function (scope, cb) { new Chat(scope.db, scope.ed, scope.schema, scope.account, scope.logger, cb); + }], + state: ['db', 'bus', 'ed', 'schema', 'account', 'logger', function (scope, cb) { + new State(scope.db, scope.ed, scope.schema, scope.account, scope.logger, cb); }], block: ['db', 'bus', 'ed', 'schema', 'genesisblock', 'account', 'transaction', function (scope, cb) { new Block(scope.ed, scope.schema, scope.transaction, cb); diff --git a/config.json b/config.json index e5cb4cd0..9d2a274c 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,8 @@ { "port": 36666, "address": "0.0.0.0", - "version": "0.1.2", - "minVersion": ">=0.1.0", + "version": "0.2.0", + "minVersion": ">=0.2.0", "fileLogLevel": "info", "logFileName": "logs/adamant.log", "consoleLogLevel": "none", diff --git a/helpers/constants.js b/helpers/constants.js index d011d760..9087f52b 100644 --- a/helpers/constants.js +++ b/helpers/constants.js @@ -58,6 +58,7 @@ module.exports = { dapp: 2500000000, old_chat_message: 500000, chat_message: 100000, + state_store: 100000, profile_update: 5000000, avatar_upload: 10000000 }, @@ -96,7 +97,7 @@ module.exports = { 10000000 // Milestone 8 ], offset: 2000000, // Start rewards at block (n) - distance: 6300000, // Distance between each milestone + distance: 6300000 // Distance between each milestone }, signatureLength: 196, // WARNING: When changing totalAmount you also need to change getBlockRewards(int) SQL function! diff --git a/helpers/transactionTypes.js b/helpers/transactionTypes.js index c008d3ff..c5ec427c 100644 --- a/helpers/transactionTypes.js +++ b/helpers/transactionTypes.js @@ -9,5 +9,6 @@ module.exports = { DAPP: 5, IN_TRANSFER: 6, OUT_TRANSFER: 7, - CHAT_MESSAGE: 8 + CHAT_MESSAGE: 8, + STATE: 9 }; diff --git a/logic/state.js b/logic/state.js new file mode 100644 index 00000000..1276b712 --- /dev/null +++ b/logic/state.js @@ -0,0 +1,376 @@ +'use strict'; + +var ByteBuffer = require('bytebuffer'); +var constants = require('../helpers/constants.js'); +var sql = require('../sql/states.js'); +var valid_url = require('valid-url'); +var slots = require('../helpers/slots.js'); + +// Private fields +var self, library, __private = {}; + +__private.unconfirmedNames = {}; +__private.unconfirmedLinks = {}; +__private.unconfirmedAscii = {}; + +/** + * Initializes library. + * @memberof module:states + * @class + * @classdesc Main state logic. + * @param {Database} db + * @param {Object} logger + * @param {ZSchema} schema + * @param {Object} network + */ +// Constructor +function State (db, ed, schema, account, logger, cb) { + this.scope = { + db: db, + ed: ed, + schema: schema, + logger: logger, + account: account + }; + self = this; + if (cb) { + return setImmediate(cb, null, this); + } +} + +// Public methods +/** + * Binds scope.modules to private variable modules. + */ +State.prototype.bind = function () {}; + +/** + * Creates transaction.asset.State based on data. + * @param {state} data + * @param {transaction} trs + * @return {transaction} trs with new data + */ +State.prototype.create = function (data, trs) { + trs.amount = 0; + trs.recipientId = null; + trs.asset.state = { + value: data.value, + key: data.key, + type: 0 + }; + if (data.state_type) { + trs.asset.state.type = data.state_type; + } + return trs; +}; + +/** + * Returns state fee from constants. + * @param {transaction} trs + * @param {account} sender + * @return {number} fee + */ +State.prototype.calculateFee = function (trs, sender) { + var length = Buffer.from(trs.asset.state.value, 'hex').length; + var char_length= Math.floor((length * 100 / 150)/255); + if (char_length==0) { + char_length = 1; + } + return char_length * constants.fees.state_store; +}; + +/** + * Verifies transaction and state fields. + * @implements {library.db.query} + * @param {transaction} trs + * @param {account} sender + * @param {function} cb + * @return {setImmediateCallback} errors | trs + */ +State.prototype.verify = function (trs, sender, cb) { + + if (!trs.asset || !trs.asset.state) { + return setImmediate(cb, 'Invalid transaction asset'); + } + + if (trs.asset.state.type > 1 || trs.asset.state.type < 0) { + return setImmediate(cb, 'Invalid state type'); + } + + if (!trs.asset.state.value || trs.asset.state.value.trim().length === 0 || trs.asset.state.value.trim() !== trs.asset.state.value) { + return setImmediate(cb, 'Value must not be blank'); + } + + if (trs.asset.state.value.length > 20480) { + return setImmediate(cb, 'Value is too long. Maximum is 20480 characters'); + } + + return setImmediate(cb, null, trs); +}; + +/** + * @param {transaction} trs + * @param {account} sender + * @param {function} cb + * @return {setImmediateCallback} cb, null, trs + */ +State.prototype.process = function (trs, sender, cb) { + return setImmediate(cb, null, trs); +}; + +/** + * Creates a buffer with state information: + * - type + * - key + * - value + * @param {transaction} trs + * @return {Array} Buffer + * @throws {e} error + */ +State.prototype.getBytes = function (trs) { + var buf; + + try { + buf = Buffer.from([]); + var stateBuf = Buffer.from(trs.asset.state.value, 'hex'); + buf = Buffer.concat([buf, stateBuf]); + + if (trs.asset.state.key) { + var keyBuf = Buffer.from(trs.asset.state.key, 'hex'); + buf = Buffer.concat([buf, keyBuf]); + } + + var bb = new ByteBuffer(4 + 4, true); + bb.writeInt(trs.asset.state.type); + bb.flip(); + + buf = Buffer.concat([buf, bb.toBuffer()]); + } catch (e) { + throw e; + } + + return buf; +}; + +/** + * @param {transaction} trs + * @param {block} block + * @param {account} sender + * @param {function} cb + * @return {setImmediateCallback} cb + */ +State.prototype.apply = function (trs, block, sender, cb) { + return setImmediate(cb); +}; + +/** + * @param {transaction} trs + * @param {block} block + * @param {account} sender + * @param {function} cb + * @return {setImmediateCallback} cb + */ +State.prototype.undo = function (trs, block, sender, cb) { + return setImmediate(cb); +}; + +/** + * applyUncorfirmed stub + * @param {transaction} trs + * @param {account} sender + * @param {function} cb + * @return {setImmediateCallback} cb|errors + */ +State.prototype.applyUnconfirmed = function (trs, sender, cb) { + return setImmediate(cb); +}; + +/** + * undoUnconfirmed stub + * @param {transaction} trs + * @param {account} sender + * @param {function} cb + * @return {setImmediateCallback} cb + */ +State.prototype.undoUnconfirmed = function (trs, sender, cb) { + return setImmediate(cb); +}; + + + +/** + * @typedef {Object} state + * @property {string} key - Between 0 and 20 chars + * @property {string} value - Between 1 and 20480 chars + * @property {integer} type - Number, minimum 0 + * @property {string} transactionId - transaction id + */ +State.prototype.schema = { + id: 'State', + type: 'object', + properties: { + key: { + type: 'string', + minLength: 0, + maxLength: 20 + }, + value: { + type: 'string', + minLength: 1, + maxLength: 20480 + }, + type: { + type: 'integer', + minimum: 0 + } + }, + required: ['type', 'value'] +}; + +/** + * Deletes null or undefined state from transaction and validate state schema. + * @implements {library.schema.validate} + * @param {transaction} trs + * @return {transaction} + * @throws {string} Failed to validate state schema. + */ +State.prototype.objectNormalize = function (trs) { + for (var i in trs.asset.state) { + if (trs.asset.state[i] === null || typeof trs.asset.state[i] === 'undefined') { + delete trs.asset.state[i]; + } + } + + var report = this.scope.schema.validate(trs.asset.state, State.prototype.schema); + + if (!report) { + throw 'Failed to validate state schema: ' + this.scope.schema.getLastErrors().map(function (err) { + return err.message; + }).join(', '); + } + + return trs; +}; + +/** + * Creates chat object based on raw data. + * @param {Object} raw + * @return {null|state} state object + */ +State.prototype.dbRead = function (raw) { + if (!raw.st_stored_value) { + return null; + } else { + return {state: { + value: raw.st_stored_value, + key: raw.st_stored_key, + type: raw.st_type + }}; + } + return null; +}; + +State.prototype.dbTable = 'states'; + +State.prototype.dbFields = [ + 'stored_value', + 'stored_key', + 'type', + 'transactionId' +]; +State.prototype.publish = function (data) { + if (!__private.types[data.type]) { + throw 'Unknown transaction type ' + data.type; + } + + if (!data.senderId) { + throw 'Invalid sender'; + } + + if (!data.signature) { + throw 'Invalid signature'; + } + + var trs = data; + + + trs.id = this.getId(trs); + + trs.fee = __private.types[trs.type].calculateFee.call(this, trs, data.senderId) || false; + + return trs; +}; +State.prototype.normalize = function (data) { + if (!__private.types[data.type]) { + throw 'Unknown transaction type ' + data.type; + } + + if (!data.sender) { + throw 'Invalid sender'; + } + + var trs = { + type: data.type, + amount: 0, + senderPublicKey: data.sender.publicKey, + senderId: data.sender.account, + recipientId: null, + timestamp: slots.getTime(), + asset: {} + }; + + trs = __private.types[trs.type].create.call(this, data, trs); + + return trs; +}; + +/** + * Creates db operation object based on dapp data. + * @see privateTypes + * @param {transaction} trs + * @return {Object[]} table, fields, values. + */ +State.prototype.dbSave = function (trs) { + return { + table: this.dbTable, + fields: this.dbFields, + values: { + stored_key: trs.asset.state.key, + stored_value: trs.asset.state.value, + type: trs.asset.state.type, + transactionId: trs.id + } + }; +}; + +/** + * Emits 'states/change' signal. + * @implements {library.network.io.sockets} + * @param {transaction} trs + * @param {function} cb + * @return {setImmediateCallback} cb + */ +State.prototype.afterSave = function (trs, cb) { + return setImmediate(cb); +}; + +/** + * Checks sender multisignatures and transaction signatures. + * @param {transaction} trs + * @param {account} sender + * @return {boolean} True if transaction signatures greather than + * sender multimin or there are not sender multisignatures. + */ +State.prototype.ready = function (trs, sender) { + if (Array.isArray(sender.multisignatures) && sender.multisignatures.length) { + if (!Array.isArray(trs.signatures)) { + return false; + } + return trs.signatures.length >= sender.multimin; + } else { + return true; + } +}; + +// Export +module.exports = State; diff --git a/modules/states.js b/modules/states.js new file mode 100644 index 00000000..a033bcc0 --- /dev/null +++ b/modules/states.js @@ -0,0 +1,500 @@ +var _ = require('lodash'); +var async = require('async'); +var constants = require('../helpers/constants.js'); +var crypto = require('crypto'); +var State = require('../logic/state.js'); +var extend = require('extend'); +var ip = require('ip'); +var npm = require('npm'); +var OrderBy = require('../helpers/orderBy.js'); +var path = require('path'); +var popsicle = require('popsicle'); +var Router = require('../helpers/router.js'); +var schema = require('../schema/states.js'); +var sql = require('../sql/states.js'); +var TransactionPool = require('../logic/transactionPool.js'); +var transactionTypes = require('../helpers/transactionTypes.js'); + +// Private fields +var modules, library, self, __private = {}, shared = {}; + +__private.assetTypes = {}; + + +/** + * Initializes library with scope content and generates instances for: + * - States + * Calls logic.transaction.attachAssetType(). + * + * Listens `exit` signal. + * Checks 'public/state' folder and created it if doesn't exists. + * @memberof module:states + * @class + * @classdesc Main states methods. + * @param {function} cb - Callback function. + * @param {scope} scope - App instance. + * @return {setImmediateCallback} Callback function with `self` as data. + * @todo apply node pattern for callbacks: callback always at the end. + * @todo add 'use strict'; + */ +// Constructor +function States (cb, scope) { + library = { + logger: scope.logger, + db: scope.db, + public: scope.public, + network: scope.network, + schema: scope.schema, + ed: scope.ed, + balancesSequence: scope.balancesSequence, + logic: { + transaction: scope.logic.transaction, + chat: scope.logic.chat + } + }; + self = this; + + __private.assetTypes[transactionTypes.STATE] = library.logic.transaction.attachAssetType( + transactionTypes.STATE, + new State( + scope.db, + scope.logger, + scope.schema, + scope.network + ) + ); + setImmediate(cb, null, self); +} + +// Private methods +/** + * Gets record from `states` table based on id + * @private + * @implements {library.db.query} + * @param {string} id + * @param {function} cb + * @return {setImmediateCallback} error description | row data + */ +__private.get = function (id, cb) { + library.db.query(sql.get, {id: id}).then(function (rows) { + if (rows.length === 0) { + return setImmediate(cb, 'state records not found'); + } else { + return setImmediate(cb, null, rows[0]); + } + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'STATE#get error'); + }); +}; + +/** + * Gets records from `states` table based on id list + * @private + * @implements {library.db.query} + * @param {string[]} ids + * @param {function} cb + * @return {setImmediateCallback} error description | rows data + */ +__private.getByIds = function (ids, cb) { + library.db.query(sql.getByIds, [ids]).then(function (rows) { + return setImmediate(cb, null, rows); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'STATE#getByIds error'); + }); +}; + +/** + * Gets records from `states` table based on filter + * @private + * @implements {library.db.query} + * @param {Object} filter - Could contains type, address, limit, + * offset, orderBy + * @param {function} cb + * @return {setImmediateCallback} error description | rows data + */ +__private.list = function (filter, cb) { + var params = {}, where = []; + + if (filter.type >= 0) { + where.push('"type" = ${type}'); + params.type = filter.type; + } + where.push('"t_type" = '+ transactionTypes.STATE); + + if (filter.senderId) { + where.push('"t_senderId" = ${name}'); + params.name = filter.senderId; + } + if (filter.fromHeight) { + where.push('"b_height" > ${height}'); + params.height = filter.fromHeight; + } + if (!filter.limit) { + params.limit = 100; + } else { + params.limit = Math.abs(filter.limit); + } + + if (!filter.offset) { + params.offset = 0; + } else { + params.offset = Math.abs(filter.offset); + } + + if (params.limit > 100) { + return setImmediate(cb, 'Invalid limit. Maximum is 100'); + } + + var orderBy = OrderBy( + filter.orderBy, { + sortFields: sql.sortFields + } + ); + + if (orderBy.error) { + return setImmediate(cb, orderBy.error); + } + + library.db.query(sql.list({ + where: where, + sortField: orderBy.sortField, + sortMethod: orderBy.sortMethod + }), params).then(function (rows) { + var transactions = []; + + for (var i = 0; i < rows.length; i++) { + transactions.push(library.logic.transaction.dbRead(rows[i])); + } + + var data = { + transactions: transactions + }; + + return setImmediate(cb, null, data); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, err); + }); +}; + + +States.prototype.onBind = function (scope) { + modules = { + transactions: scope.transactions, + accounts: scope.accounts, + peers: scope.peers, + sql: scope.sql, + }; +}; + +/** + * Checks if `modules` is loaded. + * @return {boolean} True if `modules` is loaded. + */ +States.prototype.isLoaded = function () { + return !!modules; +}; + +/** + * Internal & Shared + * - DApps.prototype.internal + * - shared. + * @todo implement API comments with apidoc. + * @see {@link http://apidocjs.com/} + */ +States.prototype.internal = { + getTransactions: function (req, cb) { + async.waterfall([ + function (waterCb) { + var params = {}; + var pattern = /(and|or){1}:/i; + + // Filter out 'and:'/'or:' from params to perform schema validation + _.each(req.body, function (value, key) { + var param = String(key).replace(pattern, ''); + // Dealing with array-like parameters (csv comma separated) + if (_.includes(['senderIds', 'senderPublicKeys'], param)) { + value = String(value).split(','); + req.body[key] = value; + } + params[param] = value; + }); + + library.schema.validate(params, schema.getTransactions, function (err) { + if (err) { + return setImmediate(waterCb, err[0].message); + } else { + return setImmediate(waterCb, null); + } + }); + }, + function (waterCb) { + __private.list(req.body, function (err, data) { + if (err) { + return setImmediate(waterCb, 'Failed to get transactions: ' + err); + } else { + return setImmediate(waterCb, null, {transactions: data.transactions, count: data.count}); + } + }); + } + ], function (err, res) { + return setImmediate(cb, err, res); + }); + }, + normalize: function (req, cb) { + library.schema.validate(req.body, schema.normalize, function (err) { + if (err) { + return setImmediate(cb, err[0].message); + } + + var query = {address: req.body.recipientId}; + + modules.accounts.getAccount(query, function (err, recipient) { + if (err) { + return setImmediate(cb, err); + } + var keypair = {publicKey: ''}; + var recipientId = recipient ? recipient.address : req.body.recipientId; + + if (!recipientId) { + return setImmediate(cb, 'Invalid recipient'); + } + + if (req.body.multisigAccountPublicKey && req.body.multisigAccountPublicKey !== keypair.publicKey.toString('hex')) { + modules.accounts.getAccount({publicKey: req.body.multisigAccountPublicKey}, function (err, account) { + if (err) { + return setImmediate(cb, err); + } + + if (!account || !account.publicKey) { + return setImmediate(cb, 'Multisignature account not found'); + } + + if (!Array.isArray(account.multisignatures)) { + return setImmediate(cb, 'Account does not have multisignatures enabled'); + } + + if (account.multisignatures.indexOf(keypair.publicKey.toString('hex')) < 0) { + return setImmediate(cb, 'Account does not belong to multisignature group'); + } + + modules.accounts.getAccount({publicKey: keypair.publicKey}, function (err, requester) { + if (err) { + return setImmediate(cb, err); + } + + if (!requester || !requester.publicKey) { + return setImmediate(cb, 'Requester not found'); + } + + if (requester.secondSignature && !req.body.secondSecret) { + return setImmediate(cb, 'Missing requester second passphrase'); + } + + if (requester.publicKey === account.publicKey) { + return setImmediate(cb, 'Invalid requester public key'); + } + + var secondKeypair = null; + + if (requester.secondSignature) { + var secondHash = library.ed.createPassPhraseHash(req.body.secondSecret); + secondKeypair = library.ed.makeKeypair(secondHash); + } + + var transaction; + + try { + transaction = library.logic.transaction.create({ + type: transactionTypes.SEND, + amount: req.body.amount, + sender: account, + recipientId: recipientId, + keypair: keypair, + requester: keypair, + secondKeypair: secondKeypair + }); + } catch (e) { + return setImmediate(cb, e.toString()); + } + + modules.transactions.receiveTransactions([transaction], true, cb); + }); + }); + } else { + modules.accounts.setAccountAndGet({publicKey: req.body.publicKey}, function (err, account) { + if (err) { + return setImmediate(cb, err); + } + + if (!account || !account.publicKey) { + return setImmediate(cb, 'Account not found'); + } + + if (account.secondSignature && !req.body.secondSecret) { + return setImmediate(cb, 'Missing second passphrase'); + } + + var secondKeypair = null; + + if (account.secondSignature) { + var secondHash = library.ed.createPassPhraseHash(req.body.secondSecret); + secondKeypair = library.ed.makeKeypair(secondHash); + } + + var transaction; + + try { + transaction = library.logic.transaction.normalize({ + type: transactionTypes.CHAT_MESSAGE, + amount: 0, + sender: account, + recipientId: recipientId, + message: req.body.message, + own_message: req.body.own_message, + message_type: req.body.message_type, + keypair: keypair, + secondKeypair: secondKeypair + }); + } catch (e) { + return setImmediate(cb, e.toString()); + } + return setImmediate(cb, null, {transaction: transaction}); + + }); + } + }); + + }); + }, + store: function (req, cb) { + library.schema.validate(req.body.transaction, schema.store, function (err) { + if (err) { + return setImmediate(cb, err[0].message); + } + + var query = {address: req.body.transaction.senderId}; + var keypair = {}; + library.balancesSequence.add(function (cb) { + + + if (req.body.multisigAccountPublicKey && req.body.multisigAccountPublicKey !== req.body.transaction.publicKey) { + modules.accounts.getAccount({publicKey: req.body.multisigAccountPublicKey}, function (err, account) { + if (err) { + return setImmediate(cb, err); + } + + if (!account || !account.publicKey) { + return setImmediate(cb, 'Multisignature account not found'); + } + + if (!Array.isArray(account.multisignatures)) { + return setImmediate(cb, 'Account does not have multisignatures enabled'); + } + + if (account.multisignatures.indexOf(keypair.publicKey.toString('hex')) < 0) { + return setImmediate(cb, 'Account does not belong to multisignature group'); + } + + modules.accounts.getAccount({publicKey: keypair.publicKey}, function (err, requester) { + if (err) { + return setImmediate(cb, err); + } + + if (!requester || !requester.publicKey) { + return setImmediate(cb, 'Requester not found'); + } + + if (requester.secondSignature && !req.body.secondSecret) { + return setImmediate(cb, 'Missing requester second passphrase'); + } + + if (requester.publicKey === account.publicKey) { + return setImmediate(cb, 'Invalid requester public key'); + } + + var secondKeypair = null; + + if (requester.secondSignature) { + var secondHash = library.ed.createPassPhraseHash(req.body.secondSecret); + secondKeypair = library.ed.makeKeypair(secondHash); + } + + var transaction; + + try { + transaction = library.logic.transaction.create({ + type: transactionTypes.SEND, + amount: req.body.amount, + sender: account, + recipientId: null, + keypair: null, + requester: null, + secondKeypair: secondKeypair + }); + } catch (e) { + return setImmediate(cb, e.toString()); + } + + modules.transactions.receiveTransactions([transaction], true, cb); + }); + }); + } else { + + modules.accounts.setAccountAndGet({publicKey: req.body.transaction.senderPublicKey}, function (err, account) { + if (err) { + return setImmediate(cb, err); + } + + if (!account || !account.publicKey) { + return setImmediate(cb, 'Account not found'); + } + + if (account.secondSignature && !req.body.secondSecret) { + return setImmediate(cb, 'Missing second passphrase'); + } + + var secondKeypair = null; + + if (account.secondSignature) { + var secondHash = library.ed.createPassPhraseHash(req.body.secondSecret); + secondKeypair = library.ed.makeKeypair(secondHash); + } + + var transaction; + + try { + transaction = library.logic.transaction.publish(req.body.transaction); + } catch (e) { + return setImmediate(cb, e.toString()); + } + + modules.transactions.receiveTransactions([transaction], true, cb); + }); + } + + }, function (err, transaction) { + if (err) { + return setImmediate(cb, err); + } + + return setImmediate(cb, null, {transactionId: transaction[0].id}); + }); + }); + }, + get: function (query, cb) { + __private.get(query.id, function (err, state) { + if (err) { + return setImmediate(cb, null, {success: false, error: err}); + } else { + return setImmediate(cb, null, {success: true, state: state}); + } + }); + } + +}; + +// Export +module.exports = States; diff --git a/schema/states.js b/schema/states.js new file mode 100644 index 00000000..87ebc198 --- /dev/null +++ b/schema/states.js @@ -0,0 +1,150 @@ +'use strict'; + +var constants = require('../helpers/constants.js'); + +module.exports = { + getTransactions: { + id: 'states.getTransactions', + type: 'object', + properties: { + blockId: { + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 + }, + type: { + type: 'integer', + minimum: 0, + maximum: 10 + }, + senderId: { + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 + }, + senderPublicKey: { + type: 'string', + format: 'publicKey' + }, + ownerPublicKey: { + type: 'string', + format: 'publicKey' + }, + ownerAddress: { + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 + }, + senderPublicKeys: { + type: 'array', + minItems: 1, + 'items': { + type: 'string', + format: 'publicKey' + } + }, + senderIds: { + type: 'array', + minItems: 1, + 'items': { + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 + } + }, + fromHeight: { + type: 'integer', + minimum: 1 + }, + toHeight: { + type: 'integer', + minimum: 1 + }, + fromTimestamp: { + type: 'integer', + minimum: 0 + }, + toTimestamp: { + type: 'integer', + minimum: 1 + }, + fromUnixTime: { + type: 'integer', + minimum: (constants.epochTime.getTime() / 1000) + }, + toUnixTime: { + type: 'integer', + minimum: (constants.epochTime.getTime() / 1000 + 1) + }, + minConfirmations: { + type: 'integer', + minimum: 0 + }, + orderBy: { + type: 'string' + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 1000 + }, + offset: { + type: 'integer', + minimum: 0 + } + } + }, + get: { + id: 'states.get', + type: 'object', + properties: { + id: { + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 + } + }, + required: ['id'] + }, + normalize: { + id: 'states.normalize', + type: 'object', + properties: { + value: { + type: 'string', + minLength: 1 + }, + key: { + type: 'string', + minLength: 0 + }, + recipientId: { + type: 'string', + format: 'address', + minLength: 1, + maxLength: 40 + }, + publicKey: { + type: 'string', + format: 'publicKey' + } + }, + required: ['value', 'publicKey'] + }, + store: { + id: 'states.store', + type: 'object', + properties: { + signature: { + type: 'string', + format: 'signature' + } + }, + required: ['signature'] + } +}; diff --git a/sql/migrations/20180330203200_createStates.sql b/sql/migrations/20180330203200_createStates.sql new file mode 100644 index 00000000..e115230d --- /dev/null +++ b/sql/migrations/20180330203200_createStates.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS "states"( + "stored_value" TEXT, + "stored_key" VARCHAR(20), + "type" INT NOT NULL, + "transactionId" VARCHAR(20) NOT NULL, + FOREIGN KEY("transactionId") REFERENCES "trs"("id") ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS "states_trs_id" ON "states"("transactionId"); \ No newline at end of file diff --git a/sql/migrations/20180330203400_recreateFullBlocksListView.sql b/sql/migrations/20180330203400_recreateFullBlocksListView.sql new file mode 100644 index 00000000..4e275e22 --- /dev/null +++ b/sql/migrations/20180330203400_recreateFullBlocksListView.sql @@ -0,0 +1,66 @@ +DROP VIEW IF EXISTS full_blocks_list; + +CREATE VIEW full_blocks_list AS + + SELECT b."id" AS "b_id", + b."version" AS "b_version", + b."timestamp" AS "b_timestamp", + b."height" AS "b_height", + b."previousBlock" AS "b_previousBlock", + b."numberOfTransactions" AS "b_numberOfTransactions", + (b."totalAmount")::bigint AS "b_totalAmount", + (b."totalFee")::bigint AS "b_totalFee", + (b."reward")::bigint AS "b_reward", + b."payloadLength" AS "b_payloadLength", + ENCODE(b."payloadHash", 'hex') AS "b_payloadHash", + ENCODE(b."generatorPublicKey", 'hex') AS "b_generatorPublicKey", + ENCODE(b."blockSignature", 'hex') AS "b_blockSignature", + t."id" AS "t_id", + t."rowId" AS "t_rowId", + t."type" AS "t_type", + t."timestamp" AS "t_timestamp", + ENCODE(t."senderPublicKey", 'hex') AS "t_senderPublicKey", + t."senderId" AS "t_senderId", + t."recipientId" AS "t_recipientId", + (t."amount")::bigint AS "t_amount", + (t."fee")::bigint AS "t_fee", + ENCODE(t."signature", 'hex') AS "t_signature", + ENCODE(t."signSignature", 'hex') AS "t_signSignature", + ENCODE(s."publicKey", 'hex') AS "s_publicKey", + d."username" AS "d_username", + v."votes" AS "v_votes", + m."min" AS "m_min", + m."lifetime" AS "m_lifetime", + m."keysgroup" AS "m_keysgroup", + dapp."name" AS "dapp_name", + dapp."description" AS "dapp_description", + dapp."tags" AS "dapp_tags", + dapp."type" AS "dapp_type", + dapp."link" AS "dapp_link", + dapp."category" AS "dapp_category", + dapp."icon" AS "dapp_icon", + it."dappId" AS "in_dappId", + ot."dappId" AS "ot_dappId", + ot."outTransactionId" AS "ot_outTransactionId", + ENCODE(t."requesterPublicKey", 'hex') AS "t_requesterPublicKey", + t."signatures" AS "t_signatures", + c."message" AS "c_message", + c."own_message" AS "c_own_message", + c."type" AS "c_type", + st."type" as "st_type", + st."stored_value" as "st_stored_value", + st."stored_key" as "st_stored_key" + + FROM blocks b + + LEFT OUTER JOIN trs AS t ON t."blockId" = b."id" + LEFT OUTER JOIN delegates AS d ON d."transactionId" = t."id" + LEFT OUTER JOIN votes AS v ON v."transactionId" = t."id" + LEFT OUTER JOIN signatures AS s ON s."transactionId" = t."id" + LEFT OUTER JOIN multisignatures AS m ON m."transactionId" = t."id" + LEFT OUTER JOIN dapps AS dapp ON dapp."transactionId" = t."id" + LEFT OUTER JOIN intransfer AS it ON it."transactionId" = t."id" + LEFT OUTER JOIN outtransfer AS ot ON ot."transactionId" = t."id" + LEFT OUTER JOIN chats AS c ON c."transactionId" = t."id" + LEFT OUTER JOIN states AS st ON st."transactionId" = t."id"; + diff --git a/sql/states.js b/sql/states.js new file mode 100644 index 00000000..cb5655be --- /dev/null +++ b/sql/states.js @@ -0,0 +1,37 @@ +'use strict'; + +var StatesSql = { + sortFields: ['type','timestamp'], + + countByTransactionId: 'SELECT COUNT(*)::int AS "count" FROM states WHERE "transactionId" = ${id}', + + + search: function (params) { + return [ + 'SELECT "transactionId", "stored_value", "stored_key", "type" ', + 'FROM states WHERE to_tsvector("stored_value" || \' \' || "stored_key" || \' \' ) @@ to_tsquery(${q})', + '', + 'LIMIT ${limit}' + ].filter(Boolean).join(' '); + }, + + get: 'SELECT "stored_value", "stored_key", "type", "transactionId" FROM states WHERE "transactionId" = ${id}', + + getByIds: 'SELECT "stored_value", "stored_key", "type", "transactionId" FROM states WHERE "transactionId" IN ($1:csv)', + + // Need to fix "or" or "and" in query + list: function (params) { + return [ + + 'SELECT *, t_timestamp as timestamp FROM full_blocks_list', + (params.where.length ? 'WHERE ' + params.where.join(' AND ') : ''), + (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') : ''), + 'LIMIT ${limit} OFFSET ${offset}' + ].filter(Boolean).join(' '); + }, + + getGenesis: 'SELECT b."height" AS "height", b."id" AS "id", t."senderId" AS "authorId" FROM trs t INNER JOIN blocks b ON t."blockId" = b."id" WHERE t."id" = ${id}' + +}; + +module.exports = StatesSql;