diff --git a/contracts/roles.js b/contracts/roles.js new file mode 100644 index 00000000..56d5978f --- /dev/null +++ b/contracts/roles.js @@ -0,0 +1,917 @@ +/* eslint-disable max-len */ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable no-await-in-loop */ +/* global actions, api */ + +const ContractName = 'roles'; +const FeeMethod = ['burn', 'issuer']; + +actions.createSSC = async () => { + const tableExists = await api.db.tableExists('instances'); + if (tableExists === false) { + await api.db.createTable('instances', ['id', 'lastTickTime']); + await api.db.createTable('roles', [ + 'instanceId', + { name: 'byLastTickTime', index: { instanceId: 1, active: 1, lastTickTime: 1 } }, + ]); + await api.db.createTable('candidates', [ + 'account', + { name: 'byAccountRole', index: { roleId: 1, account: 1 } }, + { name: 'byApprovalWeight', index: { roleId: 1, approvalWeight: 1, active: 1 } }, + ]); + await api.db.createTable('approvals', ['from', 'to']); + await api.db.createTable('accounts', [], { primaryKey: ['account'] }); + await api.db.createTable('params'); + + const params = {}; + params.instanceCreationFee = '500'; + params.instanceUpdateFee = '100'; + params.instanceTickHours = '24'; + params.roleCreationFee = '50'; + params.roleUpdateFee = '25'; + params.maxSlots = 10; + params.maxInstancesPerBlock = 1; + params.maxRolesPerBlock = 4; + params.maxAccountApprovals = 50; + params.processQueryLimit = 1000; + await api.db.insert('params', params); + } +}; + +actions.updateParams = async (payload) => { + const { + instanceCreationFee, + instanceUpdateFee, + instanceTickHours, + roleCreationFee, + roleUpdateFee, + maxSlots, + maxInstancesPerBlock, + maxRolesPerBlock, + maxAccountApprovals, + processQueryLimit, + } = payload; + + if (api.sender !== api.owner) return; + const params = await api.db.findOne('params', {}); + if (instanceCreationFee) { + if (!api.assert(typeof instanceCreationFee === 'string' && !api.BigNumber(instanceCreationFee).isNaN() && api.BigNumber(instanceCreationFee).gte(0), 'invalid instanceCreationFee')) return; + params.instanceCreationFee = instanceCreationFee; + } + if (instanceUpdateFee) { + if (!api.assert(typeof instanceUpdateFee === 'string' && !api.BigNumber(instanceUpdateFee).isNaN() && api.BigNumber(instanceUpdateFee).gte(0), 'invalid instanceUpdateFee')) return; + params.instanceUpdateFee = instanceUpdateFee; + } + if (instanceTickHours) { + if (!api.assert(typeof instanceTickHours === 'string' && api.BigNumber(instanceTickHours).isInteger() && api.BigNumber(instanceTickHours).gte(1), 'invalid instanceTickHours')) return; + params.instanceTickHours = instanceTickHours; + } + if (roleCreationFee) { + if (!api.assert(typeof roleCreationFee === 'string' && !api.BigNumber(roleCreationFee).isNaN() && api.BigNumber(roleCreationFee).gte(0), 'invalid roleCreationFee')) return; + params.roleCreationFee = roleCreationFee; + } + if (roleUpdateFee) { + if (!api.assert(typeof roleUpdateFee === 'string' && !api.BigNumber(roleUpdateFee).isNaN() && api.BigNumber(roleUpdateFee).gte(0), 'invalid roleUpdateFee')) return; + params.roleUpdateFee = roleUpdateFee; + } + if (maxSlots) { + if (!api.assert(typeof maxSlots === 'string' && api.BigNumber(maxSlots).isInteger() && api.BigNumber(maxSlots).gte(1), 'invalid maxSlots')) return; + params.maxSlots = api.BigNumber(maxSlots).toNumber(); + } + if (maxInstancesPerBlock) { + if (!api.assert(typeof maxInstancesPerBlock === 'string' && api.BigNumber(maxInstancesPerBlock).isInteger() && api.BigNumber(maxInstancesPerBlock).gte(1), 'invalid maxInstancesPerBlock')) return; + params.maxInstancesPerBlock = api.BigNumber(maxInstancesPerBlock).toNumber(); + } + if (maxRolesPerBlock) { + if (!api.assert(typeof maxRolesPerBlock === 'string' && api.BigNumber(maxRolesPerBlock).isInteger() && api.BigNumber(maxRolesPerBlock).gte(1), 'invalid maxRolesPerBlock')) return; + params.maxRolesPerBlock = api.BigNumber(maxRolesPerBlock).toNumber(); + } + if (maxAccountApprovals) { + if (!api.assert(typeof maxAccountApprovals === 'string' && api.BigNumber(maxAccountApprovals).isInteger() && api.BigNumber(maxAccountApprovals).gte(1), 'invalid maxAccountApprovals')) return; + params.maxAccountApprovals = api.BigNumber(maxAccountApprovals).toNumber(); + } + if (processQueryLimit) { + if (!api.assert(typeof processQueryLimit === 'string' && api.BigNumber(processQueryLimit).isInteger() && api.BigNumber(processQueryLimit).gte(1), 'invalid processQueryLimit')) return; + params.processQueryLimit = api.BigNumber(processQueryLimit).toNumber(); + } + await api.db.update('params', params); +}; + +async function updateCandidateWeight(id, deltaApprovalWeight, deltaToken = null) { + const candidate = await api.db.findOne('candidates', { _id: id }); + if (candidate) { + if (deltaToken) { + const role = await api.db.findOne('roles', { _id: candidate.roleId }); + const inst = await api.db.findOne('instances', { _id: role.instanceId }); + if (inst.voteToken !== deltaToken.symbol) return true; + } + candidate.approvalWeight = { + $numberDecimal: api.BigNumber(candidate.approvalWeight.$numberDecimal) + .plus(deltaApprovalWeight), + }; + await api.db.update('candidates', candidate); + + const role = await api.db.findOne('roles', { _id: candidate.roleId }); + role.totalApprovalWeight = { + $numberDecimal: api.BigNumber(role.totalApprovalWeight.$numberDecimal) + .plus(deltaApprovalWeight), + }; + await api.db.update('roles', role); + + return true; + } + return false; +} + +actions.createInstance = async (payload) => { + const { + voteToken, candidateFee, isSignedWithActiveKey, + } = payload; + + const params = await api.db.findOne('params', {}); + const { instanceCreationFee } = params; + + // eslint-disable-next-line no-template-curly-in-string + const utilityTokenBalance = await api.db.findOneInTable('tokens', 'balances', { account: api.sender, symbol: "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'" }); + + const authorizedCreation = api.BigNumber(instanceCreationFee).lte(0) || api.sender === api.owner + ? true + : utilityTokenBalance && api.BigNumber(utilityTokenBalance.balance).gte(instanceCreationFee); + + if (api.assert(authorizedCreation, 'you must have enough tokens to cover the creation fee') + && api.assert(isSignedWithActiveKey === true, 'you must use a transaction signed with your active key')) { + if (candidateFee) { + if (!api.assert(typeof candidateFee === 'object' + && typeof candidateFee.method === 'string' + && FeeMethod.indexOf(candidateFee.method) !== -1 + && typeof candidateFee.symbol === 'string' + && typeof candidateFee.amount === 'string' && api.BigNumber(candidateFee.amount).gte(0), 'invalid candidateFee properties')) return; + const feeTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: candidateFee.symbol }); + if (!api.assert(feeTokenObj && api.BigNumber(candidateFee.amount).dp() <= feeTokenObj.precision, 'invalid candidateFee token or precision')) return; + } + + const voteTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: voteToken }); + if (!api.assert(voteTokenObj && voteTokenObj.stakingEnabled, 'voteToken must have staking enabled')) return; + + const now = new Date(`${api.hiveBlockTimestamp}.000Z`); + const newInstance = { + voteToken, + candidateFee, + active: false, + creator: api.sender, + lastTickTime: now.getTime(), + }; + const insertedInst = await api.db.insert('instances', newInstance); + + if (api.sender !== api.owner + && api.sender !== 'null' + && api.BigNumber(instanceCreationFee).gt(0)) { + await api.executeSmartContract('tokens', 'transfer', { + // eslint-disable-next-line no-template-curly-in-string + to: 'null', symbol: "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'", quantity: instanceCreationFee, isSignedWithActiveKey, + }); + } + api.emit('createInstance', { id: insertedInst._id }); + } +}; + +actions.updateInstance = async (payload) => { + const { + instanceId, candidateFee, isSignedWithActiveKey, + } = payload; + + const params = await api.db.findOne('params', {}); + const { instanceUpdateFee } = params; + + // eslint-disable-next-line no-template-curly-in-string + const utilityTokenBalance = await api.db.findOneInTable('tokens', 'balances', { account: api.sender, symbol: "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'" }); + + const authorizedUpdate = api.BigNumber(instanceUpdateFee).lte(0) || api.sender === api.owner + ? true + : utilityTokenBalance && api.BigNumber(utilityTokenBalance.balance).gte(instanceUpdateFee); + + if (api.assert(authorizedUpdate, 'you must have enough tokens to cover the update fee') + && api.assert(isSignedWithActiveKey === true, 'you must use a transaction signed with your active key') + && api.assert(typeof instanceId === 'string' && api.BigNumber(instanceId).isInteger(), 'invalid instanceId') + && api.assert(candidateFee, 'specify at least one field to update')) { + const existingInst = await api.db.findOne('instances', { _id: api.BigNumber(instanceId).toNumber() }); + if (!api.assert(existingInst, 'instance not found') + || !api.assert(existingInst.creator === api.sender || api.owner === api.sender, 'must be instance creator')) return; + + if (candidateFee) { + if (!api.assert(typeof candidateFee === 'object' + && typeof candidateFee.method === 'string' + && FeeMethod.indexOf(candidateFee.method) !== -1 + && typeof candidateFee.symbol === 'string' + && typeof candidateFee.amount === 'string' && api.BigNumber(candidateFee.amount).gte(0), 'invalid candidateFee object')) return; + const feeTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: candidateFee.symbol }); + if (!api.assert(feeTokenObj && api.BigNumber(candidateFee.amount).dp() <= feeTokenObj.precision, 'invalid candidateFee token or precision')) return; + existingInst.candidateFee = candidateFee; + } + + await api.db.update('instances', existingInst); + + // burn the token update fees + if (api.sender !== api.owner + && api.sender !== 'null' + && api.BigNumber(instanceUpdateFee).gt(0)) { + await api.executeSmartContract('tokens', 'transfer', { + // eslint-disable-next-line no-template-curly-in-string + to: 'null', symbol: "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'", quantity: instanceUpdateFee, isSignedWithActiveKey, + }); + } + api.emit('updateInstance', { id: instanceId }); + } +}; + +actions.createRoles = async (payload) => { + const { + instanceId, roles, isSignedWithActiveKey, + } = payload; + + const params = await api.db.findOne('params', {}); + const { roleCreationFee } = params; + + // eslint-disable-next-line no-template-curly-in-string + const utilityTokenBalance = await api.db.findOneInTable('tokens', 'balances', { account: api.sender, symbol: "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'" }); + + const authorizedCreation = api.BigNumber(roleCreationFee).lte(0) || api.sender === api.owner + ? true + : utilityTokenBalance && api.BigNumber(utilityTokenBalance.balance).gte(roleCreationFee); + + if (api.assert(authorizedCreation, 'you must have enough tokens to cover the creation fee') + && api.assert(isSignedWithActiveKey === true, 'you must use a transaction signed with your active key') + && api.assert(typeof instanceId === 'string' && api.BigNumber(instanceId).isInteger(), 'invalid instanceId') + && api.assert(typeof roles === 'object' && Array.isArray(roles) && roles.length > 0 && roles.length <= 50, 'invalid roles object')) { + const existingInst = await api.db.findOne('instances', { _id: api.BigNumber(instanceId).toNumber() }); + if (!api.assert(existingInst, 'instance not found') + || !api.assert(existingInst.creator === api.sender || api.owner === api.sender, 'must be instance creator')) return; + + const voteTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: existingInst.voteToken }); + for (let i = 0; i < roles.length; i += 1) { + const role = roles[i]; + if (!api.assert(Object.keys(role).length === 5 + && typeof role.name === 'string' && role.name.length < 50 + && typeof role.voteThreshold === 'string' && api.BigNumber(role.voteThreshold).gte(0) && api.BigNumber(role.voteThreshold).dp() <= voteTokenObj.precision + && typeof role.mainSlots === 'string' && api.BigNumber(role.mainSlots).isInteger() && api.BigNumber(role.mainSlots).gt(0) && api.BigNumber(role.mainSlots).lte(params.maxSlots) + && typeof role.backupSlots === 'string' && api.BigNumber(role.backupSlots).isInteger() && api.BigNumber(role.backupSlots).gte(0) && api.BigNumber(role.backupSlots).lte(api.BigNumber(params.maxSlots).minus(role.mainSlots)) + && typeof role.tickHours === 'string' && api.BigNumber(role.tickHours).isInteger() && api.BigNumber(role.tickHours).gte(params.instanceTickHours) && api.BigNumber(role.tickHours).mod(params.instanceTickHours).eq(0), 'invalid roles properties')) return; + } + + const insertedRoles = []; + for (let i = 0; i < roles.length; i += 1) { + const newRole = { + instanceId: existingInst._id, + ...roles[i], + active: true, + lastTickTime: 0, + totalApprovalWeight: { $numberDecimal: '0' }, + }; + const insertedRole = await api.db.insert('roles', newRole); + insertedRoles.push({ instanceId: insertedRole.instanceId, roleId: insertedRole._id, name: insertedRole.name }); + } + + if (api.sender !== api.owner + && api.sender !== 'null' + && api.BigNumber(roleCreationFee).gt(0)) { + await api.executeSmartContract('tokens', 'transfer', { + // eslint-disable-next-line no-template-curly-in-string + to: 'null', symbol: "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'", quantity: roleCreationFee, isSignedWithActiveKey, + }); + } + api.emit('createRoles', { roles: insertedRoles }); + } +}; + +actions.updateRole = async (payload) => { + const { + roleId, + active, name, voteThreshold, + mainSlots, backupSlots, tickHours, + isSignedWithActiveKey, + } = payload; + + const params = await api.db.findOne('params', {}); + const { roleUpdateFee } = params; + + // eslint-disable-next-line no-template-curly-in-string + const utilityTokenBalance = await api.db.findOneInTable('tokens', 'balances', { account: api.sender, symbol: "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'" }); + + const authorizedUpdate = api.BigNumber(roleUpdateFee).lte(0) || api.sender === api.owner + ? true + : utilityTokenBalance && api.BigNumber(utilityTokenBalance.balance).gte(roleUpdateFee); + + if (api.assert(authorizedUpdate, 'you must have enough tokens to cover the update fee') + && api.assert(isSignedWithActiveKey === true, 'you must use a transaction signed with your active key') + && api.assert(typeof roleId === 'string' && api.BigNumber(roleId).isInteger(), 'invalid roleId') + && api.assert(typeof active !== 'undefined' || name || voteThreshold || mainSlots || backupSlots || tickHours, 'specify at least one field to update')) { + const existingRole = await api.db.findOne('roles', { _id: api.BigNumber(roleId).toNumber() }); + const existingInst = await api.db.findOne('instances', { _id: existingRole.instanceId }); + if (!api.assert(existingRole, 'role not found') || !api.assert(existingInst, 'instance not found') + || !api.assert(existingInst.creator === api.sender || api.owner === api.sender, 'must be instance creator')) return; + + if (typeof active !== 'undefined') { + existingRole.active = !!active; + } + if (name) { + if (!api.assert(typeof name === 'string' && name.length < 50, 'name must be a string less than 50 characters')) return; + existingRole.name = name; + } + if (voteThreshold) { + const voteTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: existingInst.voteToken }); + if (!api.assert(typeof voteThreshold === 'string' + && api.BigNumber(voteThreshold).gte(0) + && api.BigNumber(voteThreshold).dp() <= voteTokenObj.precision, 'voteThreshold must be greater than or equal to 0, precision matching voteToken')) return; + existingRole.voteThreshold = voteThreshold; + } + if (mainSlots) { + if (!api.assert(typeof mainSlots === 'string' + && api.BigNumber(mainSlots).isInteger() + && api.BigNumber(mainSlots).gt(0) + && api.BigNumber(mainSlots).lte(params.maxSlots), 'mainSlots must be a integer between 1 - params.maxSlots')) return; + existingRole.mainSlots = mainSlots; + } + if (backupSlots) { + const remainingSlots = api.BigNumber(params.maxSlots).minus(existingRole.mainSlots); + if (!api.assert(typeof backupSlots === 'string' + && api.BigNumber(backupSlots).isInteger() + && api.BigNumber(backupSlots).gte(0) + && api.BigNumber(backupSlots).lte(remainingSlots), 'backupSlots must be an integer between 0 - remainingSlots')) return; + existingRole.backupSlots = backupSlots; + } + if (tickHours) { + if (!api.assert(typeof tickHours === 'string' + && api.BigNumber(tickHours).isInteger() + && api.BigNumber(tickHours).gte(params.instanceTickHours) + && api.BigNumber(tickHours).mod(params.instanceTickHours).eq(0), 'tickHours must be an integer greater than or equal to, and a multiple of params.instanceTickHours')) return; + existingRole.tickHours = tickHours; + } + + await api.db.update('roles', existingRole); + + // burn the token update fees + if (api.sender !== api.owner + && api.sender !== 'null' + && api.BigNumber(roleUpdateFee).gt(0)) { + await api.executeSmartContract('tokens', 'transfer', { + // eslint-disable-next-line no-template-curly-in-string + to: 'null', symbol: "'${CONSTANTS.UTILITY_TOKEN_SYMBOL}$'", quantity: roleUpdateFee, isSignedWithActiveKey, + }); + } + api.emit('updateRole', { roleId: existingRole._id }); + } +}; + +actions.setInstanceActive = async (payload) => { + const { + instanceId, + active, + isSignedWithActiveKey, + } = payload; + + if (!api.assert(isSignedWithActiveKey === true, 'you must use a transaction signed with your active key') + || !api.assert(typeof instanceId === 'string' && api.BigNumber(instanceId).isInteger(), 'invalid instanceId')) { + return; + } + const inst = await api.db.findOne('instances', { _id: api.BigNumber(instanceId).toNumber() }); + if (api.assert(inst, 'instance does not exist') + && api.assert(inst.creator === api.sender || api.owner === api.sender, 'must be instance creator')) { + inst.active = !!active; + await api.db.update('instances', inst); + api.emit('setInstanceActive', { instanceId: inst._id, active: inst.active }); + } +}; + +actions.setRoleActive = async (payload) => { + const { + roleId, + active, + isSignedWithActiveKey, + } = payload; + + if (!api.assert(isSignedWithActiveKey === true, 'you must use a transaction signed with your active key') + || !api.assert(typeof roleId === 'string' && api.BigNumber(roleId).isInteger(), 'invalid roleId')) { + return; + } + const existingRole = await api.db.findOne('roles', { _id: api.BigNumber(roleId).toNumber() }); + const existingInst = await api.db.findOne('instances', { _id: existingRole.instanceId }); + if (api.assert(existingRole, 'role does not exist') + && api.assert(existingInst.creator === api.sender || api.owner === api.sender, 'must be instance creator')) { + existingRole.active = !!active; + await api.db.update('roles', existingRole); + api.emit('setRoleActive', { roleId: existingRole._id, active: existingRole.active }); + } +}; + +actions.applyForRole = async (payload) => { + const { + roleId, + isSignedWithActiveKey, + } = payload; + + if (!api.assert(isSignedWithActiveKey === true, 'you must use a transaction signed with your active key') + && !api.assert(typeof roleId === 'string', 'invalid roleId')) { + return; + } + const role = await api.db.findOne('roles', { _id: api.BigNumber(roleId).toNumber() }); + if (!api.assert(role, 'role does not exist')) return; + const inst = await api.db.findOne('instances', { _id: role.instanceId }); + + let authorizedCreation = true; + if (inst.candidateFee) { + const feeTokenBalance = await api.db.findOneInTable('tokens', 'balances', { account: api.sender, symbol: inst.candidateFee.symbol }); + authorizedCreation = api.BigNumber(inst.candidateFee.amount).lte(0) || api.sender === api.owner + ? true + : feeTokenBalance && api.BigNumber(feeTokenBalance.balance).gte(inst.candidateFee.amount); + } + + const existingApply = await api.db.findOne('candidates', { roleId: role._id, account: api.sender }); + if (api.assert(authorizedCreation, 'you must have enough tokens to cover the application fee') + && api.assert(!existingApply, 'sender already applied for role')) { + const newCandidate = { + roleId: role._id, + account: api.sender, + active: true, + approvalWeight: { $numberDecimal: '0' }, + }; + const insertedId = await api.db.insert('candidates', newCandidate); + + if (api.sender !== api.owner && inst.candidateFee) { + if (inst.candidateFee.method === 'burn') { + await api.executeSmartContract('tokens', 'transfer', { + to: 'null', symbol: inst.candidateFee.symbol, quantity: inst.candidateFee.amount, + }); + } else if (inst.candidateFee.method === 'issuer') { + const feeTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: inst.candidateFee.symbol }); + await api.executeSmartContract('tokens', 'transfer', { + to: feeTokenObj.issuer, symbol: inst.candidateFee.symbol, quantity: inst.candidateFee.amount, + }); + } + } + + api.emit('applyForRole', { roleId: role._id, candidateId: insertedId._id }); + } +}; + +actions.setApplyActive = async (payload) => { + const { + roleId, + active, + isSignedWithActiveKey, + } = payload; + + if (!api.assert(isSignedWithActiveKey === true, 'you must use a transaction signed with your active key') + && !api.assert(typeof roleId === 'string', 'invalid roleId')) { + return; + } + const role = await api.db.findOne('roles', { _id: api.BigNumber(roleId).toNumber() }); + const existingApply = await api.db.findOne('candidates', { roleId: role._id, account: api.sender }); + if (api.assert(role, 'role does not exist') + && api.assert(existingApply, 'candidate does not exist for sender')) { + existingApply.active = !!active; + await api.db.update('candidates', existingApply); + api.emit('setApplyActive', { roleId: role._id, account: existingApply.account, active }); + } +}; + +// deposit + +async function updateTokenBalances(role, token, quantity) { + const upRole = role; + if (upRole.tokenBalances) { + const tIndex = upRole.tokenBalances.findIndex(t => t.symbol === token.symbol); + if (tIndex === -1) { + upRole.tokenBalances.push({ symbol: token.symbol, quantity }); + } else { + upRole.tokenBalances[tIndex].quantity = api.BigNumber(upRole.tokenBalances[tIndex].quantity) + .plus(quantity) + .toFixed(token.precision, api.BigNumber.ROUND_DOWN); + } + } else { + upRole.tokenBalances = [ + { symbol: token.symbol, quantity }, + ]; + } + await api.db.update('roles', upRole); +} + +actions.deposit = async (payload) => { + const { + roleId, symbol, quantity, + isSignedWithActiveKey, + } = payload; + + const depToken = await api.db.findOneInTable('tokens', 'tokens', { symbol }); + if (!api.assert(isSignedWithActiveKey === true, 'you must use a custom_json signed with your active key') + || !api.assert(typeof roleId === 'string' && api.BigNumber(roleId).isInteger(), 'invalid roleId') + || !api.assert(typeof quantity === 'string' && api.BigNumber(quantity).gt(0), 'invalid quantity') + || !api.assert(api.BigNumber(quantity).dp() <= depToken.precision, 'quantity precision mismatch')) { + return; + } + + const role = await api.db.findOne('roles', { _id: api.BigNumber(roleId).toNumber() }); + if (api.assert(role, 'role not found') && api.assert(role.active, 'role must be active to deposit')) { + const res = await api.executeSmartContract('tokens', 'transferToContract', { symbol, quantity, to: ContractName }); + if (res.errors === undefined + && res.events && res.events.find(el => el.contract === 'tokens' && el.event === 'transferToContract' && el.data.from === api.sender && el.data.to === ContractName && el.data.quantity === quantity) !== undefined) { + await updateTokenBalances(role, depToken, quantity); + api.emit('deposit', { + roleId, + symbol, + quantity, + }); + } + } +}; + +actions.receiveDtfTokens = async (payload) => { + const { + data, symbol, quantity, + callingContractInfo, + } = payload; + + if (!api.assert(callingContractInfo && callingContractInfo.name === 'tokenfunds', 'not authorized')) return; + if (!api.assert(typeof data === 'object' + && data.constructor.name === 'Object' + && 'roleId' in data && typeof data.roleId === 'string' + && api.BigNumber(data.roleId).isInteger(), 'invalid incoming payload')) return; + + const role = await api.db.findOne('roles', { _id: api.BigNumber(data.roleId).toNumber() }); + if (api.assert(role, 'role not found') && api.assert(role.active, 'role must be active to deposit')) { + const depToken = await api.db.findOneInTable('tokens', 'tokens', { symbol }); + await updateTokenBalances(role, depToken, quantity); + api.emit('receiveDtfTokens', { + roleId: data.roleId, + symbol, + quantity, + }); + } +}; + +actions.receiveDistTokens = async (payload) => { + const { + data, symbol, quantity, + callingContractInfo, + } = payload; + + if (!api.assert(callingContractInfo && callingContractInfo.name === 'distribution', 'not authorized')) return; + if (!api.assert(typeof data === 'object' + && data.constructor.name === 'Object' + && 'roleId' in data && typeof data.roleId === 'string' + && api.BigNumber(data.roleId).isInteger(), 'invalid incoming payload')) return; + + const role = await api.db.findOne('roles', { _id: api.BigNumber(data.roleId).toNumber() }); + if (api.assert(role, 'role not found') && api.assert(role.active, 'role must be active to deposit')) { + const depToken = await api.db.findOneInTable('tokens', 'tokens', { symbol }); + await updateTokenBalances(role, depToken, quantity); + api.emit('receiveDistTokens', { + roleId: data.roleId, + symbol, + quantity, + }); + } +}; + +// voting + +actions.approveCandidate = async (payload) => { + const { id } = payload; + const params = await api.db.findOne('params', {}); + + if (api.assert(typeof id === 'string' && api.BigNumber(id).isInteger(), 'invalid id')) { + const candidate = await api.db.findOne('candidates', { _id: api.BigNumber(id).toNumber() }); + + if (api.assert(candidate, 'candidate does not exist') + && api.assert(candidate.active, 'candidate is not active')) { + const role = await api.db.findOne('roles', { _id: candidate.roleId }); + if (!api.assert(role.active, 'role must be active to approve')) return; + + const inst = await api.db.findOne('instances', { _id: role.instanceId }); + const voteTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: inst.voteToken }); + let acct = await api.db.findOne('accounts', { account: api.sender }); + if (acct === null) { + acct = { + account: api.sender, + weights: [], + }; + acct = await api.db.insert('accounts', acct); + } + + let activeApprovals = 0; + const approvals = await api.db.find('approvals', + { from: api.sender, candidatePending: true }, + params.maxAccountApprovals, + 0, + [{ index: '_id', descending: true }]); + for (let index = 0; index < approvals.length; index += 1) { + const approval = approvals[index]; + const approvalCandidate = await api.db.findOne('candidates', { _id: approval.to }); + if (approvalCandidate && approvalCandidate.active) { + activeApprovals += 1; + } else { + approval.candidatePending = false; + await api.db.update('approvals', approval); + } + } + if (!api.assert(activeApprovals < params.maxAccountApprovals, `you can only approve ${params.maxAccountApprovals} active candidates`)) return; + + let approval = await api.db.findOne('approvals', { from: api.sender, to: candidate._id }); + if (api.assert(approval === null, 'you already approved this candidate')) { + approval = { + from: api.sender, + to: candidate._id, + candidatePending: true, + }; + await api.db.insert('approvals', approval); + + const balance = await api.db.findOneInTable('tokens', 'balances', { account: api.sender, symbol: inst.voteToken }); + let approvalWeight = 0; + if (balance && balance.stake) { + approvalWeight = balance.stake; + } + if (balance && balance.delegationsIn) { + approvalWeight = api.BigNumber(approvalWeight) + .plus(balance.delegationsIn) + .toFixed(voteTokenObj.precision, api.BigNumber.ROUND_HALF_UP); + } + const wIndex = acct.weights.findIndex(x => x.symbol === inst.voteToken); + if (wIndex !== -1) { + acct.weights[wIndex].weight = approvalWeight; + } else { + acct.weights.push({ symbol: inst.voteToken, weight: approvalWeight }); + } + await api.db.update('accounts', acct); + await updateCandidateWeight(candidate._id, approvalWeight); + api.emit('approveCandidate', { id: candidate._id }); + } + } + } +}; + +actions.disapproveCandidate = async (payload) => { + const { id } = payload; + + if (api.assert(typeof id === 'string' && api.BigNumber(id).isInteger(), 'invalid id')) { + const candidate = await api.db.findOne('candidates', { _id: api.BigNumber(id).toNumber() }); + if (api.assert(candidate, 'candidate does not exist')) { + const inst = await api.db.findOne('instances', { id: candidate.instanceId }); + const voteTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: inst.voteToken }); + let acct = await api.db.findOne('accounts', { account: api.sender }); + if (acct === null) { + acct = { + account: api.sender, + weights: [], + }; + acct = await api.db.insert('accounts', acct); + } + + const approval = await api.db.findOne('approvals', { from: api.sender, to: candidate._id }); + if (api.assert(approval !== null, 'you have not approved this candidate')) { + await api.db.remove('approvals', approval); + + const balance = await api.db.findOneInTable('tokens', 'balances', { account: api.sender, symbol: inst.voteToken }); + let approvalWeight = 0; + if (balance && balance.stake) { + approvalWeight = balance.stake; + } + if (balance && balance.delegationsIn) { + approvalWeight = api.BigNumber(approvalWeight) + .plus(balance.delegationsIn) + .toFixed(voteTokenObj.precision, api.BigNumber.ROUND_HALF_UP); + } + const wIndex = acct.weights.findIndex(x => x.symbol === inst.voteToken); + if (wIndex !== -1) { + acct.weights[wIndex].weight = approvalWeight; + } else { + acct.weights.push({ symbol: inst.voteToken, weight: approvalWeight }); + } + await api.db.update('accounts', acct); + await updateCandidateWeight(candidate._id, api.BigNumber(approvalWeight).negated()); + api.emit('disapproveCandidate', { id: candidate._id }); + } + } + } +}; + +actions.updateCandidateApprovals = async (payload) => { + const { account, token, callingContractInfo } = payload; + + if (callingContractInfo === undefined) return; + if (callingContractInfo.name !== 'tokens') return; + + const acct = await api.db.findOne('accounts', { account }); + if (acct !== null) { + const params = await api.db.findOne('params', {}); + + // only update existing weights + const wIndex = acct.weights.findIndex(x => x.symbol === token.symbol); + if (wIndex !== -1) { + // calculate approval weight of the account + const balance = await api.db.findOneInTable('tokens', 'balances', { account, symbol: token.symbol }); + let approvalWeight = 0; + if (balance && balance.stake) { + approvalWeight = balance.stake; + } + + if (balance && balance.delegationsIn) { + approvalWeight = api.BigNumber(approvalWeight) + .plus(balance.delegationsIn) + .toFixed(token.precision, api.BigNumber.ROUND_HALF_UP); + } + + let oldApprovalWeight = 0; + oldApprovalWeight = acct.weights[wIndex].weight; + acct.weights[wIndex].weight = approvalWeight; + + const deltaApprovalWeight = api.BigNumber(approvalWeight) + .minus(oldApprovalWeight) + .dp(token.precision, api.BigNumber.ROUND_HALF_UP); + + if (!api.BigNumber(deltaApprovalWeight).eq(0)) { + await api.db.update('accounts', acct); + const approvals = await api.db.find('approvals', + { from: account, candidatePending: true }, + params.maxAccountApprovals, + 0, + [{ index: '_id', descending: true }]); + for (let index = 0; index < approvals.length; index += 1) { + const approval = approvals[index]; + const candidatePending = await updateCandidateWeight(approval.to, + deltaApprovalWeight, + token); + if (!candidatePending) { + approval.candidatePending = false; + await api.db.update('approvals', approval); + } + } + } + } + } +}; + +// ticks + +async function payRecipient(account, symbol, quantity, type = 'user', contractPayload = null) { + if (api.BigNumber(quantity).gt(0)) { + const res = await api.transferTokens(account, symbol, quantity, type); + if (type === 'contract' && contractPayload) { + await api.executeSmartContract(account, 'receiveRolesTokens', + { data: contractPayload, symbol, quantity }); + } + if (res.errors) { + api.debug(`Error paying out roles of ${quantity} ${symbol} to ${account} (TXID ${api.transactionId}): \n${res.errors}`); + return false; + } + return true; + } + return false; +} + +async function checkPendingCandidates(inst, params) { + const random = api.random(); + const blockDate = new Date(`${api.hiveBlockTimestamp}.000Z`); + const upInst = JSON.parse(JSON.stringify(inst)); + const voteTokenObj = await api.db.findOneInTable('tokens', 'tokens', { symbol: inst.voteToken }); + const voteTokenMinValue = api.BigNumber(1) + .dividedBy(api.BigNumber(10).pow(voteTokenObj.precision)); + const instTickTime = api.BigNumber(blockDate.getTime()) + .minus(params.instanceTickHours * 3600 * 1000).toNumber(); + + let rolesProcessed = 0; + const pendingRoles = await api.db.find('roles', + { + instanceId: inst.id, + active: true, + 'tokenBalances.0': { $exists: true }, + lastTickTime: { $lte: instTickTime }, + }, + params.maxRolesPerBlock, + 0, + [{ index: 'byLastTickTime', descending: false }, { index: '_id', descending: false }]); + + for (let i = 0; i < pendingRoles.length; i += 1) { + const role = pendingRoles[i]; + const funded = []; + const payTokens = role.tokenBalances.filter(t => api.BigNumber(t.quantity).gt(0)); + const totalSlots = api.BigNumber(role.mainSlots).plus(role.backupSlots).toNumber(); + const roleTickTime = api.BigNumber(blockDate.getTime()) + .minus(role.tickHours * 3600 * 1000).toNumber(); + + if (role.lastTickTime <= roleTickTime) { + if (payTokens.length > 0) { + let offset = 0; + let candidates = await api.db.find('candidates', + { + roleId: role._id, + active: true, + approvalWeight: { $gt: { $numberDecimal: api.BigNumber(role.voteThreshold) } }, + }, + params.processQueryLimit, + offset, + [{ index: 'byApprovalWeight', descending: true }, { index: '_id', descending: false }]); + + let accWeight = 0; + let backupWeight = null; + do { + for (let j = 0; j < candidates.length; j += 1) { + const candidate = candidates[j]; + if (funded.length >= role.mainSlots && backupWeight === null) { + backupWeight = api.BigNumber(accWeight) + .plus(voteTokenMinValue) + .plus(api.BigNumber(role.totalApprovalWeight.$numberDecimal) + .minus(accWeight) + .times(random)) + .toFixed(voteTokenObj.precision, api.BigNumber.ROUND_HALF_UP); + } + + accWeight = api.BigNumber(accWeight) + .plus(candidate.approvalWeight.$numberDecimal) + .toFixed(voteTokenObj.precision, api.BigNumber.ROUND_HALF_UP); + + if (candidate.active === true) { + if (funded.length < role.mainSlots || api.BigNumber(backupWeight).lte(accWeight)) { + funded.push({ + candidate: candidate._id, + account: candidate.account, + }); + } + } + if (funded.length >= totalSlots) break; + } + + if (funded.length < totalSlots) { + offset += params.processQueryLimit; + candidates = await api.db.find('candidates', + { + roleId: role._id, + active: true, + approvalWeight: { $gt: { $numberDecimal: api.BigNumber(role.voteThreshold) } }, + }, + params.processQueryLimit, + offset, + [{ index: 'byApprovalWeight', descending: true }, { index: '_id', descending: false }]); + } + } while (candidates.length > 0 && funded.length < totalSlots); + + for (let l = 0; l < payTokens.length; l += 1) { + const payToken = await api.db.findOneInTable('tokens', 'tokens', { symbol: payTokens[l].symbol }); + const payoutQty = api.BigNumber(payTokens[l].quantity) + .dividedBy(totalSlots) + .toFixed(payToken.precision, api.BigNumber.ROUND_DOWN); + if (api.BigNumber(payoutQty).gt(0)) { + for (let k = 0; k < funded.length; k += 1) { + const fund = funded[k]; + const payResult = await payRecipient(fund.account, payTokens[l].symbol, payoutQty); + if (payResult) { + const tbIndex = role.tokenBalances.findIndex(b => b.symbol === payTokens[l].symbol); + role.tokenBalances[tbIndex].quantity = api.BigNumber(role.tokenBalances[tbIndex].quantity) + .minus(payoutQty) + .toFixed(payToken.precision, api.BigNumber.ROUND_DOWN); + api.emit('rolePayment', { + instanceId: inst.id, + roleId: role._id, + account: fund.account, + symbol: payTokens[l].symbol, + quantity: payoutQty, + }); + } + } + } + } + } + rolesProcessed += 1; + const upRole = JSON.parse(JSON.stringify(role)); + upRole.lastTickTime = blockDate.getTime(); + await api.db.update('roles', upRole); + } + } + + if (rolesProcessed === 0) { + upInst.lastTickTime = blockDate.getTime(); + await api.db.update('instances', upInst); + } +} + +actions.checkPendingInstances = async () => { + if (api.assert(api.sender === 'null', 'not authorized')) { + const params = await api.db.findOne('params', {}); + const blockDate = new Date(`${api.hiveBlockTimestamp}.000Z`); + const tickTime = api.BigNumber(blockDate.getTime()) + .minus(params.instanceTickHours * 3600 * 1000).toNumber(); + + const pendingInst = await api.db.find('instances', + { + active: true, + lastTickTime: { + $lte: tickTime, + }, + }, + params.maxInstancesPerBlock, + 0, + [{ index: 'lastTickTime', descending: false }, { index: '_id', descending: false }]); + + for (let i = 0; i < pendingInst.length; i += 1) { + await checkPendingCandidates(pendingInst[i], params); + } + } +}; diff --git a/contracts/tokens.js b/contracts/tokens.js index d47a3481..601f339b 100644 --- a/contracts/tokens.js +++ b/contracts/tokens.js @@ -757,6 +757,7 @@ const processUnstake = async (unstake) => { await api.executeSmartContract('mining', 'handleStakeChange', { account, symbol, quantity: api.BigNumber(nextTokensToRelease).negated() }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account, token }); } await api.db.update('balances', balance); @@ -894,6 +895,7 @@ actions.stake = async (payload) => { await api.executeSmartContract('mining', 'handleStakeChange', { account: finalTo, symbol, quantity }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account: finalTo, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account: finalTo, token }); } } } @@ -940,6 +942,7 @@ actions.stakeFromContract = async (payload) => { await api.executeSmartContract('mining', 'handleStakeChange', { account: finalTo, symbol, quantity }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account: finalTo, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account: finalTo, token }); } } } @@ -1000,6 +1003,7 @@ const startUnstake = async (account, token, quantity) => { quantity: api.BigNumber(nextTokensToRelease).negated(), }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account, token }); } } else { return false; @@ -1099,6 +1103,7 @@ const processCancelUnstake = async (unstake) => { await api.executeSmartContract('mining', 'handleStakeChange', { account, symbol, quantity: tokensToRelease }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account, token }); return true; } @@ -1291,6 +1296,8 @@ actions.delegate = async (payload) => { { account: api.sender, symbol, quantity: api.BigNumber(quantity).negated() }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account: api.sender, token }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account: finalTo, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account: api.sender, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account: finalTo, token }); } else { // if a delegation already exists, increase it @@ -1340,6 +1347,8 @@ actions.delegate = async (payload) => { { account: api.sender, symbol, quantity: api.BigNumber(quantity).negated() }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account: api.sender, token }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account: finalTo, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account: api.sender, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account: finalTo, token }); } } } @@ -1443,6 +1452,7 @@ actions.undelegate = async (payload) => { delegated: true, }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account: finalFrom, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account: finalFrom, token }); } } } @@ -1492,6 +1502,7 @@ const processUndelegation = async (undelegation) => { await api.executeSmartContract('mining', 'handleStakeChange', { account, symbol, quantity }); await api.executeSmartContract('tokenfunds', 'updateProposalApprovals', { account, token }); + await api.executeSmartContract('roles', 'updateCandidateApprovals', { account, token }); } } }; diff --git a/test/roles.js b/test/roles.js new file mode 100644 index 00000000..1400c634 --- /dev/null +++ b/test/roles.js @@ -0,0 +1,942 @@ +/* eslint-disable */ +const assert = require('assert').strict; +const { MongoClient } = require('mongodb'); +const dhive = require('@hiveio/dhive'); +const enchex = require('crypto-js/enc-hex'); +const BigNumber = require('bignumber.js'); + +const { CONSTANTS } = require('../libs/Constants'); +const { Database } = require('../libs/Database'); +const blockchain = require('../plugins/Blockchain'); +const { Transaction } = require('../libs/Transaction'); +const { setupContractPayload } = require('../libs/util/contractUtil'); +const { Fixture, conf } = require('../libs/util/testing/Fixture'); +const { TableAsserts } = require('../libs/util/testing/TableAsserts'); +const { assertError } = require('../libs/util/testing/Asserts'); + +const tokensContractPayload = setupContractPayload('tokens', './contracts/tokens.js'); +const miningContractPayload = setupContractPayload('mining', './contracts/mining.js'); +const distributionContractPayload = setupContractPayload('distribution', './contracts/distribution.js'); +const inflationContractPayload = setupContractPayload('inflation', './contracts/inflation.js'); +const witnessContractPayload = setupContractPayload('witnesses', './contracts/witnesses.js'); +const dtfContractPayload = setupContractPayload('tokenfunds', './contracts/tokenfunds.js'); +const contractPayload = setupContractPayload('roles', './contracts/roles.js'); + +const fixture = new Fixture(); +const tableAsserts = new TableAsserts(fixture); + +async function assertUserWeight(account, symbol, weight = 0) { + const res = await fixture.database.findOne({ + contract: 'roles', + table: 'accounts', + query: { + account, + 'weights.symbol': symbol, + } + }); + assert.ok(res, `No weight for ${account}, ${symbol}`); + const wIndex = res.weights.findIndex(x => x.symbol === symbol); + assert.equal(res.weights[wIndex].weight, weight, `${account} has ${symbol} weight ${res.weights[wIndex].weight}, expected ${weight}`); +} + +async function assertUserApproval(account, candidateId, present = true) { + const res = await fixture.database.findOne({ + contract: 'roles', + table: 'approvals', + query: { + from: account, + to: candidateId + } + }); + + if (!present) { + assert(!res, `candidateId found for ${account}, expected none.`); + return; + } + assert.ok(res, `No candidateId for ${account}, ${candidateId}`); +} + +async function assertContractBalance(account, symbol, balance) { + const res = await fixture.database.findOne({ + contract: 'tokens', + table: 'contractsBalances', + query: { account, symbol } + }); + + if (!balance) { + assert(!res, `Balance found for ${account}, ${symbol}, expected none.`); + return; + } + assert.ok(res, `No balance for ${account}, ${symbol}`); + assert.equal(res.balance, balance, `${account} has ${symbol} balance ${res.balance}, expected ${balance}`); +} + +async function assertWeightConsistency(candidateId, voteSymbol) { + const prop = await fixture.database.findOne({ + contract: 'roles', + table: 'candidates', + query: { _id: candidateId } + }); + const app = await fixture.database.find({ + contract: 'roles', + table: 'approvals', + query: { to: candidateId } + }); + let appWeight = 0; + for (let i = 0; i < app.length; i += 1) { + const acct = await fixture.database.findOne({ + contract: 'roles', + table: 'accounts', + query: { account: app[i].from } + }); + const wIndex = acct.weights.findIndex(x => x.symbol === voteSymbol); + if (wIndex !== -1) { + appWeight = BigNumber(appWeight).plus(acct.weights[wIndex].weight).toNumber(); + } + } + assert.strictEqual(appWeight, BigNumber(prop.approvalWeight.$numberDecimal).toNumber(), `prop.approvalWeight (${prop.approvalWeight.$numberDecimal}) doesn\'t equal total of account weights (${appWeight})`); +} + +async function setUpEnv(configOverride = {}) { + let transactions = []; + let refBlockNumber = fixture.getNextRefBlockNumber(); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'contract', 'update', JSON.stringify(tokensContractPayload))); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'contract', 'deploy', JSON.stringify(witnessContractPayload))); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'contract', 'deploy', JSON.stringify(miningContractPayload))); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'contract', 'deploy', JSON.stringify(dtfContractPayload))); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'contract', 'deploy', JSON.stringify(contractPayload))); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2018-05-31T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + await tableAsserts.assertNoErrorInLastBlock(); + + transactions = []; + refBlockNumber = fixture.getNextRefBlockNumber(); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'contract', 'registerTick', '{ "contractName": "roles", "tickAction": "checkPendingInstances"}')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'tokens', 'transfer', `{ "symbol": "${CONSTANTS.UTILITY_TOKEN_SYMBOL}", "to": "donchate", "quantity": "5000", "isSignedWithActiveKey": true }`)); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'tokens', 'transfer', `{ "symbol": "${CONSTANTS.UTILITY_TOKEN_SYMBOL}", "to": "buffet", "quantity": "5000", "isSignedWithActiveKey": true }`)); + + block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2018-05-31T01:00:00', + transactions, + }; + + await fixture.sendBlock(block); + await tableAsserts.assertNoErrorInLastBlock(); +} + +// distribution test suite +describe('roles tests', function () { + this.timeout(30000); + + before((done) => { + new Promise(async (resolve) => { + client = await MongoClient.connect(conf.databaseURL, { useNewUrlParser: true, useUnifiedTopology: true }); + db = await client.db(conf.databaseName); + await db.dropDatabase(); + resolve(); + }) + .then(() => { + done() + }) + }); + + after((done) => { + new Promise(async (resolve) => { + await client.close(); + resolve(); + }) + .then(() => { + done() + }) + }); + + beforeEach((done) => { + new Promise(async (resolve) => { + db = await client.db(conf.databaseName); + resolve(); + }) + .then(() => { + done() + }) + }); + + afterEach((done) => { + // runs after each test in this block + new Promise(async (resolve) => { + await db.dropDatabase() + resolve(); + }) + .then(() => { + done() + }) + }); + + it('should not create invalid instance', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "PRO", "amount": "1" }, "isSignedWithActiveKey": false }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "ABC", "amount": "1" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "PRO", "amount": "1" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2018-06-01T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + let res = await fixture.database.getLatestBlockInfo(); + let txs = res.transactions; + + assertError(txs[1], 'you must use a transaction signed with your active key'); + assertError(txs[2], 'invalid candidateFee properties'); + assertError(txs[3], 'invalid candidateFee token or precision'); + assertError(txs[4], 'voteToken must have staking enabled'); + + res = await fixture.database.find({ + contract: 'roles', + table: 'instances', + query: { _id: 1 } + }); + + assert.ok(!res || res.length === 0, 'uncaught errors, invalid instance created'); + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + + }); + + it('should create valid instance', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "BEE", "amount": "0" }, "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2018-06-01T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + await tableAsserts.assertNoErrorInLastBlock(); + res = await fixture.database.findOne({ + contract: 'roles', + table: 'instances', + query: { + _id: 1 + } + }); + assert.ok(res, 'newly created instance not found'); + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + it('should not update invalid instance', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "BEE", "amount": "0" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateInstance', '{ "instanceId": "1", "candidateFee": { "method": "burn", "symbol": "PRO", "amount": "1" }, "isSignedWithActiveKey": false }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateInstance', '{ "instanceId": "1", "candidateFee": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateInstance', '{ "instanceId": "1", "candidateFee": { "method": "burn", "symbol": "ABC", "amount": "1" }, "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2018-06-01T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + let res = await fixture.database.getLatestBlockInfo(); + let txs = res.transactions; + + assertError(txs[3], 'you must use a transaction signed with your active key'); + assertError(txs[4], 'invalid candidateFee object'); + assertError(txs[5], 'invalid candidateFee token or precision'); + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + + }); + + it('should update valid instance', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "BEE", "amount": "0" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateInstance', '{ "instanceId": "1", "candidateFee": { "method": "issuer", "symbol": "PRO", "amount": "10" }, "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2018-06-01T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + await tableAsserts.assertNoErrorInLastBlock(); + let res = await fixture.database.findOne({ + contract: 'roles', + table: 'instances', + query: { + _id: 1 + } + }); + const expected = { + _id: 1, + voteToken: 'PRO', + candidateFee: { method: 'issuer', symbol: 'PRO', amount: '10' }, + active: false, + creator: 'donchate', + lastTickTime: 1527811200000 + } + assert.deepEqual(res, expected, 'updates not found in instance'); + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + it('should allow owner to update params', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'roles', 'updateParams', '{ "instanceCreationFee": "1", "instanceUpdateFee": "1", "instanceTickHours": "1", "roleCreationFee": "1", "roleUpdateFee": "1", "maxSlots": "1", "maxInstancesPerBlock": "1", "maxRolesPerBlock": "1", "maxAccountApprovals": "1", "processQueryLimit": "1", "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2018-06-01T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + await tableAsserts.assertNoErrorInLastBlock(); + let res = await fixture.database.findOne({ + contract: 'roles', + table: 'params', + query: {}, + }); + const expected = { + _id: 1, + instanceCreationFee: '1', + instanceUpdateFee: '1', + instanceTickHours: '1', + roleCreationFee: '1', + roleUpdateFee: '1', + maxSlots: 1, + maxInstancesPerBlock: 1, + maxRolesPerBlock: 1, + maxAccountApprovals: 1, + processQueryLimit: 1 + }; + assert.deepEqual(res, expected, 'updates not as expected'); + + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + it('should not create invalid roles', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "BEE", "amount": "0" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [], "isSignedWithActiveKey": false }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "2", "roles": [{ "name": "Worker 1", "voteThreshold": "0", "mainSlots": "5", "backupSlots": "2", "tickHours": "24"}], "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [], "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'buffet', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [{ "name": "Worker 1", "voteThreshold": "0", "mainSlots": "5", "backupSlots": "2", "tickHours": "24"}], "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [{ "name": "Worker X", "voteThreshold": "-1", "mainSlots": "0", "backupSlots": "5", "tickHours": "12"}], "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-12T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + let res = await fixture.database.getLatestBlockInfo(); + // console.log(res); + let txs = res.transactions; + + assertError(txs[3], 'you must use a transaction signed with your active key'); + assertError(txs[4], 'instance not found'); + assertError(txs[5], 'invalid roles object'); + assertError(txs[6], 'must be instance creator'); + assertError(txs[7], 'invalid roles properties'); + + res = await fixture.database.find({ + contract: 'roles', + table: 'roles', + query: {} + }); + + assert.ok(!res || res.length === 0, 'uncaught errors, invalid role created'); + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + + }); + + it('should create valid roles', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "BEE", "amount": "0" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [{ "name": "Worker 1", "voteThreshold": "0", "mainSlots": "5", "backupSlots": "2", "tickHours": "24"},{ "name": "Worker 2", "voteThreshold": "0", "mainSlots": "1", "backupSlots": "1", "tickHours": "168"}], "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-12T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + // let res = await fixture.database.getLatestBlockInfo(); + // console.log(res); + await tableAsserts.assertNoErrorInLastBlock(); + let resx = await fixture.database.find({ + contract: 'roles', + table: 'roles', + query: {} + }); + assert.ok(resx.length === 2, 'newly created roles not found'); + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + it('should not update invalid role', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "BEE", "amount": "0" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [{ "name": "Worker 1", "voteThreshold": "0", "mainSlots": "5", "backupSlots": "2", "tickHours": "24"},{ "name": "Worker 2", "voteThreshold": "0", "mainSlots": "1", "backupSlots": "1", "tickHours": "168"}], "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateRole', '{ "roleId": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'buffet', 'roles', 'updateRole', '{ "roleId": "1", "mainSlots": "5", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateRole', '{ "roleId": "1", "name": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateRole', '{ "roleId": "1", "voteThreshold": "-1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateRole', '{ "roleId": "1", "mainSlots": "0", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateRole', '{ "roleId": "1", "backupSlots": "36", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateRole', '{ "roleId": "1", "tickHours": "6", "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-12T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + let res = await fixture.database.getLatestBlockInfo(); + // console.log(res); + let txs = res.transactions; + + assertError(txs[4], 'specify at least one field to update'); + assertError(txs[5], 'must be instance creator'); + assertError(txs[6], 'name must be a string less than 50 characters'); + assertError(txs[7], 'voteThreshold must be greater than or equal to 0, precision matching voteToken'); + assertError(txs[8], 'mainSlots must be a integer between 1 - params.maxSlots'); + assertError(txs[9], 'backupSlots must be an integer between 0 - remainingSlots'); + assertError(txs[10], 'tickHours must be an integer greater than or equal to, and a multiple of params.instanceTickHours'); + + res = await fixture.database.findOne({ + contract: 'roles', + table: 'roles', + query: { _id: 1 } + }); + // console.log(res); + const original = { + _id: 1, + instanceId: 1, + name: 'Worker 1', + voteThreshold: '0', + mainSlots: '5', + backupSlots: '2', + tickHours: '24', + active: true, + lastTickTime: 0, + totalApprovalWeight: { $numberDecimal: '0' } + }; + assert.deepEqual(res, original, 'role has changed'); + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + it('should update valid role', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "BEE", "amount": "0" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [{ "name": "Worker 1", "voteThreshold": "0", "mainSlots": "5", "backupSlots": "2", "tickHours": "24"},{ "name": "Worker 2", "voteThreshold": "0", "mainSlots": "1", "backupSlots": "1", "tickHours": "168"}], "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateRole', '{ "roleId": "1", "name": "Worker 1A", "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-12T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + // let res = await fixture.database.getLatestBlockInfo(); + // console.log(res); + await tableAsserts.assertNoErrorInLastBlock(); + let resx = await fixture.database.findOne({ + contract: 'roles', + table: 'roles', + query: { + _id: 1, + } + }); + const updated = { + _id: 1, + instanceId: 1, + name: 'Worker 1A', + voteThreshold: '0', + mainSlots: '5', + backupSlots: '2', + tickHours: '24', + active: true, + lastTickTime: 0, + totalApprovalWeight: { $numberDecimal: '0' } + }; + + assert.deepEqual(resx, updated, 'updates not found in role'); + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + it('should not run inactive roles', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "1000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "candidateFee": { "method": "burn", "symbol": "BEE", "amount": "0" }, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'setInstanceActive', '{ "instanceId": "1", "active": true, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [{ "name": "Worker 1", "voteThreshold": "0", "mainSlots": "5", "backupSlots": "2", "tickHours": "24"},{ "name": "Worker 2", "voteThreshold": "0", "mainSlots": "1", "backupSlots": "1", "tickHours": "168"}], "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'updateRole', '{ "instanceId": "1", "roleId": 1, "active": false, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'deposit', '{ "roleId": "2", "symbol": "BEE", "quantity": "1", "isSignedWithActiveKey": true }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-12T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + refBlockNumber = fixture.getNextRefBlockNumber(); + transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'buffet', 'whatever', 'whatever', '')); + + block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-14T00:00:00', + transactions, + }; + await fixture.sendBlock(block); + + res = (await fixture.database.getLatestBlockInfo()); + // console.log(res); + assert.ok(res.virtualTransactions.length === 0, 'Expected to find no virtualTransactions'); + + // balance asserts + await assertContractBalance('roles', 'BEE', '1'); + + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + it('should run roles and update approvals', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "10000000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "1000", "to": "organizer", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "1000", "to": "voter1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "10000", "to": "voter2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "100000", "to": "voter3", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "1000001", "to": "voter4", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter1', 'tokens', 'stake', '{ "to":"voter1", "symbol": "PRO", "quantity": "1000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'tokens', 'stake', '{ "to":"voter2", "symbol": "PRO", "quantity": "10000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'tokens', 'stake', '{ "to":"voter3", "symbol": "PRO", "quantity": "100000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'tokens', 'stake', '{ "to":"voter4", "symbol": "PRO", "quantity": "1000000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'setInstanceActive', '{ "instanceId": "1", "active": true, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [{ "name": "Worker 1", "voteThreshold": "0", "mainSlots": "5", "backupSlots": "2", "tickHours": "24"},{ "name": "Worker 2", "voteThreshold": "0", "mainSlots": "1", "backupSlots": "1", "tickHours": "168"}], "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'setRoleActive', '{ "roleId": "1", "active": false, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'deposit', '{ "roleId": "2", "symbol": "BEE", "quantity": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'organizer', 'roles', 'applyForRole', '{ "roleId": "2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter1', 'roles', 'applyForRole', '{ "roleId": "2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'roles', 'applyForRole', '{ "roleId": "2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'roles', 'applyForRole', '{ "roleId": "2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'roles', 'applyForRole', '{ "roleId": "2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter1', 'roles', 'approveCandidate', '{ "id": "1" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'roles', 'approveCandidate', '{ "id": "1" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'roles', 'approveCandidate', '{ "id": "1" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'roles', 'approveCandidate', '{ "id": "1" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'roles', 'approveCandidate', '{ "id": "2" }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-12T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + let res = await fixture.database.getLatestBlockInfo(); + // console.log(res); + await tableAsserts.assertNoErrorInLastBlock(); + + // weight asserts + await assertUserWeight('voter1', 'PRO', '1000.00000000'); + await assertUserWeight('voter2', 'PRO', '10000.00000000'); + await assertUserWeight('voter3', 'PRO', '100000.00000000'); + await assertUserWeight('voter4', 'PRO', '1000000.00000000'); + await assertUserApproval('voter1', 1); + await assertUserApproval('voter2', 1); + await assertUserApproval('voter3', 1); + await assertUserApproval('voter4', 1); + await assertWeightConsistency(1, 'PRO'); + await assertWeightConsistency(2, 'PRO'); + await assertWeightConsistency(3, 'PRO'); + await assertWeightConsistency(4, 'PRO'); + + + refBlockNumber = fixture.getNextRefBlockNumber(); + transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'tokens', 'stake', '{ "to": "voter4", "symbol": "PRO", "quantity": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'tokens', 'unstake', '{ "symbol": "PRO", "quantity": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'roles', 'setApplyActive', '{ "roleId": "2", "active": false, "isSignedWithActiveKey": true }')); + + block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-13T00:00:00', + transactions, + }; + await fixture.sendBlock(block); + + res = (await fixture.database.getLatestBlockInfo()); + // console.log(res); + await tableAsserts.assertNoErrorInLastBlock(); + + await assertWeightConsistency(1, 'PRO'); + await assertWeightConsistency(2, 'PRO'); + await assertWeightConsistency(3, 'PRO'); + await assertWeightConsistency(4, 'PRO'); + + assert.ok(res.virtualTransactions.length > 0, 'Expected to find virtualTransactions'); + let virtualEventLog = JSON.parse(res.virtualTransactions[0].logs); + let e = virtualEventLog.events.find(x => x.event === 'rolePayment'); + assert.ok(e, 'Expected to find rolePayment event'); + + // balance asserts + await tableAsserts.assertUserBalances({ account: 'organizer', symbol: 'BEE', balance: '0.50000000'}); + await tableAsserts.assertUserBalances({ account: 'voter1', symbol: 'BEE', balance: '0.50000000'}); + await assertContractBalance('roles', 'BEE', '0.00000000'); + + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + it('should run roles over several blocks', (done) => { + new Promise(async (resolve) => { + + await fixture.setUp(); await setUpEnv(); + + let refBlockNumber = fixture.getNextRefBlockNumber(); + let transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), CONSTANTS.HIVE_ENGINE_ACCOUNT, 'roles', 'updateParams', '{ "maxInstancesPerBlock": "1", "maxRolesPerBlock": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'create', '{ "isSignedWithActiveKey": true, "name": "token", "symbol": "PRO", "precision": 8, "maxSupply": "10000000" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'enableStaking', '{ "symbol": "PRO", "unstakingCooldown": 3, "numberTransactions": 1, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "1000", "to": "organizer", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "1000", "to": "voter1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "10000", "to": "voter2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "100000", "to": "voter3", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'tokens', 'issue', '{ "symbol": "PRO", "quantity": "1000001", "to": "voter4", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter1', 'tokens', 'stake', '{ "to":"voter1", "symbol": "PRO", "quantity": "1000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'tokens', 'stake', '{ "to":"voter2", "symbol": "PRO", "quantity": "10000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'tokens', 'stake', '{ "to":"voter3", "symbol": "PRO", "quantity": "100000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'tokens', 'stake', '{ "to":"voter4", "symbol": "PRO", "quantity": "1000000", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createInstance', '{ "voteToken": "PRO", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'setInstanceActive', '{ "instanceId": "1", "active": true, "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'createRoles', '{ "instanceId": "1", "roles": [{ "name": "Worker 1", "voteThreshold": "0", "mainSlots": "3", "backupSlots": "1", "tickHours": "24"},{ "name": "Worker 2", "voteThreshold": "0", "mainSlots": "4", "backupSlots": "0", "tickHours": "24"}], "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'deposit', '{ "roleId": "1", "symbol": "BEE", "quantity": "100", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'donchate', 'roles', 'deposit', '{ "roleId": "2", "symbol": "BEE", "quantity": "100", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'organizer', 'roles', 'applyForRole', '{ "roleId": "2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter1', 'roles', 'applyForRole', '{ "roleId": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'roles', 'applyForRole', '{ "roleId": "1", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'roles', 'applyForRole', '{ "roleId": "2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'roles', 'applyForRole', '{ "roleId": "2", "isSignedWithActiveKey": true }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter1', 'roles', 'approveCandidate', '{ "id": "1" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'roles', 'approveCandidate', '{ "id": "1" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'roles', 'approveCandidate', '{ "id": "1" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'roles', 'approveCandidate', '{ "id": "2" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'roles', 'approveCandidate', '{ "id": "1" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'roles', 'approveCandidate', '{ "id": "2" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter1', 'roles', 'approveCandidate', '{ "id": "3" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'roles', 'approveCandidate', '{ "id": "3" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter3', 'roles', 'approveCandidate', '{ "id": "3" }')); + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter4', 'roles', 'approveCandidate', '{ "id": "3" }')); + + let block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-12T00:00:00', + transactions, + }; + + await fixture.sendBlock(block); + + let res = await fixture.database.getLatestBlockInfo(); + // console.log(res); + await tableAsserts.assertNoErrorInLastBlock(); + + // weight asserts + await assertUserWeight('voter1', 'PRO', '1000.00000000'); + await assertUserWeight('voter2', 'PRO', '10000.00000000'); + await assertUserWeight('voter3', 'PRO', '100000.00000000'); + await assertUserWeight('voter4', 'PRO', '1000000.00000000'); + await assertUserApproval('voter1', 1); + await assertUserApproval('voter2', 1); + await assertUserApproval('voter3', 1); + await assertUserApproval('voter4', 1); + await assertWeightConsistency(1, 'PRO'); + await assertWeightConsistency(2, 'PRO'); + await assertWeightConsistency(3, 'PRO'); + await assertWeightConsistency(4, 'PRO'); + + + refBlockNumber = fixture.getNextRefBlockNumber(); + transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'roles', 'approveCandidate', '{ "id": "2" }')); + + block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-13T00:00:00', + transactions, + }; + await fixture.sendBlock(block); + + res = (await fixture.database.getLatestBlockInfo()); + // console.log(res); + await tableAsserts.assertNoErrorInLastBlock(); + + await assertWeightConsistency(1, 'PRO'); + await assertWeightConsistency(2, 'PRO'); + await assertWeightConsistency(3, 'PRO'); + await assertWeightConsistency(4, 'PRO'); + + assert.ok(res.virtualTransactions.length > 0, 'Expected to find virtualTransactions'); + let virtualEventLog = JSON.parse(res.virtualTransactions[0].logs); + let e = virtualEventLog.events.find(x => x.event === 'rolePayment'); + assert.ok(e, 'Expected to find rolePayment event'); + + refBlockNumber = fixture.getNextRefBlockNumber(); + transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'roles', 'disapproveCandidate', '{ "id": "2" }')); + + block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-13T00:00:03', + transactions, + }; + await fixture.sendBlock(block); + + res = (await fixture.database.getLatestBlockInfo()); + // console.log(res); + await tableAsserts.assertNoErrorInLastBlock(); + + await assertWeightConsistency(1, 'PRO'); + await assertWeightConsistency(2, 'PRO'); + await assertWeightConsistency(3, 'PRO'); + await assertWeightConsistency(4, 'PRO'); + + assert.ok(res.virtualTransactions.length > 0, 'Expected to find virtualTransactions'); + virtualEventLog = JSON.parse(res.virtualTransactions[0].logs); + e = virtualEventLog.events.find(x => x.event === 'rolePayment'); + assert.ok(e, 'Expected to find rolePayment event'); + + // balance asserts + await tableAsserts.assertUserBalances({ account: 'organizer', symbol: 'BEE', balance: '25.00000000'}); + await tableAsserts.assertUserBalances({ account: 'voter1', symbol: 'BEE', balance: '25.00000000'}); + await tableAsserts.assertUserBalances({ account: 'voter2', symbol: 'BEE', balance: '25.00000000'}); + await tableAsserts.assertUserBalances({ account: 'voter3', symbol: 'BEE'}); + await tableAsserts.assertUserBalances({ account: 'voter4', symbol: 'BEE'}); + await assertContractBalance('roles', 'BEE', '125.00000000'); + + // let roleState = await fixture.database.find({ + // contract: 'roles', + // table: 'roles', + // query: {} + // }); + // console.log(JSON.stringify(roleState, null, 2)); + + refBlockNumber = fixture.getNextRefBlockNumber(); + transactions = []; + transactions.push(new Transaction(refBlockNumber, fixture.getNextTxId(), 'voter2', 'roles', 'approveCandidate', '{ "id": "4" }')); + + block = { + refHiveBlockNumber: refBlockNumber, + refHiveBlockId: 'ABCD1', + prevRefHiveBlockId: 'ABCD2', + timestamp: '2021-03-13T00:00:06', + transactions, + }; + await fixture.sendBlock(block); + + res = (await fixture.database.getLatestBlockInfo()); + // console.log(res); + await tableAsserts.assertNoErrorInLastBlock(); + assert.ok(res.virtualTransactions.length == 0, 'Expected to not find virtualTransactions'); + + resolve(); + }) + .then(() => { + fixture.tearDown(); + done(); + }); + }); + + // END TESTS +});