leasehold-chain
Version:
Leasehold sidechain
643 lines (573 loc) • 16.4 kB
JavaScript
/*
* Copyright © 2019 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
'use strict';
const path = require('path');
const assert = require('assert');
const _ = require('lodash');
const {
entities: { Account: AccountEntity },
} = require('leasehold-lisk-framework/src/components/storage');
const defaultCreateValues = {
publicKey: null,
secondPublicKey: null,
secondSignature: false,
username: null,
isDelegate: false,
balance: '0',
missedBlocks: 0,
producedBlocks: 0,
rank: null,
fees: '0',
rewards: '0',
vote: '0',
nameExist: false,
multiMin: 0,
multiLifetime: 0,
asset: {},
};
const readOnlyFields = ['address'];
const dependentFieldsTableMap = {
membersPublicKeys: 'mem_accounts2multisignatures',
votedDelegatesPublicKeys: 'mem_accounts2delegates',
};
const sqlFiles = {
create: 'accounts/create.sql',
update: 'accounts/update.sql',
updateOne: 'accounts/update_one.sql',
delete: 'accounts/delete.sql',
resetMemTables: 'accounts/reset_mem_tables.sql',
increaseFieldBy: 'accounts/increase_field_by.sql',
decreaseFieldBy: 'accounts/decrease_field_by.sql',
createDependentRecord: 'accounts/create_dependent_record.sql',
createDependentRecords: 'accounts/create_dependent_records.sql',
deleteDependentRecord: 'accounts/delete_dependent_record.sql',
deleteDependentRecords: 'accounts/delete_dependent_records.sql',
deleteAllDependentRecords: 'accounts/delete_all_dependent_records.sql',
delegateBlocksRewards: 'accounts/delegate_blocks_rewards.sql',
syncDelegatesRank: 'accounts/sync_delegates_rank.sql',
insertFork: 'accounts/insert_fork.sql',
};
class ChainAccount extends AccountEntity {
/**
* Constructor
* @param {BaseAdapter} adapter - Adapter to retrive the data from
* @param {filters.Account} defaultFilters - Set of default filters applied on every query
*/
constructor(adapter, defaultFilters = {}) {
super(adapter, defaultFilters);
this.sqlDirectory = path.join(path.dirname(__filename), '../sql');
this.SQLs = this.loadSQLFiles('account', sqlFiles, this.sqlDirectory);
}
/*
* Create account object
*
* @param {Object|Array.<Object>} data
* @param {Object} [_options]
* @param {Object} [tx] - Transaction object
* @return {*}
*/
create(data, _options, tx) {
assert(data, 'Must provide data to create account');
assert(
typeof data === 'object' || Array.isArray(data),
'Data must be an object or array of objects',
);
const accounts = ChainAccount._sanitizeCreateData(data);
// We assume that all accounts have same attributes
// and pick defined fields as template
const attributes = Object.keys(this.fields);
const createSet = this.getValuesSet(accounts, attributes);
const fields = attributes
.map(k => `"${this.fields[k].fieldName}"`)
.join(',');
const accountCreatePromise = this.adapter.executeFile(
this.SQLs.create,
{ createSet, fields },
{ expectedResultCount: 0 },
tx,
);
const dependentRecordsPromsies = [];
if (data.membersPublicKeys && data.membersPublicKeys.length > 0) {
dependentRecordsPromsies.push(
this.updateDependentRecords(
'membersPublicKeys',
data.address,
data.membersPublicKeys,
tx,
),
);
}
if (
data.votedDelegatesPublicKeys &&
data.votedDelegatesPublicKeys.length > 0
) {
dependentRecordsPromsies.push(
this.updateDependentRecords(
'votedDelegatesPublicKeys',
data.address,
data.votedDelegatesPublicKeys,
tx,
),
);
}
return accountCreatePromise.then(() =>
Promise.all(dependentRecordsPromsies),
);
}
/**
* Parse account data and parse in default values
* @param {Array[Object}] | Object} data raw database account data
* @return {Array[Object]} Parsed accounts
*/
static _sanitizeCreateData(data) {
let accounts;
if (Array.isArray(data)) {
accounts = data.map(item => ({
...item,
}));
} else if (typeof data === 'object') {
accounts = [
{
...data,
},
];
}
accounts = accounts.map(account => {
const parsedAccount = _.defaults(account, defaultCreateValues);
return parsedAccount;
});
return accounts;
}
/**
* Update the records based on given condition
*
* @param {filters.Account} [filters]
* @param {Object} data
* @param {Object} [options]
* @param {Object} tx - Transaction object
* @return {*}
*/
async update(filters, data, _options, tx) {
const atLeastOneRequired = true;
this.validateFilters(filters, atLeastOneRequired);
const objectData = _.omit(data, readOnlyFields);
const mergedFilters = this.mergeFilters(filters);
const parsedFilters = this.parseFilters(mergedFilters);
const updateSet = this.getUpdateSet(objectData);
const params = {
...objectData,
parsedFilters,
updateSet,
};
if (_.isEmpty(objectData)) {
return false;
}
if (data.membersPublicKeys && data.membersPublicKeys.length > 0) {
await this.updateDependentRecords(
'membersPublicKeys',
filters.address,
data.membersPublicKeys,
tx,
);
}
if (
data.votedDelegatesPublicKeys &&
data.votedDelegatesPublicKeys.length > 0
) {
await this.updateDependentRecords(
'votedDelegatesPublicKeys',
filters.address,
data.votedDelegatesPublicKeys,
tx,
);
}
// Account remove all votes
if (
data.votedDelegatesPublicKeys &&
data.votedDelegatesPublicKeys.length === 0
) {
await this.adapter.executeFile(
this.SQLs.deleteAllDependentRecords,
{
accountId: filters.address,
tableName: dependentFieldsTableMap.votedDelegatesPublicKeys,
},
{},
tx,
);
}
// Account remove all multisignatures
if (data.membersPublicKeys && data.membersPublicKeys.length === 0) {
await this.adapter.executeFile(
this.SQLs.deleteAllDependentRecords,
{
accountId: filters.address,
tableName: dependentFieldsTableMap.membersPublicKeys,
},
{},
tx,
);
}
return this.adapter.executeFile(this.SQLs.update, params, {}, tx);
}
/**
* Update one record based on the condition given
*
* @param {filters.Account} filters
* @param {Object} data
* @param {Object} [options]
* @param {Object} tx - Transaction object
* @return {*}
*/
updateOne(filters, data, _options, tx) {
const atLeastOneRequired = true;
this.validateFilters(filters, atLeastOneRequired);
const objectData = _.omit(data, readOnlyFields);
const mergedFilters = this.mergeFilters(filters);
const parsedFilters = this.parseFilters(mergedFilters);
const updateSet = this.getUpdateSet(objectData);
const params = {
...objectData,
parsedFilters,
updateSet,
};
return this.adapter.executeFile(this.SQLs.updateOne, params, {}, tx);
}
/**
* Delete records with following conditions
*
* @param {filters.Account} filters
* @param {Object} [_options]
* @param {Object} [tx]
* @returns {Promise.<boolean, Error>}
*/
delete(filters, _options, tx = null) {
this.validateFilters(filters);
const mergedFilters = this.mergeFilters(filters);
const parsedFilters = this.parseFilters(mergedFilters);
return this.adapter
.executeFile(
this.SQLs.delete,
{
parsedFilters,
},
{
expectedResultCount: 0,
},
tx,
)
.then(result => !result);
}
/**
* Update data based on filters or insert data if no matching record found
*
* @param {filters.Account} filters - Filters to match the object
* @param {Object} data - Object data to be inserted
* @param {Object} [updateData] - If provided will be used for update, otherwise default data will be updated
* @param {Object} [tx] - DB transaction object
* @returns {Promise.<boolean, Error>}
*/
upsert(filters, data, updateData = {}, tx = null) {
const task = t =>
this.isPersisted(filters, {}, t).then(dataFound => {
if (dataFound) {
const dataToUpdate = _.isEmpty(updateData) ? data : updateData;
return this.update(filters, dataToUpdate, {}, t);
}
return this.create(data, {}, t);
});
if (tx) {
return task(tx);
}
return this.begin('storage:account:upsert', task);
}
/**
* Get blocks rewards of delegate for time period.
* TODO: move this method to Delegate entity once implemented
*
* @param {Object} filters = {} - Filters to filter data
* @param {string} filters.generatorPublicKey - Delegate Public Key to calculate reward
* @param {Number} [filters.fromTimestamp] - WHERE timestamp >= fromTimestamp
* @param {Number} [filters.toTimestamp] - WHERE timestamp <= toTimestamp
* @param {Object} tx - Database transaction object
* @return {Promise.<DatabaseRow, Error>}
*/
delegateBlocksRewards(filters, tx) {
assert(
filters && filters.generatorPublicKey,
'filters must be an object and contain generatorPublicKey',
);
const parseFilters = {
generatorPublicKey: filters.generatorPublicKey,
fromTimestamp: filters.fromTimestamp,
toTimestamp: filters.toTimestamp,
};
return this.adapter.executeFile(
this.SQLs.delegateBlocksRewards,
parseFilters,
{},
tx,
);
}
/**
* Clear data in memory tables:
* - mem_accounts
* - rounds_rewards
* - mem_round
* - mem_accounts2delegates
* - mem_accounts2multisignatures
*
* @param {Object} tx - DB transaction object
* @returns {Promise}
*/
resetMemTables(tx) {
return this.adapter.executeFile(this.SQLs.resetMemTables, {}, {}, tx);
}
/**
* Increase a field value in mem_accounts.
*
* @param {filters.Account} [filters] - Filters to match the objects
* @param {string} field - Name of the field to increase
* @param {Number|string} value - Value increase
* @param {Object} [tx] - Transaction object
* @returns {Promise}
*/
increaseFieldBy(filters, field, value, tx) {
return this._updateField(filters, field, value, 'increase', tx);
}
/**
* Decrease a field value in mem_accounts.
*
* @param {filters.Account} [filters] - Filters to match the objects
* @param {string} field - Name of the field to decrease
* @param {Number|string} value - Value decrease
* @param {Object} [tx] - Transaction object
* @returns {Promise}
*/
decreaseFieldBy(filters, field, value, tx) {
return this._updateField(filters, field, value, 'decrease', tx);
}
/**
* Create dependent record for the account
*
* @param {string} dependencyName - Name of the dependent table
* @param {string} address - Address of the account
* @param {string} dependentPublicKey - Dependent public id
* @param {Object} [tx] - Transaction object
* @return {*}
*/
createDependentRecord(dependencyName, address, dependentPublicKey, tx) {
return this._updateDependentRecord(
dependencyName,
address,
dependentPublicKey,
'insert',
tx,
);
}
/**
* Delete dependent record for the account
*
* @param {string} dependencyName - Name of the dependent table
* @param {string} address - Address of the account
* @param {string} dependentPublicKey - Dependent public id
* @param {Object} [tx] - Transaction object
* @return {*}
*/
deleteDependentRecord(dependencyName, address, dependentPublicKey, tx) {
return this._updateDependentRecord(
dependencyName,
address,
dependentPublicKey,
'delete',
tx,
);
}
/**
* Sync rank for all delegates.
*
* @param {Object} [tx] - Database transaction object
* @returns {Promise}
*/
syncDelegatesRanks(tx) {
return this.adapter.executeFile(this.SQLs.syncDelegatesRank, {}, {}, tx);
}
// TODO: Should create a separate entity to manage forks
/**
* Inserts a fork data table entry.
*
* @param {Object} fork
* @param {string} fork.delegatePublicKey
* @param {integer} fork.blockTimestamp
* @param {string} fork.blockId
* @param {integer} fork.blockHeight
* @param {string} fork.previousBlockId
* @param {string} fork.cause
* @param {Object} [tx] - Database transaction
* @returns {Promise}
*/
insertFork(fork, tx) {
return this.adapter.executeFile(
this.SQLs.insertFork,
fork,
{
expectedResultCount: 0,
},
tx,
);
}
/**
* Update the dependent records used to manage votes and multisignature account members
*
* @param {string} dependencyName
* @param {string} address
* @param {string} dependentPublicKey
* @param {('insert'|'delete')} mode
* @param {Object} [tx]
* @returns {Promise}
*/
_updateDependentRecord(
dependencyName,
address,
dependentPublicKey,
mode,
tx,
) {
assert(
Object.keys(dependentFieldsTableMap).includes(dependencyName),
`Invalid dependency name "${dependencyName}" provided.`,
);
const params = {
tableName: dependentFieldsTableMap[dependencyName],
accountId: address,
dependentId: dependentPublicKey,
};
const sql = {
insert: this.SQLs.createDependentRecord,
delete: this.SQLs.deleteDependentRecord,
}[mode];
return this.adapter.executeFile(
sql,
params,
{
expectedResultCount: 0,
},
tx,
);
}
/**
* Update the field value
*
* @param {filters.Account} filters - Filters object
* @param {string} field - Filed name to update
* @param {*} value - Value to be update
* @param {('increase'|'decrease')} mode - Mode of update
* @param {Object} [tx] - Database transaction object
* @returns {Promise}
*/
_updateField(filters, field, value, mode, tx) {
const atLeastOneRequired = true;
const validFieldName = Object.keys(this.fields).includes(field);
assert(validFieldName, `Field name "${field}" is not valid.`);
this.validateFilters(filters, atLeastOneRequired);
const mergedFilters = this.mergeFilters(filters);
const parsedFilters = this.parseFilters(mergedFilters);
const filedName = this.fields[field].fieldName;
const params = {
parsedFilters,
field: filedName,
value,
};
const sql = {
increase: this.SQLs.increaseFieldBy,
decrease: this.SQLs.decreaseFieldBy,
}[mode];
return this.adapter.executeFile(
sql,
params,
{
expectedResultCount: 0,
},
tx,
);
}
/**
* Update dependent records for the account
* Delete dependent record for the account
*
* @param {string} dependencyName - Name of the dependent table
* @param {string} address - Address of the account
* @param {string} dependentPublicKey - Dependent public id
* @param {Object} [tx] - Transaction object
* @return {*}
*/
async updateDependentRecords(
dependencyName,
address,
dependentPublicKeys,
tx,
) {
assert(
Object.keys(dependentFieldsTableMap).includes(dependencyName),
`Invalid dependency name "${dependencyName}" provided.`,
);
const sqlForDelete = this.SQLs.deleteDependentRecords;
const sqlForInsert = this.SQLs.createDependentRecords;
const tableName = dependentFieldsTableMap[dependencyName];
const dependentRecordsForAddress = await this.adapter.execute(
`SELECT "dependentId" FROM ${tableName} WHERE "accountId" = $1`,
[address],
);
const oldDependentPublicKeys = dependentRecordsForAddress.map(
dependentRecord => dependentRecord.dependentId,
);
const publicKeysToBeRemoved = oldDependentPublicKeys.filter(
aPK => !dependentPublicKeys.includes(aPK),
);
const publicKeysToBeInserted = dependentPublicKeys.filter(
aPK => !oldDependentPublicKeys.includes(aPK),
);
const paramsForDelete = {
tableName,
accountId: address,
dependentIds: publicKeysToBeRemoved,
};
if (publicKeysToBeRemoved.length > 0) {
await this.adapter.executeFile(
sqlForDelete,
paramsForDelete,
{ expectedResultCount: 0 },
tx,
);
}
if (publicKeysToBeInserted.length > 0) {
const valuesForInsert = publicKeysToBeInserted.map(dependentId => ({
accountId: address,
dependentId,
}));
const createSet = this.getValuesSet(
valuesForInsert,
['accountId', 'dependentId'],
{ useRawObject: true },
);
await this.adapter.executeFile(
sqlForInsert,
{ tableName, createSet },
{ expectedResultCount: 0 },
tx,
);
}
}
}
module.exports = ChainAccount;