architecture updates
This commit is contained in:
@@ -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 () {
|
||||
/**
|
||||
Vendored
+1
-1
@@ -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'), {
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
};
|
||||
@@ -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']
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
@@ -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
Reference in New Issue
Block a user