architecture updates

This commit is contained in:
Igor Lins e Silva
2020-03-13 03:00:18 -03:00
parent 0e9701495f
commit 2bfd7501eb
115 changed files with 2625 additions and 770 deletions
@@ -90,7 +90,7 @@ var __values = (this && this.__values) || function (o) {
};
Object.defineProperty(exports, "__esModule", { value: true });
var ser = require("./eosjs-serialize");
var abiAbi = require('./src/abi.abi');
var abiAbi = require('./src/abi.abi.json');
var transactionAbi = require('./src/transaction.abi.json');
var Api = /** @class */ (function () {
/**
+1 -1
View File
@@ -44,7 +44,7 @@ fastify.register(AutoLoad, {dir: path.join(__dirname, 'handlers', 'v1-history'),
fastify.register(AutoLoad, {dir: path.join(__dirname, 'handlers', 'v1-chain'), options: {prefix: '/v1/chain'}});
fastify.register(AutoLoad, {dir: path.join(__dirname, 'handlers', 'history'), options: {prefix: '/v2/history'}});
fastify.register(AutoLoad, {dir: path.join(__dirname, 'handlers', 'state'), options: {prefix: '/v2/state'}});
fastify.register(require('./handlers/health'), {prefix: '/v2'});
fastify.register(require('./routes/health'), {prefix: '/v2'});
// Serve integrated explorer
fastify.register(require('fastify-static'), {
-30
View File
@@ -1,30 +0,0 @@
const packageData = require('../../package');
const health_link = `https://${process.env.SERVER_NAME}/v2/health`;
const explorer_link = `https://${process.env.SERVER_NAME}/v2/explore`;
const description = `
<img height="64" src="https://eosrio.io/hyperion.png">
### Scalable Full History API Solution for EOSIO based blockchains
*Made with ♥️ by [EOS Rio](https://eosrio.io/)*
***
#### Current Chain: ${process.env.CHAIN_NAME} <img style="transform: translateY(8px)" height="32" src="${process.env.CHAIN_LOGO_URL}">
#### Provided by [${process.env.PROVIDER_NAME}](${process.env.PROVIDER_URL})
#### Health API: <a target="_blank" href="${health_link}">${health_link}</a>
#### Integrated Explorer: <a target="_blank" href="${explorer_link}">${explorer_link}</a>
`;
exports.options = {
routePrefix: '/v2/docs',
exposeRoute: true,
swagger: {
info: {
title: `Hyperion History API for ${process.env.CHAIN_NAME}`,
description: description,
version: packageData.version
},
host: process.env.SERVER_NAME,
schemes: ['https', 'http'],
consumes: ['application/json'],
produces: ['application/json']
}
};
+32
View File
@@ -0,0 +1,32 @@
import {HyperionConfig} from "../../interfaces/hyperionConfig";
export function generateOpenApiConfig(config: HyperionConfig) {
const packageData = require('../../package');
const health_link = `https://${config.api.server_name}/v2/health`;
const explorer_link = `https://${config.api.server_name}/v2/explore`;
const description = `
<img height="64" src="https://eosrio.io/hyperion.png">
### Scalable Full History API Solution for EOSIO based blockchains
*Made with ♥️ by [EOS Rio](https://eosrio.io/)*
***
#### Current Chain: ${config.api.chain_name} <img style="transform: translateY(8px)" height="32" src="${config.api.chain_logo_url}">
#### Provided by [${config.api.provider_name}](${config.api.provider_url})
#### Health API: <a target="_blank" href="${health_link}">${health_link}</a>
#### Integrated Explorer: <a target="_blank" href="${explorer_link}">${explorer_link}</a>
`;
return {
routePrefix: '/v2/docs',
exposeRoute: true,
swagger: {
info: {
title: `Hyperion History API for ${config.api.chain_name}`,
description: description,
version: packageData.version
},
host: config.api.server_name,
schemes: ['https', 'http'],
consumes: ['application/json'],
produces: ['application/json']
}
};
}
-178
View File
@@ -1,178 +0,0 @@
const amqp = require('amqplib');
const {getCacheByHash} = require("../helpers/functions");
const {getLastIndexedBlock} = require("../../helpers/functions");
const {ConnectionManager} = require('../../connections/manager');
const manager = new ConnectionManager();
const ecosystem = require('../../ecosystem.config');
function checkFeat(name) {
if (currentENV) {
return currentENV[name] === 'true';
} else {
return null;
}
}
let currentENV;
if (ecosystem.apps.length > 1) {
const indexerApp = ecosystem.apps.find(app => {
return app.env.CHAIN === process.env.CHAIN && app.script === "./launcher.js";
});
if (indexerApp) {
currentENV = indexerApp['env'];
}
}
// get current github version
let last_commit_hash;
require('child_process').exec('git rev-parse HEAD', function (err, stdout) {
console.log('Last commit hash on this branch is:', stdout);
last_commit_hash = stdout.trim();
});
function createHealth(name, status, data) {
let time = Date.now();
return {
service: name,
status: status,
service_data: data,
time: time
}
}
async function checkRedis(redis) {
let result = await new Promise((resolve) => {
redis.get(process.env.CHAIN + ":" + 'abi_cache', (err, data) => {
if (err) {
resolve('Error');
} else {
try {
const json = JSON.parse(data);
if (json['eosio']) {
resolve('OK');
} else {
resolve('Missing eosio on ABI cache.');
}
} catch (e) {
console.log(e);
resolve('Error');
}
}
});
resolve('OK');
});
return createHealth('Redis', result)
}
async function checkRabbit() {
const amqp_url = manager.ampqUrl;
try {
const connection = await amqp.connect(amqp_url);
connection.close();
return createHealth('RabbitMq', 'OK');
} catch (e) {
console.log(e);
return createHealth('RabbitMq', 'Error');
}
}
async function checkNodeos() {
const rpc = manager.nodeosJsonRPC;
try {
const results = await rpc.get_info();
if (results) {
return createHealth('NodeosRPC', 'OK', {
head_block_num: results.head_block_num,
head_block_time: results.head_block_time,
last_irreversible_block: results.last_irreversible_block_num,
chain_id: results.chain_id
});
} else {
return createHealth('NodeosRPC', 'Error');
}
} catch (e) {
return createHealth('NodeosRPC', 'Error');
}
}
async function checkElastic(elastic) {
try {
let esStatus = await elastic.cat.health({format: 'json', v: true});
let lastIndexedBlock = await getLastIndexedBlock(elastic);
const data = {
last_indexed_block: lastIndexedBlock,
active_shards: esStatus.body[0]['active_shards_percent']
};
let stat = 'OK';
esStatus.body.forEach(status => {
if (status.status === 'yellow' && stat !== 'Error') {
stat = 'Warning'
} else if (status.status === 'red') {
stat = 'Error'
}
});
return createHealth('Elasticsearch', stat, data);
} catch (e) {
console.log(e, 'Elasticsearch Error');
return createHealth('Elasticsearch', 'Error');
}
}
module.exports = function (fastify, opts, next) {
fastify.route({
url: '/health',
method: 'GET',
schema: {
tags: ['status'],
summary: "API Service Health Report"
},
handler: async (request, reply) => {
const t0 = Date.now();
const {redis, elastic} = fastify;
let cachedResponse, hash;
[cachedResponse, hash] = await getCacheByHash(redis, 'health');
if (cachedResponse) {
cachedResponse = JSON.parse(cachedResponse);
cachedResponse['query_time'] = Date.now() - t0;
cachedResponse['cached'] = true;
reply.send(cachedResponse);
return;
}
let response = {
version_hash: last_commit_hash,
host: process.env.SERVER_NAME,
features: {
indices: {
all_deltas: checkFeat('INDEX_ALL_DELTAS'),
transfer_memo: checkFeat('INDEX_TRANSFER_MEMO'),
},
tables: {
proposals: checkFeat('PROPOSAL_STATE'),
accounts: checkFeat('ACCOUNT_STATE'),
voters: checkFeat('VOTERS_STATE')
},
stream: {
traces: checkFeat('STREAM_TRACES'),
deltas: checkFeat('STREAM_DELTAS')
}
},
health: []
};
response.health.push(await checkRabbit());
response.health.push(await checkNodeos());
response.health.push(await checkRedis(redis));
response.health.push(await checkElastic(elastic));
response['query_time'] = Date.now() - t0;
// prevent abuse of the health endpoint
redis.set(hash, JSON.stringify(response), 'EX', 10);
reply.send(response);
}
});
next();
};
-21
View File
@@ -1,21 +0,0 @@
const crypto = require('crypto');
const _ = require('lodash');
async function getCacheByHash(redis, key) {
const hash = crypto.createHash('sha256');
const query_hash = hash.update(process.env.CHAIN + "-" + key).digest('hex');
return [await redis.get(query_hash), query_hash];
}
function mergeActionMeta(action) {
const name = action.act.name;
if (action['@' + name]) {
action['act']['data'] = _.merge(action['@' + name], action['act']['data']);
delete action['@' + name];
}
}
module.exports = {
getCacheByHash,
mergeActionMeta
};
+187
View File
@@ -0,0 +1,187 @@
import {createHash} from "crypto";
import * as _ from "lodash";
import {FastifyInstance, FastifyReply, FastifyRequest, HTTPMethod, RouteSchema} from "fastify";
import {ServerResponse} from "http";
export function extendResponseSchema(responseProps: any) {
const props = {
query_time_ms: {type: "number"},
cached: {type: "boolean"},
lib: {type: "number"},
total: {
type: "object",
properties: {
value: {type: "number"},
relation: {type: "string"}
}
}
};
for (const p in responseProps) {
if (responseProps.hasOwnProperty(p)) {
console.log(p, responseProps[p]);
props[p] = responseProps[p];
}
}
return {
200: {
type: 'object',
properties: props
}
};
}
export function extendQueryStringSchema(queryParams: any) {
const params = {
limit: {
description: 'limit of [n] results per page',
type: 'integer',
minimum: 1
},
skip: {
description: 'skip [n] results',
type: 'integer',
minimum: 0
}
};
for (const p in queryParams) {
if (queryParams.hasOwnProperty(p)) {
params[p] = queryParams[p];
}
}
return {
type: 'object',
properties: params
}
}
export async function getCacheByHash(redis, key, chain) {
const hash = createHash('sha256');
const query_hash = hash.update(chain + "-" + key).digest('hex');
return [await redis.get(query_hash), query_hash];
}
export function mergeActionMeta(action) {
const name = action.act.name;
if (action['@' + name]) {
action['act']['data'] = _.merge(action['@' + name], action['act']['data']);
delete action['@' + name];
}
action['timestamp'] = action['@timestamp'];
delete action['@timestamp'];
}
export function mergeDeltaMeta(delta: any) {
const name = delta.table;
if (delta["@" + name]) {
delta['data'] = _.merge(delta['@' + name], delta['data']);
delete delta['@' + name];
}
delta['timestamp'] = delta['@timestamp'];
delete delta['@timestamp'];
return delta;
}
export function setCacheByHash(fastify, hash, response) {
if (fastify.manager.config.api.enable_caching) {
fastify.redis
.set(hash,
JSON.stringify(response),
'EX',
fastify.manager.config.api.cache_life)
.catch(console.log);
}
}
export function getRouteName(filename: string) {
const arr = filename.split("/");
return arr[arr.length - 2];
}
export function addApiRoute(
fastifyInstance: FastifyInstance,
method: HTTPMethod | HTTPMethod[],
routeName: string,
routeBuilder: (fastify: FastifyInstance, route: string) => (
request: FastifyRequest,
reply: FastifyReply<ServerResponse>
) => Promise<void>,
schema: RouteSchema) {
fastifyInstance.route({
url: '/' + routeName,
method,
handler: routeBuilder(fastifyInstance, routeName),
schema
});
}
export async function getCachedResponse(server: FastifyInstance, route: string, key: any) {
const chain = server.manager.chain;
let resp, hash;
if (server.manager.config.api.enable_caching) {
const keystring = JSON.stringify(key);
[resp, hash] = await getCacheByHash(server.redis, route + keystring, chain);
if (resp) {
resp = JSON.parse(resp);
resp['cached'] = true;
return [resp, hash];
} else {
return [null, hash];
}
} else {
return [null, null];
}
}
export function getTrackTotalHits(query) {
let trackTotalHits: number | boolean = 10000;
if (query?.track) {
if (query.track === 'true') {
trackTotalHits = true;
} else if (query.track === 'false') {
trackTotalHits = false;
} else {
trackTotalHits = parseInt(query.track, 10);
if (isNaN(trackTotalHits)) {
throw new Error('failed to parse track param');
}
}
}
return trackTotalHits;
}
function bigint2Milliseconds(input: bigint) {
return parseFloat((parseInt(input.toString()) / 1000000).toFixed(3));
}
export async function timedQuery(
queryFunction: (fastify: FastifyInstance, request: FastifyRequest) => Promise<any>,
fastify: FastifyInstance, request: FastifyRequest, route: string): Promise<any> {
// get reference time in nanoseconds
const t0 = process.hrtime.bigint();
// check for cached data, return the response hash if caching is enabled
const [cachedResponse, hash] = await getCachedResponse(fastify, route, request.query);
if (cachedResponse) {
// add cached query time
cachedResponse['query_time_ms'] = bigint2Milliseconds(process.hrtime.bigint() - t0);
return cachedResponse;
}
// call query function
const response = await queryFunction(fastify, request);
// save response to cash
if (hash) {
setCacheByHash(fastify, hash, response);
}
// add normal query time
if (response) {
response['query_time_ms'] = bigint2Milliseconds(process.hrtime.bigint() - t0);
return response;
} else {
return {};
}
}
+23
View File
@@ -0,0 +1,23 @@
import * as Fastify from "fastify";
import {IncomingMessage, Server, ServerResponse} from "http";
// fastify plugins
import * as fastify_elasticsearch from 'fastify-elasticsearch';
import * as fastify_oas from 'fastify-oas';
import * as fastify_cors from 'fastify-cors';
import * as fastify_formbody from 'fastify-formbody';
import * as fastify_redis from 'fastify-redis';
import * as fastify_rate_limit from 'fastify-rate-limit';
// custom plugins
import fastify_eosjs from "./plugins/fastify-eosjs";
export function registerPlugins(server: Fastify.FastifyInstance<Server, IncomingMessage, ServerResponse>, params: any) {
server.register(fastify_elasticsearch, params.fastify_elasticsearch);
server.register(fastify_oas, params.fastify_oas);
server.register(fastify_cors);
server.register(fastify_formbody);
server.register(fastify_redis, params.fastify_redis);
server.register(fastify_eosjs, params.fastify_eosjs);
server.register(fastify_rate_limit, params.fastify_rate_limit);
}
+20
View File
@@ -0,0 +1,20 @@
import * as fp from 'fastify-plugin';
import {FastifyInstance} from "fastify";
import {Api} from "eosjs/dist";
export default fp(async (fastify: FastifyInstance, options, next) => {
const rpc = fastify.manager.nodeosJsonRPC;
const chain_data = await rpc.get_info();
const api = new Api({
rpc,
signatureProvider: null,
chainId: chain_data.chain_id,
textDecoder: new TextDecoder(),
textEncoder: new TextEncoder(),
});
fastify.decorate('eosjs', {api, rpc});
next();
}, {
fastify: '>=2.0.0',
name: 'fastify-eosjs'
});
+56
View File
@@ -0,0 +1,56 @@
import * as fastify_static from "fastify-static";
import {join} from "path";
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {createReadStream} from "fs";
import * as AutoLoad from "fastify-autoload";
function addRedirect(server: FastifyInstance, url: string, redirectTo: string) {
server.route({
url,
method: 'GET',
schema: {hide: true},
handler: async (request, reply) => {
reply.redirect(redirectTo);
}
});
}
function addRoute(server: FastifyInstance, handlersPath: string, prefix: string) {
server.register(AutoLoad, {
dir: join(__dirname, 'routes', handlersPath),
ignorePattern: /.*(handler|schema).js/,
options: {prefix}
});
}
export function registerRoutes(server: FastifyInstance) {
// Register fastify api routes
addRoute(server, 'v2', '/v2');
addRoute(server, 'v2-history', '/v2/history');
addRoute(server, 'v2-state', '/v2/state');
// addRoute(server,'v1-chain', '/v1/chain');
// addRoute(server,'history', '/v2/history');
// addRoute(server,'state', '/v2/state');
// server.register(health, {prefix: '/v2'});
// Serve integrated explorer
server.register(fastify_static, {
root: join(__dirname, '..', 'hyperion-explorer', 'dist'),
redirect: true,
wildcard: true,
prefix: '/v2/explore'
});
// steam client lib
server.get('/stream-client.js', (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
const stream = createReadStream('./api/client_bundle.js');
reply.type('application/javascript').send(stream);
});
// Redirect routes to documentation
addRedirect(server, '/v2', '/v2/docs');
addRedirect(server, '/v2/history', '/v2/docs/index.html#/history');
addRedirect(server, '/v2/state', '/v2/docs/index.html#/state');
}
@@ -10,7 +10,7 @@ const extendedActions = new Set(["transfer", "newaccount", "updateauth"]);
const schema = {
description: 'legacy get actions query',
summary: 'get actions',
tags: ['actions','history'],
tags: ['actions', 'history'],
body: {
type: ['object', 'string'],
properties: {
@@ -128,6 +128,7 @@ async function get_actions(fastify, request) {
request.body = JSON.parse(request.body)
}
const reqBody = request.body;
console.log(reqBody);
const t0 = Date.now();
const {redis, elastic, eosjs} = fastify;
@@ -0,0 +1,24 @@
export const terms = [
"notified.keyword",
"act.authorization.actor"
];
export const extendedActions = new Set([
"transfer",
"newaccount",
"updateauth",
"buyram",
"buyrambytes"
]);
export const primaryTerms = [
"notified",
"block_num",
"global_sequence",
"producer",
"@timestamp",
"creator_action_ordinal",
"action_ordinal",
"cpu_usage_us",
"net_usage_words"
];
@@ -0,0 +1,213 @@
import {extendedActions, primaryTerms, terms} from "./definitions";
export function addSortedBy(query, queryBody, sort_direction) {
if (query['sortedBy']) {
const opts = query['sortedBy'].split(":");
const sortedByObj = {};
sortedByObj[opts[0]] = opts[1];
queryBody['sort'] = sortedByObj;
} else {
queryBody['sort'] = {
"global_sequence": sort_direction
};
}
}
export function processMultiVars(queryStruct, parts, field) {
const must = [];
const mustNot = [];
parts.forEach(part => {
if (part.startsWith("!")) {
mustNot.push(part.replace("!", ""));
} else {
must.push(part);
}
});
if (must.length > 1) {
queryStruct.bool.must.push({
bool: {
should: must.map(elem => {
const _q = {};
_q[field] = elem;
return {term: _q}
})
}
});
} else if (must.length === 1) {
const mustQuery = {};
mustQuery[field] = must[0];
queryStruct.bool.must.push({term: mustQuery});
}
if (mustNot.length > 1) {
queryStruct.bool.must_not.push({
bool: {
should: mustNot.map(elem => {
const _q = {};
_q[field] = elem;
return {term: _q}
})
}
});
} else if (mustNot.length === 1) {
const mustNotQuery = {};
mustNotQuery[field] = mustNot[0].replace("!", "");
queryStruct.bool.must_not.push({term: mustNotQuery});
}
}
function addRangeQuery(queryStruct, prop, pkey, query) {
const _termQuery = {};
const parts = query[prop].split("-");
_termQuery[pkey] = {
"gte": parts[0],
"lte": parts[1]
};
queryStruct.bool.must.push({range: _termQuery});
}
export function applyTimeFilter(query, queryStruct) {
if (query['after'] || query['before']) {
let _lte = "now";
let _gte = "0";
if (query['before']) {
_lte = query['before'];
if (!_lte.endsWith("Z")) {
_lte += "Z";
}
}
if (query['after']) {
_gte = query['after'];
if (!_gte.endsWith("Z")) {
_gte += "Z";
}
}
if (!queryStruct.bool['filter']) {
queryStruct.bool['filter'] = [];
}
queryStruct.bool['filter'].push({
range: {
"@timestamp": {
"gte": _gte,
"lte": _lte
}
}
});
}
}
export function applyGenericFilters(query, queryStruct) {
for (const prop in query) {
if (Object.prototype.hasOwnProperty.call(query, prop)) {
const pair = prop.split(".");
if (pair.length > 1 || primaryTerms.includes(pair[0])) {
let pkey;
if (pair.length > 1) {
pkey = extendedActions.has(pair[0]) ? "@" + prop : prop;
} else {
pkey = prop;
}
if (query[prop].indexOf("-") !== -1) {
addRangeQuery(queryStruct, prop, pkey, query);
} else {
const _termQuery = {};
const parts = query[prop].split(",");
if (parts.length > 1) {
processMultiVars(queryStruct, parts, prop);
} else if (parts.length === 1) {
const andParts = parts[0].split(" ");
if (andParts.length > 1) {
andParts.forEach(value => {
const _q = {};
console.log(value);
_q[pkey] = value;
queryStruct.bool.must.push({term: _q});
});
} else {
if (parts[0].startsWith("!")) {
_termQuery[pkey] = parts[0].replace("!", "");
queryStruct.bool.must_not.push({term: _termQuery});
} else {
_termQuery[pkey] = parts[0];
queryStruct.bool.must.push({term: _termQuery});
}
}
}
}
}
}
}
}
export function makeShouldArray(query) {
const should_array = [];
for (const entry of terms) {
const tObj = {term: {}};
tObj.term[entry] = query.account;
should_array.push(tObj);
}
return should_array;
}
export function applyCodeActionFilters(query, queryStruct) {
let filterObj = [];
if (query.filter) {
for (const filter of query.filter.split(',')) {
if (filter !== '*:*') {
const _arr = [];
const parts = filter.split(':');
if (parts.length === 2) {
const [code, method] = parts;
if (code && code !== "*") {
_arr.push({'term': {'act.account': code}});
}
if (method && method !== "*") {
_arr.push({'term': {'act.name': method}});
}
}
if (_arr.length > 0) {
filterObj.push({bool: {must: _arr}});
}
}
}
if (filterObj.length > 0) {
queryStruct.bool['should'] = filterObj;
queryStruct.bool['minimum_should_match'] = 1;
}
}
}
export function getSkipLimit(query) {
let skip, limit;
skip = parseInt(query.skip, 10);
if (skip < 0) {
throw new Error('invalid skip parameter');
}
limit = parseInt(query.limit, 10);
if (limit < 1) {
throw new Error('invalid limit parameter');
}
return {skip, limit};
}
export function getSortDir(query) {
let sort_direction = 'desc';
if (query.sort) {
if (query.sort === 'asc' || query.sort === '1') {
sort_direction = 'asc';
} else if (query.sort === 'desc' || query.sort === '-1') {
sort_direction = 'desc'
} else {
throw new Error('invalid sort direction');
}
}
return sort_direction;
}
export function applyAccountFilters(query, queryStruct) {
if (query.account) {
queryStruct.bool.must.push({"bool": {should: makeShouldArray(query)}});
}
}
@@ -0,0 +1,96 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {getTrackTotalHits, mergeActionMeta, timedQuery} from "../../../helpers/functions";
import {
addSortedBy,
applyAccountFilters,
applyCodeActionFilters,
applyGenericFilters,
applyTimeFilter,
getSkipLimit,
getSortDir
} from "./functions";
async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
const query = request.query;
const queryStruct = {
"bool": {
must: [],
must_not: [],
boost: 1.0
}
};
const {skip, limit} = getSkipLimit(query);
const sort_direction = getSortDir(query);
applyAccountFilters(query, queryStruct);
applyGenericFilters(query, queryStruct);
applyTimeFilter(query, queryStruct);
applyCodeActionFilters(query, queryStruct);
// allow precise counting of total hits
const trackTotalHits = getTrackTotalHits(query);
// Prepare query body
const query_body = {
"track_total_hits": trackTotalHits,
"query": queryStruct
};
// Include sorting
addSortedBy(query, query_body, sort_direction);
// Perform search
const maxActions = fastify.manager.config.api.limits.get_actions;
const pResults = await Promise.all([
fastify.eosjs.rpc.get_info(),
fastify.elastic.search({
"index": fastify.manager.chain + '-action-*',
"from": skip || 0,
"size": (limit > maxActions ? maxActions : limit) || 10,
"body": query_body
})
]);
const results = pResults[1]['body']['hits'];
const response: any = {
cached: false,
lib: pResults[0].last_irreversible_block_num,
total: results['total']
};
if (query.simple) {
response['simple_actions'] = [];
} else {
response['actions'] = [];
}
if (results['hits'].length > 0) {
const actions = results['hits'];
for (let action of actions) {
action = action._source;
mergeActionMeta(action);
if (query.simple) {
response.simple_actions.push({
block: action['block_num'],
irreversible: action['block_num'] < pResults[0].last_irreversible_block_num,
timestamp: action['@timestamp'],
transaction_id: action['trx_id'],
actors: action['act']['authorization'].map(a => `${a.actor}@${a.permission}`).join(","),
notified: action['notified'].join(','),
contract: action['act']['account'],
action: action['act']['name'],
data: action['act']['data']
});
} else {
response.actions.push(action);
}
}
}
return response;
}
export function getActionsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getActions, fastify, request, route));
}
}
+116
View File
@@ -0,0 +1,116 @@
import {FastifyInstance, RouteSchema} from "fastify";
import {getActionsHandler} from "./get_actions";
import {addApiRoute, extendQueryStringSchema, extendResponseSchema, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema: RouteSchema = {
description: 'get actions based on notified account. this endpoint also accepts generic filters based on indexed fields' +
' (e.g. act.authorization.actor=eosio or act.name=delegatebw), if included they will be combined with a AND operator',
summary: 'get root actions',
tags: ['actions', 'history'],
querystring: extendQueryStringSchema({
"account": {
description: 'notified account',
type: 'string',
minLength: 1,
maxLength: 12
},
"track": {
description: 'total results to track (count) [number or true]',
type: 'string'
},
"filter": {
description: 'code:name filter',
type: 'string',
minLength: 3
},
"sort": {
description: 'sort direction',
enum: ['desc', 'asc', '1', '-1'],
type: 'string'
},
"after": {
description: 'filter after specified date (ISO8601)',
type: 'string'
},
"before": {
description: 'filter before specified date (ISO8601)',
type: 'string'
},
"simple": {
description: 'simplified output mode',
type: 'boolean'
}
}),
response: extendResponseSchema({
"simple_actions": {
type: "array",
items: {
type: "object",
properties: {
"block": {type: "number"},
"timestamp": {type: "string"},
"irreversible": {type: "boolean"},
"contract": {type: "string"},
"action": {type: "string"},
"actors": {type: "string"},
"notified": {type: "string"},
"transaction_id": {type: "string"},
"data": {
additionalProperties: true
}
}
}
},
"actions": {
type: "array",
items: {
type: 'object',
properties: {
"timestamp": {type: "string"},
"block_num": {type: "number"},
"trx_id": {type: "string"},
"act": {
type: 'object',
properties: {
"account": {type: "string"},
"name": {type: "string"}
},
additionalProperties: true
},
"notified": {
type: "array", items: {type: "string"}
},
"cpu_usage_us": {type: "number"},
"net_usage_words": {type: "number"},
"account_ram_deltas": {
type: "array",
items: {
type: "object",
properties: {
"account": {type: "string"},
"delta": {type: "number"}
},
additionalProperties: true
}
},
"global_sequence": {type: "number"},
"receiver": {type: 'string'},
"producer": {type: "string"},
"parent": {type: "number"},
"action_ordinal": {type: 'number'},
"creator_action_ordinal": {type: 'number'}
}
}
}
})
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getActionsHandler,
schema
);
next();
}
@@ -0,0 +1,77 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {mergeDeltaMeta, timedQuery} from "../../../helpers/functions";
async function getDeltas(fastify: FastifyInstance, request: FastifyRequest) {
let skip, limit;
let sort_direction = 'desc';
const mustArray = [];
for (const param in request.query) {
if (Object.prototype.hasOwnProperty.call(request.query, param)) {
console.log(param, request.query[param]);
const value = request.query[param];
switch (param) {
case 'limit': {
limit = parseInt(value, 10);
if (limit < 1) {
return 'invalid limit parameter';
}
break;
}
case 'skip': {
skip = parseInt(value, 10);
if (skip < 0) {
return 'invalid skip parameter';
}
break;
}
case 'sort': {
if (value === 'asc' || value === '1') {
sort_direction = 'asc';
} else if (value === 'desc' || value === '-1') {
sort_direction = 'desc'
} else {
return 'invalid sort direction';
}
break;
}
default: {
const values = request.query[param].split(",");
const terms = {};
terms[param] = values;
const shouldArray = {terms: terms};
const boolStruct = {bool: {should: [shouldArray]}};
mustArray.push(boolStruct);
break;
}
}
}
}
const maxDeltas = fastify.manager.config.api.limits.get_deltas ?? 1000;
const results = await fastify.elastic.search({
"index": fastify.manager.chain + '-delta-*',
"from": skip || 0,
"size": (limit > maxDeltas ? maxDeltas : limit) || 10,
"body": {
query: {bool: {must: mustArray}},
sort: {
"block_num": sort_direction
}
}
});
const deltas = results['body']['hits']['hits'].map((d) => {
return mergeDeltaMeta(d._source);
});
return {
query_time: null,
total: results['body']['hits']['total'],
deltas
};
}
export function getDeltasHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getDeltas, fastify, request, route));
}
}
+61
View File
@@ -0,0 +1,61 @@
import {FastifyInstance, RouteSchema} from "fastify";
import {getDeltasHandler} from "./get_deltas";
import {addApiRoute, extendQueryStringSchema, extendResponseSchema, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema: RouteSchema = {
description: 'get state deltas',
summary: 'get state deltas',
tags: ['history'],
querystring: extendQueryStringSchema({
"code": {
description: 'contract account',
type: 'string'
},
"scope": {
description: 'table scope',
type: 'string'
},
"table": {
description: 'table name',
type: 'string'
},
"payer": {
description: 'payer account',
type: 'string'
}
}),
response: extendResponseSchema({
"deltas": {
type: "array",
items: {
type: 'object',
properties: {
"timestamp": {type: 'string'},
"code": {type: 'string'},
"scope": {type: 'string'},
"table": {type: 'string'},
"primary_key": {type: 'string'},
"payer": {type: 'string'},
"present": {type: 'boolean'},
"block_num": {type: 'number'},
"data": {
type: 'object',
additionalProperties: true
}
},
additionalProperties: true
}
}
})
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getDeltasHandler,
schema
);
next();
}
@@ -0,0 +1,45 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {mergeActionMeta, timedQuery} from "../../../helpers/functions";
async function getTransaction(fastify: FastifyInstance, request: FastifyRequest) {
const pResults = await Promise.all([
fastify.eosjs.rpc.get_info(),
fastify.elastic.search({
"index": fastify.manager.chain + '-action-*',
"body": {
"query": {
"bool": {
must: [
{term: {"trx_id": request.query.id.toLowerCase()}}
]
}
},
"sort": {
"global_sequence": "asc"
}
}
})
]);
const results = pResults[1];
const response = {
"trx_id": request.query.id,
"lib": pResults[0].last_irreversible_block_num,
"actions": []
};
const hits = results['body']['hits']['hits'];
if (hits.length > 0) {
for (let action of hits) {
action = action._source;
mergeActionMeta(action);
response.actions.push(action);
}
}
return response;
}
export function getTransactionHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getTransaction, fastify, request, route));
}
}
@@ -0,0 +1,29 @@
import {FastifyInstance} from "fastify";
import {getTransactionHandler} from "./get_transaction";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get all actions belonging to the same transaction',
summary: 'get transaction by id',
tags: ['transactions', 'history'],
querystring: {
type: 'object',
properties: {
"id": {
description: 'transaction id',
type: 'string'
}
},
required: ["id"]
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getTransactionHandler,
schema
);
next();
}
@@ -0,0 +1,42 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import got from "got";
import {timedQuery} from "../../../helpers/functions";
async function getAccount(fastify: FastifyInstance, request: FastifyRequest) {
const response = {
query_time: null,
cached: false,
account: null,
actions: null,
tokens: null,
links: null
};
const account = request.query.account;
const reqQueue = [];
reqQueue.push(fastify.eosjs.rpc.get_account(account));
const localApi = `http://${fastify.manager.config.api.server_addr}:${fastify.manager.config.api.server_port}/v2`;
const getTokensApi = localApi + '/state/get_tokens';
const getActionsApi = localApi + '/history/get_actions';
// fetch recent actions
reqQueue.push(got.get(`${getActionsApi}?account=${account}&limit=10`));
// fetch account tokens
reqQueue.push(got.get(`${getTokensApi}?account=${account}`));
const results = await Promise.all(reqQueue);
response.account = results[0];
response.actions = JSON.parse(results[1].body).actions;
response.tokens = JSON.parse(results[2].body).tokens;
return response;
}
export function getAccountHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getAccount, fastify, request, route));
}
}
+28
View File
@@ -0,0 +1,28 @@
import {FastifyInstance} from "fastify";
import {getAccountHandler} from "./get_account";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get account data',
summary: 'get account summary',
tags: ['accounts', 'state'],
querystring: {
type: 'object',
properties: {
"account": {
description: 'account name',
type: 'string'
}
}
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getAccountHandler,
schema
);
next();
}
@@ -0,0 +1,100 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {getTrackTotalHits, timedQuery} from "../../../helpers/functions";
async function getProposals(fastify: FastifyInstance, request: FastifyRequest) {
// Pagination
let skip, limit;
skip = parseInt(request.query.skip, 10);
if (skip < 0) {
return 'invalid skip parameter';
}
limit = parseInt(request.query.limit, 10);
if (limit < 1) {
return 'invalid limit parameter';
}
let queryStruct: any = {
"bool": {
"must": []
}
};
// Filter by account
if (request.query.account) {
queryStruct.bool.must.push({
"bool": {
"should": [
{"term": {"requested_approvals.actor": request.query.account}},
{"term": {"provided_approvals.actor": request.query.account}}
]
}
});
}
// Filter by proposer account
if (request.query.proposer) {
queryStruct.bool.must.push({"term": {"proposer": request.query.proposer}});
}
// Filter by proposal name
if (request.query.proposal) {
queryStruct.bool.must.push({"term": {"proposal_name": request.query.proposal}});
}
// Filter by execution status
if (typeof request.query.executed !== 'undefined') {
queryStruct.bool.must.push({"term": {"executed": request.query.executed}});
}
// Filter by requested actors
if (request.query.requested) {
queryStruct.bool.must.push({"term": {"requested_approvals.actor": request.query.requested}});
}
// Filter by provided actors
if (request.query.provided) {
queryStruct.bool.must.push({"term": {"provided_approvals.actor": request.query.provided}});
}
// If no filter switch to full match
if (queryStruct.bool.must.length === 0) {
queryStruct = {
"match_all": {}
};
}
const maxDocs = fastify.manager.config.api.limits.get_proposals ?? 100;
const results = await fastify.elastic.search({
"index": fastify.manager.chain + '-table-proposals-*',
"from": skip || 0,
"size": (limit > maxDocs ? maxDocs : limit) || 10,
"body": {
"track_total_hits": getTrackTotalHits(request.query),
"query": queryStruct,
"sort": [{"block_num": "desc"}]
}
});
const response = {
query_time: null,
cached: false,
total: results['body']['hits']['total'],
proposals: []
};
const hits = results['body']['hits']['hits'];
for (const hit of hits) {
const prop = hit._source;
response.proposals.push(prop);
}
return response;
}
export function getProposalsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getProposals, fastify, request, route));
}
}
@@ -0,0 +1,62 @@
import {FastifyInstance} from "fastify";
import {getProposalsHandler} from "./get_proposals";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get proposals',
summary: 'get proposals',
tags: ['state'],
querystring: {
type: 'object',
properties: {
"proposer": {
description: 'filter by proposer',
type: 'string'
},
"proposal": {
description: 'filter by proposal name',
type: 'string'
},
"account": {
description: 'filter by either requested or provided account',
type: 'string'
},
"requested": {
description: 'filter by requested account',
type: 'string'
},
"provided": {
description: 'filter by provided account',
type: 'string'
},
"executed": {
description: 'filter by execution status',
type: 'boolean'
},
"track": {
description: 'total results to track (count) [number or true]',
type: 'string'
},
"skip": {
description: 'skip [n] actions (pagination)',
type: 'integer',
minimum: 0
},
"limit": {
description: 'limit of [n] actions per page',
type: 'integer',
minimum: 1
}
}
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getProposalsHandler,
schema
);
next();
}
@@ -0,0 +1,69 @@
import {ServerResponse} from "http";
import {timedQuery} from "../../../helpers/functions";
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
async function getTokens(fastify: FastifyInstance, request: FastifyRequest) {
const response = {
query_time: null,
cached: false,
'account': request.query.account,
'tokens': []
};
const results = await fastify.elastic.search({
"index": process.env.CHAIN + '-action-*',
"body": {
size: 0,
query: {
bool: {
// must_not: {term: {"act.account": "eosio.token"}},
filter: [
{term: {"notified": request.query.account}},
{terms: {"act.name": ["transfer", "issue"]}}
]
}
},
aggs: {
tokens: {
terms: {
field: "act.account",
size: 1000
}
}
}
}
});
for (const bucket of results['body']['aggregations']['tokens']['buckets']) {
let token_data;
try {
token_data = await fastify.eosjs.rpc.get_currency_balance(bucket['key'], request.query.account);
} catch (e) {
console.log(`get_currency_balance error - contract:${bucket['key']} - account:${request.query.account}`);
continue;
}
for (const entry of token_data) {
let precision = 0;
const [amount, symbol] = entry.split(" ");
const amount_arr = amount.split(".");
if (amount_arr.length === 2) {
precision = amount_arr[1].length;
}
response.tokens.push({
symbol: symbol,
precision: precision,
amount: parseFloat(amount),
contract: bucket['key']
});
}
}
return response;
}
export function getTokensHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getTokens, fastify, request, route));
}
}
+28
View File
@@ -0,0 +1,28 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
import {getTokensHandler} from "./get_tokens";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get account data',
summary: 'get account summary',
tags: ['accounts', 'state'],
querystring: {
type: 'object',
properties: {
"account": {
description: 'account name',
type: 'string'
}
}
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getTokensHandler,
schema
);
next();
}
@@ -0,0 +1,72 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {timedQuery} from "../../../helpers/functions";
async function getVoters(fastify: FastifyInstance, request: FastifyRequest) {
let skip, limit;
skip = parseInt(request.query.skip, 10);
if (skip < 0) {
return 'invalid skip parameter';
}
limit = parseInt(request.query.limit, 10);
if (limit < 1) {
return 'invalid limit parameter';
}
const response = {
query_time: null,
voter_count: 0,
'voters': []
};
let queryStruct: any = {
"bool": {
"must": []
}
};
if (request.query.producer) {
for (const bp of request.query.producer.split(",")) {
queryStruct.bool.must.push({"term": {"producers": bp}});
}
}
if (request.query.proxy === 'true') {
queryStruct.bool.must.push({"term": {"is_proxy": true}});
}
if (queryStruct.bool.must.length === 0) {
queryStruct = {
"match_all": {}
};
}
const maxDocs = fastify.manager.config.api.limits.get_voters ?? 100;
const results = await fastify.elastic.search({
"index": fastify.manager.chain + '-table-voters-*',
"from": skip || 0,
"size": (limit > maxDocs ? maxDocs : limit) || 10,
"body": {
"query": queryStruct,
"sort": [{"last_vote_weight": "desc"}]
}
});
const hits = results['body']['hits']['hits'];
for (const hit of hits) {
const voter = hit._source;
response.voters.push({
account: voter.voter,
weight: voter.last_vote_weight,
last_vote: voter.block_num
});
}
response.voter_count = results['body']['hits']['total']['value'];
return response;
}
export function getVotersHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getVoters, fastify, request, route));
}
}
+38
View File
@@ -0,0 +1,38 @@
import {FastifyInstance} from "fastify";
import {getVotersHandler} from "./get_voters";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get voters',
summary: 'get voters',
tags: ['accounts', 'state'],
querystring: {
type: 'object',
properties: {
"producer": {
description: 'filter by voted producer (comma separated)',
type: 'string'
},
"skip": {
description: 'skip [n] actions (pagination)',
type: 'integer',
minimum: 0
},
"limit": {
description: 'limit of [n] actions per page',
type: 'integer',
minimum: 1
}
}
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getVotersHandler,
schema
);
next();
}
+116
View File
@@ -0,0 +1,116 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {Redis} from "ioredis";
import {connect} from "amqplib";
import {timedQuery} from "../../../helpers/functions";
import {getLastIndexedBlock} from "../../../../helpers/common_functions";
async function checkRedis(fastify: FastifyInstance) {
let result = await new Promise((resolve) => {
fastify.redis.get(fastify.manager.chain + ":" + 'abi_cache', (err, data) => {
if (err) {
resolve('Error');
} else {
try {
const json = JSON.parse(data);
if (json['eosio']) {
resolve('OK');
} else {
resolve('Missing eosio on ABI cache.');
}
} catch (e) {
console.log(e);
resolve('Error');
}
}
});
resolve('OK');
});
return createHealth('Redis', result)
}
async function checkRabbit(fastify: FastifyInstance) {
try {
const connection = await connect(fastify.manager.ampqUrl);
await connection.close();
return createHealth('RabbitMq', 'OK');
} catch (e) {
console.log(e);
return createHealth('RabbitMq', 'Error');
}
}
async function checkNodeos(fastify: FastifyInstance) {
const rpc = fastify.manager.nodeosJsonRPC;
try {
const results = await rpc.get_info();
if (results) {
const diff = (new Date().getTime()) - (new Date(results.head_block_time).getTime());
return createHealth('NodeosRPC', 'OK', {
head_block_num: results.head_block_num,
head_block_time: results.head_block_time,
time_offset: diff,
last_irreversible_block: results.last_irreversible_block_num,
chain_id: results.chain_id
});
} else {
return createHealth('NodeosRPC', 'Error');
}
} catch (e) {
return createHealth('NodeosRPC', 'Error');
}
}
async function checkElastic(fastify: FastifyInstance) {
try {
let esStatus = await fastify.elastic.cat.health({format: 'json', v: true});
let lastIndexedBlock = await getLastIndexedBlock(fastify.elastic, fastify.manager.chain);
const data = {
last_indexed_block: lastIndexedBlock,
active_shards: esStatus.body[0]['active_shards_percent']
};
let stat = 'OK';
esStatus.body.forEach(status => {
if (status.status === 'yellow' && stat !== 'Error') {
stat = 'Warning'
} else if (status.status === 'red') {
stat = 'Error'
}
});
return createHealth('Elasticsearch', stat, data);
} catch (e) {
console.log(e, 'Elasticsearch Error');
return createHealth('Elasticsearch', 'Error');
}
}
function createHealth(name: string, status, data?: any) {
let time = Date.now();
return {
service: name,
status: status,
service_data: data,
time: time
}
}
async function getHealthQuery(fastify: FastifyInstance, request: FastifyRequest) {
let response = {
version: fastify.manager.current_version,
version_hash: fastify.manager.getServerHash(),
host: fastify.manager.config.api.server_name,
features: fastify.manager.config.features,
health: []
};
response.health.push(await checkRabbit(fastify));
response.health.push(await checkNodeos(fastify));
response.health.push(await checkRedis(fastify));
response.health.push(await checkElastic(fastify));
return response;
}
export function healthHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getHealthQuery, fastify, request, route));
}
}
+18
View File
@@ -0,0 +1,18 @@
import {FastifyInstance, RouteSchema} from "fastify";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
import {healthHandler} from "./health";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema: RouteSchema = {
tags: ['status'],
summary: "API Service Health Report"
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
healthHandler,
schema
);
next();
}
+110
View File
@@ -0,0 +1,110 @@
import {hLog} from "../helpers/common_functions";
import {ConfigurationModule} from "../modules/config";
import {ConnectionManager} from "../connections/manager.class";
import {HyperionConfig} from "../interfaces/hyperionConfig";
import {IncomingMessage, Server, ServerResponse} from "http";
import * as Fastify from 'fastify';
import * as Redis from 'ioredis';
import {registerPlugins} from "./plugins";
import {AddressInfo} from "net";
import {registerRoutes} from "./routes";
import {generateOpenApiConfig} from "./config/open_api";
class HyperionApiServer {
private conf: HyperionConfig;
private readonly manager: ConnectionManager;
private readonly fastify: Fastify.FastifyInstance<Server, IncomingMessage, ServerResponse>;
private readonly chain: string;
constructor() {
const cm = new ConfigurationModule();
this.conf = cm.config;
this.chain = this.conf.settings.chain;
process.title = `hyp-${this.chain}-api`;
this.manager = new ConnectionManager(cm);
this.manager.calculateServerHash();
this.manager.getHyperionVersion();
this.fastify = Fastify({
ignoreTrailingSlash: false, trustProxy: true, logger: {
level: 'trace'
}
});
this.fastify.decorate('manager', this.manager);
const ioRedisClient = new Redis(this.manager.conn.redis);
const api_rate_limit = {
max: 1000,
whitelist: [],
timeWindow: '1 minute',
redis: ioRedisClient
};
if (this.conf.features.streaming.enable) {
this.activateStreaming();
}
registerPlugins(this.fastify, {
fastify_elasticsearch: {
client: this.manager.elasticsearchClient
},
fastify_oas: generateOpenApiConfig(this.manager.config),
fastify_rate_limit: api_rate_limit,
fastify_redis: this.manager.conn.redis,
fastify_eosjs: this.manager,
});
registerRoutes(this.fastify);
this.addGenericTypeParsing();
this.fastify.ready().then(async () => {
await this.fastify.oas();
console.log(this.chain + ' api ready!');
}, (err) => {
console.log('an error happened', err)
});
}
activateStreaming() {
const connOpts = this.manager.conn.chains[this.chain];
const {SocketManager} = require("./socketManager");
const socketManager = new SocketManager(
this.fastify,
`http://${connOpts['WS_ROUTER_HOST']}:${connOpts['WS_ROUTER_PORT']}`,
this.manager.conn.redis
);
socketManager.startRelay();
}
private addGenericTypeParsing() {
this.fastify.addContentTypeParser('*', (req, done) => {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
done(null, data);
});
req.on('error', (err) => {
console.log('---- Content Parsing Error -----');
console.log(err);
});
});
}
async init() {
try {
await this.fastify.listen({
host: this.conf.api.server_addr,
port: this.conf.api.server_port
});
console.log(`server listening on ${(this.fastify.server.address() as AddressInfo).port}`);
} catch (err) {
this.fastify.log.error(err);
process.exit(1)
}
}
}
const server = new HyperionApiServer();
server.init().catch(hLog);
-71
View File
@@ -1,71 +0,0 @@
{
"settings": {
"preview": true,
"chain": "example_chain",
"eosio_alias": "eosio",
"parser": "1.8",
"auto_stop": 300,
"index_version": "v1",
"debug": false,
"rate_monitoring": true,
"bp_logs": false
},
"blacklists": {
"actions": [],
"deltas": []
},
"whitelists": {
"actions": [],
"deltas": []
},
"scaling": {
"batch_size": 5000,
"queue_limit": 10000,
"readers": 1,
"ds_queues": 1,
"ds_threads": 1,
"ds_pool_size": 1,
"indexing_queues": 1,
"ad_idx_queues": 1
},
"indexer": {
"start_on": 0,
"stop_on": 0,
"rewrite": false,
"abi_scan_mode": false,
"purge_queues": false,
"live_reader": true,
"live_only_mode": false,
"fetch_block": true,
"fetch_traces": true,
"disable_reading": false,
"disable_indexing": false,
"process_deltas": true,
"repair_mode": false
},
"features": {
"streaming": {
"enable": true,
"traces": true,
"deltas": true
},
"tables": {
"proposals": true,
"accounts": true,
"voters": true,
"userres": false,
"delband": false
},
"index_deltas": true,
"index_transfer_memo": true,
"index_all_deltas": false
},
"prefetch": {
"read": 50,
"block": 100,
"index": 500
},
"experimental": {
"PATCHED_SHIP": false
}
}
+32 -3
View File
@@ -1,4 +1,19 @@
{
"api": {
"chain_name": "REM Mainnet",
"server_addr": "192.168.0.130",
"server_port": 7000,
"server_name": "192.168.0.130:7000",
"provider_name": "EOS Rio",
"provider_url": "https://eosrio.io",
"chain_logo_url": "",
"enable_caching": true,
"cache_life": 1,
"limits": {
"get_actions": 1000,
"get_voters": 100
}
},
"settings": {
"preview": false,
"chain": "rem",
@@ -8,7 +23,20 @@
"index_version": "v1",
"debug": false,
"rate_monitoring": true,
"bp_logs": false
"bp_logs": false,
"bp_monitoring": false
},
"blacklists": {
"actions": [
"rem::rem.null::*"
],
"deltas": [
"rem::rem::global"
]
},
"whitelists": {
"actions": [],
"deltas": []
},
"scaling": {
"batch_size": 10000,
@@ -16,15 +44,16 @@
"readers": 1,
"ds_queues": 1,
"ds_threads": 1,
"ds_pool_size": 2,
"indexing_queues": 1,
"ad_idx_queues": 1
},
"indexer": {
"start_on": 4000000,
"start_on": 0,
"stop_on": 0,
"rewrite": true,
"purge_queues": true,
"live_reader": true,
"live_reader": false,
"live_only_mode": false,
"abi_scan_mode": false,
"fetch_block": true,
+12 -6
View File
@@ -1,3 +1,5 @@
import {hLog} from "../helpers/common_functions";
const got = require('got');
import {connect, Connection} from 'amqplib';
@@ -5,7 +7,7 @@ const {debugLog} = require("../helpers/functions");
export async function createConnection(config): Promise<Connection> {
try {
const amqp_url = `amqp://${config.user}:${config.pass}@${config.host}/%2F${config.vhost}`;
const amqp_url = getAmpqUrl(config);
const conn: Connection = await connect(amqp_url);
debugLog("[AMQP] connection established");
return conn;
@@ -17,6 +19,11 @@ export async function createConnection(config): Promise<Connection> {
}
}
export function getAmpqUrl(config): string {
return `amqp://${config.user}:${config.pass}@${config.host}/%2F${config.vhost}`;
}
async function createChannels(connection) {
try {
const channel = await connection.createChannel();
@@ -35,17 +42,16 @@ export async function amqpConnect(onReconnect, config) {
const channels = await createChannels(connection);
if (channels) {
connection.on('error', (err) => {
console.log('[AMQP] Error!');
console.log(err);
hLog(err.message);
});
connection.on('close', () => {
console.log('[AMQP] Connection closed!');
hLog('Connection closed!');
setTimeout(async () => {
console.log('Retrying in 5 seconds...');
hLog('Retrying in 3 seconds...');
const _channels = await amqpConnect(onReconnect, config);
onReconnect(_channels);
return _channels;
}, 5000);
}, 3000);
});
return channels;
} else {
+51 -13
View File
@@ -4,9 +4,10 @@ import got from "got";
import {Client} from '@elastic/elasticsearch'
import {HyperionConnections} from "../interfaces/hyperionConnections";
import {HyperionConfig} from "../interfaces/hyperionConfig";
import {amqpConnect, checkQueueSize} from "./amqp";
import {amqpConnect, checkQueueSize, getAmpqUrl} from "./amqp";
import {StateHistorySocket} from "./state-history";
import fetch from 'cross-fetch'
import {exec} from "child_process";
export class ConnectionManager {
@@ -14,11 +15,19 @@ export class ConnectionManager {
conn: HyperionConnections;
chain: string;
last_commit_hash: string;
current_version: string;
private esIngestClients: Client[];
private esIngestClient: Client;
constructor(private cm: ConfigurationModule) {
this.config = cm.config;
this.conn = cm.connections;
this.chain = this.config.settings.chain;
this.esIngestClients = [];
this.prepareESClient();
this.prepareIngestClients();
}
get nodeosJsonRPC() {
@@ -29,7 +38,14 @@ export class ConnectionManager {
console.log(`Purging all ${this.chain} queues!`);
const apiUrl = `http://${this.conn.amqp.user}:${this.conn.amqp.pass}@${this.conn.amqp.api}`;
const getAllQueuesFromVHost = apiUrl + `/api/queues/%2F${this.conn.amqp.vhost}`;
const result = JSON.parse((await got(getAllQueuesFromVHost)).body);
let result;
try {
result = JSON.parse((await got(getAllQueuesFromVHost)).body);
} catch (e) {
console.log(e.message);
console.error('failed to connect to rabbitmq http api');
process.exit(1);
}
for (const queue of result) {
if (queue.name.startsWith(this.chain + ":")) {
const msg_count = parseInt(queue.messages);
@@ -38,7 +54,8 @@ export class ConnectionManager {
await got.delete(apiUrl + `/api/queues/%2F${this.conn.amqp.vhost}/${queue.name}/contents`);
console.log(`${queue.messages} messages deleted on queue ${queue.name}`);
} catch (e) {
console.log(e);
console.log(e.message);
console.error('failed to connect to rabbitmq http api');
process.exit(1);
}
}
@@ -46,7 +63,7 @@ export class ConnectionManager {
}
}
getESClient() {
prepareESClient() {
let es_url;
const _es = this.conn.elasticsearch;
if (_es.user !== '') {
@@ -54,18 +71,17 @@ export class ConnectionManager {
} else {
es_url = `http://${_es.host}`
}
return new Client({node: es_url});
this.esIngestClient = new Client({node: es_url});
}
get elasticsearchClient() {
return this.getESClient();
return this.esIngestClient;
}
get ingestClients() {
prepareIngestClients() {
const _es = this.conn.elasticsearch;
if (_es.ingest_nodes) {
if (_es.ingest_nodes.length > 0) {
const clients = [];
for (const node of _es.ingest_nodes) {
let es_url;
if (_es.user !== '') {
@@ -73,14 +89,17 @@ export class ConnectionManager {
} else {
es_url = `http://${node}`
}
clients.push(new Client({node: es_url, pingTimeout: 100}));
this.esIngestClients.push(new Client({node: es_url, pingTimeout: 100}));
}
return clients;
} else {
return [this.getESClient()];
}
}
}
get ingestClients() {
if (this.esIngestClients.length > 0) {
return this.esIngestClients;
} else {
return [this.getESClient()];
return [this.esIngestClient];
}
}
@@ -95,4 +114,23 @@ export class ConnectionManager {
get shipClient() {
return new StateHistorySocket(this.conn.chains[this.config.settings.chain]['ship']);
}
get ampqUrl() {
return getAmpqUrl(this.conn.amqp);
}
calculateServerHash() {
exec('git rev-parse HEAD', (err, stdout) => {
console.log('Last commit hash on this branch is:', stdout);
this.last_commit_hash = stdout.trim();
});
}
getServerHash() {
return this.last_commit_hash;
}
getHyperionVersion() {
this.current_version = require('../package.json').version
}
}
+6 -4
View File
@@ -1,3 +1,5 @@
import {hLog} from "../helpers/common_functions";
const WebSocket = require('ws');
export class StateHistorySocket {
@@ -9,23 +11,23 @@ export class StateHistorySocket {
}
connect(onMessage, onDisconnect, onError, onConnected) {
console.log(`[SHIP] Connecting to ${this.shipUrl}...`);
hLog(`Connecting to ${this.shipUrl}...`);
this.ws = new WebSocket(this.shipUrl, null, {
perMessageDeflate: false
});
this.ws.on('open', () => {
console.log('[SHIP] Websocket connected!');
hLog('Websocket connected!');
if (onConnected) {
onConnected();
}
});
this.ws.on('message', onMessage);
this.ws.on('close', () => {
console.log('[SHIP] Websocket disconnected!');
hLog('Websocket disconnected!');
onDisconnect();
});
this.ws.on('error', (err) => {
console.log(`${this.shipUrl} :: ${err.message}`);
hLog(`${this.shipUrl} :: ${err.message}`);
});
}
+36
View File
@@ -0,0 +1,36 @@
function addIndexer(chainName) {
return {
script: "./launcher.js",
name: chainName + "-indexer",
namespace: chainName,
interpreter: 'node',
interpreter_args: ["--max-old-space-size=4096", "--trace-deprecation"],
autorestart: false,
kill_timeout: 3600,
time: true, // include timestamps in pm2 logs
env: {
CONFIG_JSON: 'chains/' + chainName + '.config.json',
TRACE_LOGS: 'false'
}
};
}
function addApiServer(chainName, threads) {
return {
script: "./api/server.js",
name: chainName + "-api",
namespace: chainName,
node_args: ["--trace-deprecation"],
exec_mode: 'cluster',
merge_logs: true,
instances: threads,
autorestart: true,
exp_backoff_restart_delay: 100,
watch: ["api"],
env: {
CONFIG_JSON: 'chains/' + chainName + '.config.json'
}
}
}
module.exports = {addIndexer, addApiServer};
+21 -1
View File
@@ -1,5 +1,5 @@
import {ApiResponse, Client} from "@elastic/elasticsearch";
import {Serialize} from "../eosjs-native";
import {Serialize} from "../addons/eosjs-native";
function getLastResult(results: ApiResponse) {
if (results.body.hits?.hits?.length > 0) {
@@ -164,3 +164,23 @@ export function checkFilter(filter, _source) {
return false;
}
}
export function hLog(input: any, ...extra: any[]) {
let role;
if (process.env.worker_role) {
const id = parseInt(process.env.worker_id);
role = `[${(id < 10 ? '0' : '') + id.toString()}_${process.env.worker_role}]`;
} else {
role = '[00_master]';
}
if (process.env.TRACE_LOGS === 'true') {
const e = new Error();
const frame = e.stack.split("\n")[2];
const where = frame.split(" ")[6].split(/[:()]/);
const arr = where[1].split("/");
const fileName = arr[arr.length - 1];
const lineNumber = where[2];
role += ` ${fileName}:${lineNumber}`;
}
console.log(role, input, ...extra);
}

Some files were not shown because too many files have changed in this diff Show More