312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
import {hLog, waitUntilReady} from "../helpers/common_functions";
|
|
import {ConfigurationModule} from "../modules/config";
|
|
import {ConnectionManager} from "../connections/manager.class";
|
|
import {HyperionConfig} from "../interfaces/hyperionConfig";
|
|
import IORedis from 'ioredis';
|
|
import fastify from 'fastify'
|
|
import {registerPlugins} from "./plugins";
|
|
import {AddressInfo} from "net";
|
|
import {registerRoutes} from "./routes";
|
|
import {generateOpenApiConfig} from "./config/open_api";
|
|
import {createWriteStream, existsSync, mkdirSync, readFileSync} from "fs";
|
|
import {SocketManager} from "./socketManager";
|
|
import {HyperionModuleLoader} from "../modules/loader";
|
|
import {extendedActions} from "./routes/v2-history/get_actions/definitions";
|
|
import {io, Socket} from "socket.io-client";
|
|
import {CacheManager} from "./helpers/cacheManager";
|
|
|
|
import {bootstrap} from 'global-agent';
|
|
|
|
class HyperionApiServer {
|
|
|
|
private hub: Socket;
|
|
private readonly fastify;
|
|
private readonly chain: string;
|
|
private readonly conf: HyperionConfig;
|
|
private readonly manager: ConnectionManager;
|
|
private readonly cacheManager: CacheManager;
|
|
|
|
socketManager: SocketManager;
|
|
mLoader: HyperionModuleLoader;
|
|
private pluginParams: any;
|
|
|
|
constructor() {
|
|
|
|
const package_json = JSON.parse(readFileSync('./package.json').toString());
|
|
hLog(`--------- Hyperion API ${package_json.version} ---------`);
|
|
|
|
const cm = new ConfigurationModule();
|
|
this.conf = cm.config;
|
|
|
|
if (this.conf.settings.use_global_agent) {
|
|
bootstrap();
|
|
}
|
|
|
|
this.chain = this.conf.settings.chain;
|
|
process.title = `hyp-${this.chain}-api`;
|
|
this.manager = new ConnectionManager(cm);
|
|
this.manager.calculateServerHash();
|
|
this.mLoader = new HyperionModuleLoader(cm);
|
|
this.cacheManager = new CacheManager(this.conf);
|
|
|
|
if (!existsSync('./logs/' + this.chain)) {
|
|
mkdirSync('./logs/' + this.chain, {recursive: true});
|
|
}
|
|
|
|
const logStream = createWriteStream('./logs/' + this.chain + '/api.access.log');
|
|
|
|
const loggerOpts = {
|
|
stream: logStream,
|
|
redact: ['req.headers.authorization'],
|
|
level: 'info',
|
|
prettyPrint: true,
|
|
serializers: {
|
|
res: (reply) => {
|
|
return {
|
|
statusCode: reply.statusCode
|
|
};
|
|
},
|
|
req: (request) => {
|
|
return {
|
|
method: request.method,
|
|
url: request.url,
|
|
ip: request.headers['x-real-ip']
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this.fastify = fastify({
|
|
ignoreTrailingSlash: false,
|
|
trustProxy: true,
|
|
pluginTimeout: 5000,
|
|
logger: this.conf.api.access_log ? loggerOpts : false
|
|
});
|
|
|
|
this.fastify.decorate('cacheManager', this.cacheManager);
|
|
|
|
this.fastify.decorate('manager', this.manager);
|
|
|
|
// import get_actions query params from custom modules
|
|
const extendedActionsSet: Set<string> = new Set([...extendedActions]);
|
|
for (const qPrefix of this.mLoader.extendedActions) {
|
|
extendedActionsSet.add(qPrefix);
|
|
}
|
|
this.fastify.decorate('allowedActionQueryParamSet', extendedActionsSet);
|
|
|
|
// define chain api url for /v1/chain/ redirects
|
|
let chainApiUrl: string = this.conf.api.chain_api;
|
|
if (chainApiUrl === null || chainApiUrl === "") {
|
|
chainApiUrl = this.manager.conn.chains[this.chain].http;
|
|
}
|
|
this.fastify.decorate('chain_api', chainApiUrl);
|
|
|
|
// define optional push api url for /v1/chain/push_transaction
|
|
if (this.conf.api.push_api) {
|
|
this.fastify.decorate('push_api', this.conf.api.push_api);
|
|
}
|
|
|
|
hLog(`Chain API URL: "${this.fastify.chain_api}" | Push API URL: "${this.fastify.push_api}"`);
|
|
|
|
const ioRedisClient = new IORedis(this.manager.conn.redis);
|
|
|
|
const pluginParams = {
|
|
fastify_elasticsearch: {
|
|
client: this.manager.elasticsearchClient
|
|
},
|
|
fastify_redis: this.manager.conn.redis,
|
|
fastify_eosjs: this.manager,
|
|
chain_id: '',
|
|
} as any;
|
|
|
|
if (!this.conf.api.disable_rate_limit) {
|
|
let rateLimiterWhitelist = ['127.0.0.1'];
|
|
if (this.conf.api.rate_limit_allow && this.conf.api.rate_limit_allow.length > 0) {
|
|
const tempSet = new Set<string>(['127.0.0.1', ...this.conf.api.rate_limit_allow]);
|
|
rateLimiterWhitelist = [...tempSet];
|
|
}
|
|
let rateLimiterRPM = 1000;
|
|
if (this.conf.api.rate_limit_rpm) {
|
|
rateLimiterRPM = this.conf.api.rate_limit_rpm;
|
|
}
|
|
pluginParams.fastify_rate_limit = {
|
|
max: rateLimiterRPM,
|
|
allowList: rateLimiterWhitelist,
|
|
timeWindow: '1 minute',
|
|
redis: ioRedisClient
|
|
}
|
|
}
|
|
|
|
if (this.conf.features.streaming.enable) {
|
|
this.activateStreaming();
|
|
}
|
|
|
|
const docsConfig = generateOpenApiConfig(this.manager.config);
|
|
if (docsConfig) {
|
|
pluginParams.fastify_swagger = docsConfig;
|
|
}
|
|
|
|
this.pluginParams = pluginParams;
|
|
}
|
|
|
|
activateStreaming() {
|
|
console.log('Importing stream module...');
|
|
import('./socketManager').then((mod) => {
|
|
const connOpts = this.manager.conn.chains[this.chain];
|
|
let _port = 57200;
|
|
if (connOpts.WS_ROUTER_PORT) {
|
|
_port = connOpts.WS_ROUTER_PORT;
|
|
}
|
|
let _host = "127.0.0.1";
|
|
if (connOpts.WS_ROUTER_HOST) {
|
|
_host = connOpts.WS_ROUTER_HOST;
|
|
}
|
|
if (_host === "0.0.0.0") {
|
|
hLog(`[ERROR] WS Router Host is set to 0.0.0.0, please use a fixed IP address instead. Can't start streaming.`);
|
|
return;
|
|
}
|
|
this.socketManager = new mod.SocketManager(
|
|
this.fastify,
|
|
`http://${_host}:${_port}`,
|
|
this.manager.conn.redis
|
|
);
|
|
this.socketManager.startRelay();
|
|
});
|
|
}
|
|
|
|
private addGenericTypeParsing() {
|
|
this.fastify.addContentTypeParser('*', (request, payload, done) => {
|
|
let data = '';
|
|
payload.on('data', chunk => {
|
|
data += chunk;
|
|
});
|
|
payload.on('end', () => {
|
|
done(null, data);
|
|
});
|
|
payload.on('error', (err) => {
|
|
console.log('---- Content Parsing Error -----');
|
|
console.log(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
async init() {
|
|
|
|
const rpc = this.manager.nodeosJsonRPC;
|
|
await waitUntilReady(async () => {
|
|
try {
|
|
const chain_data = await rpc.get_info();
|
|
if (chain_data && chain_data.chain_id) {
|
|
this.pluginParams.chain_id = chain_data.chain_id;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
hLog(e.message);
|
|
return false;
|
|
}
|
|
}, 20, 5000, () => {
|
|
hLog('Failed to validate chain api!');
|
|
process.exit(1);
|
|
});
|
|
hLog('Chain API validated!');
|
|
|
|
// Wait for Elasticsearch availability
|
|
await waitUntilReady(async () => {
|
|
try {
|
|
const esInfo = await this.manager.elasticsearchClient.info();
|
|
hLog(`Elasticsearch: ${esInfo.body.version.number} | Lucene: ${esInfo.body.version.lucene_version}`);
|
|
return true;
|
|
} catch (e) {
|
|
console.log(e.message);
|
|
return false;
|
|
}
|
|
}, 10, 5000, () => {
|
|
hLog('Failed to check elasticsearch version!');
|
|
process.exit();
|
|
});
|
|
|
|
hLog('Elasticsearch validated!');
|
|
hLog('Registering plugins...');
|
|
|
|
registerPlugins(this.fastify, this.pluginParams);
|
|
this.addGenericTypeParsing();
|
|
|
|
await this.mLoader.init();
|
|
|
|
// add custom plugin routes
|
|
for (const plugin of this.mLoader.plugins) {
|
|
if (plugin.hasApiRoutes) {
|
|
hLog(`Adding routes for plugin: ${plugin.internalPluginName}`);
|
|
plugin.addRoutes(this.fastify);
|
|
plugin.chainName = this.chain;
|
|
}
|
|
}
|
|
|
|
registerRoutes(this.fastify);
|
|
|
|
// register documentation when ready
|
|
this.fastify.ready().then(async () => {
|
|
await this.fastify.swagger();
|
|
}, (err) => {
|
|
hLog('an error happened', err)
|
|
});
|
|
|
|
try {
|
|
await this.fastify.listen({
|
|
host: this.conf.api.server_addr,
|
|
port: this.conf.api.server_port
|
|
});
|
|
hLog(`${this.chain} hyperion api ready and listening on port ${(this.fastify.server.address() as AddressInfo).port}`);
|
|
this.startHyperionHub();
|
|
} catch (err) {
|
|
hLog(err);
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
startHyperionHub() {
|
|
if (this.conf.hub) {
|
|
const url = this.conf.hub.inform_url;
|
|
hLog(`Connecting API to Hyperion Hub`);
|
|
this.hub = io(url, {
|
|
query: {
|
|
key: this.conf.hub.publisher_key,
|
|
client_mode: 'false'
|
|
}
|
|
});
|
|
this.hub.on('connect', () => {
|
|
hLog(`Hyperion Hub connected!`);
|
|
this.emitHubApiUpdate();
|
|
});
|
|
}
|
|
}
|
|
|
|
private emitHubApiUpdate() {
|
|
this.hub.emit('hyp_info', {
|
|
type: 'api',
|
|
production: this.conf.hub.production,
|
|
location: this.conf.hub.location,
|
|
chainId: this.manager.conn.chains[this.chain].chain_id,
|
|
providerName: this.conf.api.provider_name,
|
|
explorerEnabled: this.conf.plugins.explorer?.enabled,
|
|
providerUrl: this.conf.api.provider_url,
|
|
providerLogo: this.conf.api.provider_logo,
|
|
chainLogo: this.conf.api.chain_logo_url,
|
|
chainCodename: this.chain,
|
|
chainName: this.conf.api.chain_name,
|
|
endpoint: this.conf.api.server_name,
|
|
features: this.conf.features,
|
|
filters: {
|
|
blacklists: this.conf.blacklists,
|
|
whitelists: this.conf.whitelists
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const server = new HyperionApiServer();
|
|
|
|
server.init().catch(hLog);
|