parent
43d54bbff0
commit
db1e554d37
@ -1,3 +1,4 @@
|
||||
services: mongodb
|
||||
language: node_js
|
||||
node_js:
|
||||
- "10.5"
|
||||
|
@ -0,0 +1,625 @@
|
||||
const fs = require('fs-extra');
|
||||
const Loki = require('lokijs');
|
||||
const SHA256 = require('crypto-js/sha256');
|
||||
const enchex = require('crypto-js/enc-hex');
|
||||
const validator = require('validator');
|
||||
const lfsa = require('../libs/loki-fs-structured-adapter');
|
||||
const { IPC } = require('../libs/IPC');
|
||||
|
||||
const BC_PLUGIN_NAME = require('./Blockchain.constants').PLUGIN_NAME;
|
||||
const BC_PLUGIN_ACTIONS = require('./Blockchain.constants').PLUGIN_ACTIONS;
|
||||
|
||||
const { PLUGIN_NAME, PLUGIN_ACTIONS } = require('./Database.constants');
|
||||
|
||||
const PLUGIN_PATH = require.resolve(__filename);
|
||||
|
||||
const actions = {};
|
||||
|
||||
const ipc = new IPC(PLUGIN_NAME);
|
||||
|
||||
let database = null;
|
||||
let chain = null;
|
||||
let saving = false;
|
||||
let databaseHash = '';
|
||||
|
||||
// load the database from the filesystem
|
||||
async function init(conf, callback) {
|
||||
const {
|
||||
autosaveInterval,
|
||||
databaseFileName,
|
||||
dataDirectory,
|
||||
} = conf;
|
||||
|
||||
const databaseFilePath = dataDirectory + databaseFileName;
|
||||
|
||||
// init the database
|
||||
database = new Loki(databaseFilePath, {
|
||||
adapter: new lfsa(), // eslint-disable-line new-cap
|
||||
autosave: autosaveInterval > 0,
|
||||
autosaveInterval,
|
||||
});
|
||||
|
||||
// check if the app has already be run
|
||||
if (fs.pathExistsSync(databaseFilePath)) {
|
||||
// load the database from the filesystem to the RAM
|
||||
database.loadDatabase({}, (errorDb) => {
|
||||
if (errorDb) {
|
||||
callback(errorDb);
|
||||
}
|
||||
|
||||
// if the chain or the contracts collection doesn't exist we return an error
|
||||
chain = database.getCollection('chain');
|
||||
const contracts = database.getCollection('contracts');
|
||||
if (chain === null || contracts === null) {
|
||||
callback('The database is missing either the chain or the contracts table');
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
} else {
|
||||
// create the data directory if necessary and empty it if files exists
|
||||
fs.emptyDirSync(dataDirectory);
|
||||
|
||||
// init the main tables
|
||||
chain = database.addCollection('chain', { indices: ['blockNumber'], disableMeta: true });
|
||||
database.addCollection('transactions', { unique: ['txid'], disableMeta: true });
|
||||
database.addCollection('contracts', { indices: ['name'], disableMeta: true });
|
||||
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateGenesisBlock(conf, callback) {
|
||||
const {
|
||||
chainId,
|
||||
genesisSteemBlock,
|
||||
} = conf;
|
||||
|
||||
// check if genesis block hasn't been generated already
|
||||
const genBlock = actions.getBlockInfo(0);
|
||||
|
||||
if (!genBlock) {
|
||||
// insert the genesis block
|
||||
const res = await ipc.send(
|
||||
{
|
||||
to: BC_PLUGIN_NAME,
|
||||
action: BC_PLUGIN_ACTIONS.CREATE_GENESIS_BLOCK,
|
||||
payload: {
|
||||
chainId,
|
||||
genesisSteemBlock,
|
||||
},
|
||||
},
|
||||
);
|
||||
chain.insert(res.payload);
|
||||
|
||||
// initialize the block production tools
|
||||
// BlockProduction.initialize(database, genesisSteemBlock);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
// save the blockchain as well as the database on the filesystem
|
||||
actions.save = (callback) => {
|
||||
saving = true;
|
||||
|
||||
// save the database from the RAM to the filesystem
|
||||
database.saveDatabase((err) => {
|
||||
saving = false;
|
||||
if (err) {
|
||||
callback(err);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
// save the blockchain as well as the database on the filesystem
|
||||
function stop(callback) {
|
||||
actions.save(callback);
|
||||
}
|
||||
|
||||
function addTransactions(block) {
|
||||
const transactionsTable = database.getCollection('transactions');
|
||||
const { transactions } = block;
|
||||
const nbTransactions = transactions.length;
|
||||
|
||||
for (let index = 0; index < nbTransactions; index += 1) {
|
||||
const transaction = transactions[index];
|
||||
const transactionToSave = {
|
||||
txid: transaction.transactionId,
|
||||
blockNumber: block.blockNumber,
|
||||
index,
|
||||
};
|
||||
|
||||
transactionsTable.insert(transactionToSave);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTableHash(contract, table, record) {
|
||||
const contracts = database.getCollection('contracts');
|
||||
const contractInDb = contracts.findOne({ name: contract });
|
||||
|
||||
if (contractInDb && contractInDb.tables[table] !== undefined) {
|
||||
const recordHash = SHA256(JSON.stringify(record)).toString(enchex);
|
||||
const tableHash = contractInDb.tables[table].hash;
|
||||
|
||||
contractInDb.tables[table].hash = SHA256(tableHash + recordHash).toString(enchex);
|
||||
|
||||
contracts.update(contractInDb);
|
||||
|
||||
databaseHash = SHA256(databaseHash + contractInDb.tables[table].hash).toString(enchex);
|
||||
}
|
||||
}
|
||||
|
||||
actions.initDatabaseHash = (previousDatabaseHash) => {
|
||||
databaseHash = previousDatabaseHash;
|
||||
};
|
||||
|
||||
actions.getDatabaseHash = () => databaseHash;
|
||||
|
||||
actions.getTransactionInfo = (txid) => { // eslint-disable-line no-unused-vars
|
||||
const transactionsTable = database.getCollection('transactions');
|
||||
|
||||
const transaction = transactionsTable.findOne({ txid });
|
||||
|
||||
if (transaction) {
|
||||
const { index, blockNumber } = transaction;
|
||||
const block = actions.getBlockInfo(blockNumber);
|
||||
|
||||
if (block) {
|
||||
return Object.assign({}, { blockNumber }, block.transactions[index]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
actions.addBlock = (block) => { // eslint-disable-line no-unused-vars
|
||||
chain.insert(block);
|
||||
addTransactions(block);
|
||||
};
|
||||
|
||||
actions.getLatestBlockInfo = () => { // eslint-disable-line no-unused-vars
|
||||
const { maxId } = chain;
|
||||
return chain.get(maxId);
|
||||
};
|
||||
|
||||
actions.getBlockInfo = blockNumber => chain.findOne({ blockNumber });
|
||||
|
||||
/**
|
||||
* Get the information of a contract (owner, source code, etc...)
|
||||
* @param {String} contract name of the contract
|
||||
* @returns {Object} returns the contract info if it exists, null otherwise
|
||||
*/
|
||||
actions.findContract = (payload) => {
|
||||
const { name } = payload;
|
||||
if (name && typeof name === 'string') {
|
||||
const contracts = database.getCollection('contracts');
|
||||
const contractInDb = contracts.findOne({ name });
|
||||
|
||||
if (contractInDb) {
|
||||
return contractInDb;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* add a smart contract to the database
|
||||
* @param {String} name name of the contract
|
||||
* @param {String} owner owner of the contract
|
||||
* @param {String} code code of the contract
|
||||
* @param {String} tables tables linked to the contract
|
||||
*/
|
||||
actions.addContract = (payload) => { // eslint-disable-line no-unused-vars
|
||||
const {
|
||||
name,
|
||||
owner,
|
||||
code,
|
||||
tables,
|
||||
} = payload;
|
||||
|
||||
if (name && typeof name === 'string'
|
||||
&& owner && typeof owner === 'string'
|
||||
&& code && typeof code === 'string'
|
||||
&& tables && typeof tables === 'object') {
|
||||
const contracts = database.getCollection('contracts');
|
||||
contracts.insert(payload);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* update a smart contract in the database
|
||||
* @param {String} name name of the contract
|
||||
* @param {String} owner owner of the contract
|
||||
* @param {String} code code of the contract
|
||||
* @param {String} tables tables linked to the contract
|
||||
*/
|
||||
actions.updateContract = (payload) => { // eslint-disable-line no-unused-vars
|
||||
const {
|
||||
name,
|
||||
owner,
|
||||
code,
|
||||
tables,
|
||||
} = payload;
|
||||
|
||||
if (name && typeof name === 'string'
|
||||
&& owner && typeof owner === 'string'
|
||||
&& code && typeof code === 'string'
|
||||
&& tables && typeof tables === 'object') {
|
||||
const contracts = database.getCollection('contracts');
|
||||
|
||||
if (contracts.findOne({ name, owner }) !== null) {
|
||||
contracts.update(payload);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a table to the database
|
||||
* @param {String} contractName name of the contract
|
||||
* @param {String} tableName name of the table
|
||||
* @param {Array} indexes array of string containing the name of the indexes to create
|
||||
*/
|
||||
actions.createTable = (payload) => { // eslint-disable-line no-unused-vars
|
||||
const { contractName, tableName, indexes } = payload;
|
||||
|
||||
// check that the params are correct
|
||||
// each element of the indexes array have to be a string if defined
|
||||
if (validator.isAlphanumeric(tableName)
|
||||
&& Array.isArray(indexes)
|
||||
&& (indexes.length === 0
|
||||
|| (indexes.length > 0 && indexes.every(el => typeof el === 'string' && validator.isAlphanumeric(el))))) {
|
||||
const finalTableName = `${contractName}_${tableName}`;
|
||||
// get the table from the database
|
||||
const table = database.getCollection(finalTableName);
|
||||
if (table === null) {
|
||||
// if it doesn't exist, create it (with the binary indexes)
|
||||
database.addCollection(finalTableName, { indices: indexes, disableMeta: true });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* retrieve records from the table of a contract
|
||||
* @param {String} contract contract name
|
||||
* @param {String} table table name
|
||||
* @param {JSON} query query to perform on the table
|
||||
* @param {Integer} limit limit the number of records to retrieve
|
||||
* @param {Integer} offset offset applied to the records set
|
||||
* @param {Array<Object>} indexes array of index definitions { index: string, descending: boolean }
|
||||
* @returns {Array<Object>} returns an array of objects if records found, an empty array otherwise
|
||||
*/
|
||||
actions.find = (payload) => { // eslint-disable-line no-unused-vars
|
||||
try {
|
||||
const {
|
||||
contract,
|
||||
table,
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
indexes,
|
||||
} = payload;
|
||||
|
||||
const lim = limit || 1000;
|
||||
const off = offset || 0;
|
||||
const ind = indexes || [];
|
||||
|
||||
if (contract && typeof contract === 'string'
|
||||
&& table && typeof table === 'string'
|
||||
&& query && typeof query === 'object'
|
||||
&& JSON.stringify(query).indexOf('$regex') === -1
|
||||
&& Array.isArray(ind)
|
||||
&& (ind.length === 0
|
||||
|| (ind.length > 0
|
||||
&& ind.every(el => el.index && typeof el.index === 'string'
|
||||
&& el.descending !== undefined && typeof el.descending === 'boolean')))
|
||||
&& Number.isInteger(lim)
|
||||
&& Number.isInteger(off)
|
||||
&& lim > 0 && lim <= 1000
|
||||
&& off >= 0) {
|
||||
const finalTableName = `${contract}_${table}`;
|
||||
const tableData = database.getCollection(finalTableName);
|
||||
|
||||
if (tableData) {
|
||||
// if there is an index passed, check if it exists
|
||||
if (ind.length > 0 && ind.every(el => tableData.binaryIndices[el.index] !== undefined || el.index === '$loki')) {
|
||||
return tableData.chain()
|
||||
.find(query)
|
||||
.compoundsort(ind.map(el => [el.index, el.descending]))
|
||||
.offset(off)
|
||||
.limit(lim)
|
||||
.data();
|
||||
}
|
||||
|
||||
return tableData.chain()
|
||||
.find(query)
|
||||
.offset(off)
|
||||
.limit(lim)
|
||||
.data();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* retrieve a record from the table of a contract |