Compare commits

..

6 Commits

Author SHA1 Message Date
João Barbosa 0def728dec stream client example renamed 2020-06-10 11:44:06 -03:00
Felipe Faria c6d176dbbb stream client sample 2020-05-08 17:07:33 -03:00
Felipe Faria 9f7da5b676 rabbitmq changes 2020-05-07 03:00:39 -03:00
Felipe Faria 5e7c89c0c6 rabbitmq test
install rabbitmq via docker
2020-05-07 02:58:54 -03:00
Felipe Faria c012c01e96 warn about elastic_pass.txt 2020-04-28 12:59:32 -03:00
Felipe Faria daa141de33 script updates
New name, better text outputs, elasticsearch and kibana security config
2020-04-16 00:13:51 -03:00
310 changed files with 15597 additions and 18265 deletions
+34 -24
View File
@@ -11,8 +11,33 @@ pids
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
@@ -26,11 +51,18 @@ node_modules/
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
.idea
*.pem
ecosystem.config.js
package-lock.json
connections.json
connections.json2
@@ -47,6 +79,8 @@ api/**/*.js
api/**/*.js.map
api/tests
hyperion-explorer/
hyperion-launcher.js
hyperion-launcher.js.map
@@ -73,27 +107,3 @@ modules/**/*.js.map
addons/**/*.js
addons/**/*.js.map
scripts/hpm.js
scripts/hpm.js.map
plugins/.state.json
plugins/repos
plugins/*.js
.gitmodules
docker/redis/data
docker/elasticsearch/data
docker/rabbitmq/data
docker/eosio/data
configuration_backups
scripts/hyp-config.js
scripts/hyp-config.js.map
scripts/hyp-repair.js
scripts/hyp-repair.js.map
scripts/repair-cli/*.js
scripts/repair-cli/*.js.map
+195
View File
@@ -0,0 +1,195 @@
### nodeos config.ini
```
state-history-dir = "state-history"
trace-history = true
chain-state-history = true
state-history-endpoint = 127.0.0.1:8080
plugin = eosio::state_history_plugin
```
### NodeJS
```bash
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs
```
### PM2
```bash
sudo npm install pm2@latest -g
sudo pm2 startup
```
### Elasticsearch Installation
* Follow instructions on https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html (Ubuntu/Debian)
Edit `/etc/elasticsearch/elasticsearch.yml`
```
cluster.name: myCluster
bootstrap.memory_lock: true
```
Edit `/etc/elasticsearch/jvm.options`
```
# Set your heap size, avoid allocating more than 31GB, even if you have enought RAM.
# Test on your specific machine by changing -Xmx32g in the following command:
# java -Xmx32g -XX:+UseCompressedOops -XX:+PrintFlagsFinal Oops | grep Oops
-Xms16g
-Xmx16g
```
Disable swap on /etc/fstab
Allow memlock:
run `sudo systemctl edit elasticsearch` and add the following lines
```
[Service]
LimitMEMLOCK=infinity
```
Start elasticsearch and check the logs (verify if the memory lock was successful)
```bash
sudo service elasticsearch start
sudo less /var/log/elasticsearch/myCluster.log
sudo systemctl enable elasticsearch
```
Test the REST API `curl http://localhost:9200`
```
{
"name" : "ip-172-31-5-121",
"cluster_name" : "hyperion",
"cluster_uuid" : "....",
"version" : {
"number" : "7.1.0",
"build_flavor" : "default",
"build_type" : "deb",
"build_hash" : "606a173",
"build_date" : "2019-05-16T00:43:15.323135Z",
"build_snapshot" : false,
"lucene_version" : "8.0.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
```
### Kibana Installation
```bash
wget https://artifacts.elastic.co/downloads/kibana/kibana-7.4.0-amd64.deb
sudo apt install ./kibana-7.4.0-amd64.deb
sudo systemctl enable kibana
```
Open and test Kibana on `http://localhost:5601`
### RabbitMQ Installation
`https://www.rabbitmq.com/install-debian.html#installation-methods`
```bash
sudo apt install rabbitmq-server
sudo rabbitmq-plugins enable rabbitmq_management
sudo rabbitmqctl add_vhost /hyperion
sudo rabbitmqctl add_user my_user my_password
sudo rabbitmqctl set_user_tags my_user administrator
sudo rabbitmqctl set_permissions -p /hyperion my_user ".*" ".*" ".*"
```
Check access to the WebUI `http://localhost:15672`
### Redis Installation
```bash
sudo apt install redis-server
```
Edit `/etc/redis/redis.conf` and change supervised to `systemd`
```bash
sudo systemctl restart redis.service
```
### Hyperion Indexer
```bash
git clone https://github.com/eosrio/Hyperion-History-API.git
cd Hyperion-History-API
npm install
cp example-ecosystem.config.js ecosystem.config.js
```
Edit `ecosystem.config.js`
### Setup Indices and Aliases
Load templates first by starting the Hyperion Indexer in preview mode `PREVIEW: 'true'`
Indices and aliases are created automatically using the `CREATE_INDICES` option (set it to your version suffix e.g, v1, v2, v3)
If you want to create them manually, use the commands bellow on the kibana dev console
```
PUT mainnet-action-v1-000001
PUT mainnet-abi-v1-000001
PUT mainnet-block-v1-000001
POST _aliases
{
"actions": [
{
"add": {
"index": "mainnet-abi-v1-000001",
"alias": "mainnet-abi"
}
},
{
"add": {
"index": "mainnet-action-v1-000001",
"alias": "mainnet-action"
}
},
{
"add": {
"index": "mainnet-block-v1-000001",
"alias": "mainnet-block"
}
}
]
}
```
Before indexing actions into elasticsearch its required to do a ABI scan pass
Start with
```
ABI_CACHE_MODE: 'true',
FETCH_BLOCK: 'false',
FETCH_TRACES: 'false',
INDEX_DELTAS: 'false',
INDEX_ALL_DELTAS: 'false',
```
When indexing is finished, change the settings back and restart the indexer. In case you do not have much contract updates, you do not need to run a full pass.
Tune your configs to your specific hardware using the following settings:
```
BATCH_SIZE
READERS
DESERIALIZERS
DS_MULT
ES_IDX_QUEUES
ES_AD_IDX_QUEUES
READ_PREFETCH
BLOCK_PREFETCH
INDEX_PREFETCH
```
+156 -78
View File
@@ -1,99 +1,177 @@
<!--suppress HtmlUnknownTarget, HtmlDeprecatedAttribute -->
<br></br>
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://eosrio.io/hyperion-white.png">
<img alt="Hyperion Logo"
src="https://eosrio.io/hyperion.png">
</picture>
</p>
# Hyperion History API - Private
Scalable Full History API Solution for EOSIO based blockchains
<h4 align="center">
Scalable Full History API Solution for
<a href="https://antelope.io">
Antelope
</a>
(former EOSIO) based blockchains
</h4>
Made with ♥ by [EOS Rio](https://eosrio.io/)
<br>
### Introducing an storage-optimized action format for EOSIO
Made with ♥ by [Rio Blocks / EOS Rio](https://rioblocks.io/?lang=en)
The original *history_plugin* bundled with eosio, that provided the v1 api, stored inline action traces nested inside their root actions. This led to an excessive amount of data being stored and also transferred whenever a user requested the action history for a given account. Also inline actions are used as a "event" mechanism to notify parties on a transaction. Based on those Hyperion implements some changes
### [Official documentation](https://hyperion.docs.eosrio.io)
1. actions are stored in a flattened format
2. a parent field is added to the inline actions to point to the parent global sequence
3. if the inline action data is identical to the parent it is considered a notification and thus removed from the database
4. no blocks or transaction data is stored, all information can be reconstructed from actions
### How to use
With those changes the API format focus on delivering faster search times, lower bandwidth overhead and easier usability for UI/UX developers.
- [For Providers](https://hyperion.docs.eosrio.io/manual_installation/)
#### Action Data Structure (work in progress)
- [For Developers](https://hyperion.docs.eosrio.io/howtouse/)
- `@timestamp` - block time
- `global_sequence` - unique action global_sequence, used as index id
- `parent` - points to the parent action (in the case of an inline action) or equal to 0 if root level
- `block_num` - block number where the action was processed
- `trx_id` - transaction id
- `producer` - block producer
- `act`
- `account` - contract account
- `name` - contract method name
- `authorization` - array of signers
- `actor` - signing actor
- `permission` - signing permission
- `data` - action data input object
- `account_ram_deltas` - array of ram deltas and payers
- `account`
- `delta`
- `notified` - array of accounts that were notified (via inline action events)
### Official plugins:
## Dependencies
- [Hyperion Lightweight Explorer](https://github.com/eosrio/hyperion-explorer-plugin)
This setup has only been tested with Ubuntu 18.04, but should work with other OS versions too
### 1. Overview
- [Elasticsearch 7.5.X](https://www.elastic.co/downloads/elasticsearch)
- [RabbitMQ](https://www.rabbitmq.com/install-debian.html)
- [Redis](https://redis.io/topics/quickstart)
- [Node.js v13](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions)
- [PM2](http://pm2.keymetrics.io/docs/usage/quick-start/)
- Nodeos w/ state_history_plugin and chain_api_plugin
> The indexer requires redis, pm2 and node.js to be on the same machine. Other dependencies might be installed on other machines, preferably over a very high speed and low latency network. Indexing speed will vary greatly depending on this configuration.
## Setup Instructions
Hyperion is a full history solution for indexing, storing and retrieving Antelope blockchain's historical data.
Antelope protocol is highly scalable reaching up to tens of thousands of transactions per second demanding high
performance indexing and optimized storage and querying solutions. Hyperion is developed to tackle those challenges
providing open source software to be operated by block producers, infrastructure providers and dApp developers.
Install, configure and test all dependencies above before continuing
Focused on delivering faster search times, lower bandwidth overhead and easier usability for UI/UX developers,
Hyperion implements an improved data structure. Actions are stored in a flattened format, transaction ids are added to
all inline actions, allowing to group by transaction without storing a full transaction index. Besides that if the inline
action data is identical to the parent, it is considered a notification and thus removed from the database.
No full block or transaction data is stored, all information can be reconstructed from actions and deltas, only a block
header index is stored.
Read the step-by-step instructions here - [INSTALL.md](https://github.com/eosrio/Hyperion-History-API/blob/master/INSTALL.md)
### 2. Architecture
#### 1. Clone & Install packages
```bash
git clone https://github.com/eosrio/Hyperion-History-API.git
cd Hyperion-History-API
npm install
```
The following components are required in order to have a fully functional Hyperion API deployment.
* For small use cases, it is absolutely fine to run all components on a single machine.
* For larger chains and production environments, we recommend setting them up into different servers under a high-speed local network.
#### 2. Edit configs
```
cp example-ecosystem.config.js ecosystem.config.js
nano ecosystem.config.js
#### 2.1 Elasticsearch Cluster
# Enter connection details here (chain name must match on the ecosystem file)
cp example-connections.json connections.json
nano connections.json
```
The ES cluster is responsible for storing all indexed data.
Direct access to the Hyperion API and Indexer must be provided. We recommend nodes in the
cluster to have at least 32 GB of RAM and 8 cpu cores. SSD/NVME drives are recommended for
maximum indexing throughput, although HDDs can be used for cold storage nodes.
For production environments, a multi-node cluster is highly recommended.
connections.json Reference
```
{
"amqp": {
"host": "127.0.0.1:5672", // RabbitMQ Server
"api": "127.0.0.1:15672", // RabbitMQ API Endpoint
"user": "username",
"pass": "password",
"vhost": "hyperion" // RabbitMQ vhost
},
"elasticsearch": {
"host": "127.0.0.1:9200", // Elasticsearch HTTP API Endpoint
"ingest_nodes": ["127.0.0.1:9200"], // List of ES ingest nodes
"user": "elastic",
"pass": "password"
},
"redis": {
"host": "127.0.0.1",
"port": "6379"
},
"chains": {
"eos": { // Chain name (must match on the ecosystem file)
"http": "http://127.0.0.1:8888", // Nodeos Chain API Endpoint
"ship": "ws://127.0.0.1:8080" // Nodeos State History Endpoint
},
"other_chain": {...}
}
}
```
#### 2.2 Hyperion Indexer
ecosystem.config.js Reference
```
CHAIN: 'eos', // chain prefix for indexing
ABI_CACHE_MODE: 'false', // only cache historical ABIs to redis
DEBUG: 'false', // debug mode - display extra logs for debugging
LIVE_READER: 'true', // enable continuous reading after reaching the head block
FETCH_DELTAS: 'false', // read table deltas
CREATE_INDICES: 'v1', // index suffix to be created, set to false to use existing aliases
START_ON: 0, // start indexing on block (0=disable)
STOP_ON: 0, // stop indexing on block (0=disable)
AUTO_STOP: 0, // automatically stop Indexer after X seconds if no more blocks are being processed (0=disable)
REWRITE: 'false', // force rewrite the target replay range
PURGE_QUEUES: 'false', // clear rabbitmq queues before starting the indexer
BATCH_SIZE: 2000, // parallel reader batch size in blocks
QUEUE_THRESH: 8000, // queue size limit on rabbitmq
LIVE_ONLY: 'false', // only reads realtime data serially
FETCH_BLOCK: 'true', // Request full blocks from the state history plugin
FETCH_TRACES: 'true', // Request traces from the state history plugin
PREVIEW: 'false', // preview mode - prints worker map and exit
DISABLE_READING: 'false', // completely disable block reading, for lagged queue processing
READERS: 3, // parallel state history readers
DESERIALIZERS: 4, // deserialization queues
DS_MULT: 4, // deserialization threads per queue
ES_IDX_QUEUES: 4, // elastic indexers per queue
ES_AD_IDX_QUEUES: 2, // multiplier for action indexing queues
READ_PREFETCH: 50, // Stage 1 prefecth size
BLOCK_PREFETCH: 5, // Stage 2 prefecth size
INDEX_PREFETCH: 500, // Stage 3 prefetch size
ENABLE_INDEXING: 'true', // enable elasticsearch indexing
INDEX_DELTAS: 'true', // index common table deltas (see delta on definitions/mappings)
INDEX_ALL_DELTAS: 'false' // index all table deltas (WARNING)
```
#### 3. Starting
`./run.sh Indexer`
or
```
pm2 start --only Indexer --update-env
pm2 logs Indexer
```
#### 4. Stopping
Stop reading and wait for queues to flush
```
pm2 trigger Indexer stop
```
Force stop
```
pm2 stop Indexer
```
#### 5. Starting the API node
`./run.sh API`
or
```
pm2 start --only API --update-env
pm2 logs API
```
## API Reference
The Indexer is a Node.js based app that process data from the state history plugin and allows it to be indexed.
The [PM2 process manager](https://pm2.keymetrics.io) is used to launch and operate the indexer. The configuration
flexibility is very extensive,
so system recommendations will depend on the use case and data load. It will require access to at least one ES node,
RabbitMQ and the state history node.
Documentation is automatically generated by Swagger/OpenAPI.
#### 2.3 Hyperion API
Example: [OpenAPI Docs](https://eos.hyperion.eosrio.io/v2/docs)
Parallelizable API server that provides the V2 and V1 (legacy history plugin) endpoints.
It is launched by PM2 and can also operate in cluster mode. It requires direct access to
at least one ES node for the queries and all other services for full healthcheck
### Roadmap
#### 2.4 RabbitMQ
Used as messaging queue and data transport between the indexer stages and for real-time data streaming
#### 2.5 Redis
Used for transient data storage across processes and for the preemptive transaction caching used on
the `v2/history/get_transaction` and `v2/history/check_transaction` endpoints
#### 2.6 Leap State History
[Leap / Nodeos](https://github.com/AntelopeIO/leap/tree/main/plugins/state_history_plugin) plugin used
to collect action traces and state deltas. Provides data via websocket to the indexer
#### 2.7 Hyperion Stream Client (optional)
Web and Node.js client for real-time streaming on enabled hyperion
providers. [Documentation](https://hyperion.docs.eosrio.io/stream_client/)
#### 2.8 Hyperion Plugins (optional)
Hyperion includes a flexible plugin architecture to allow further customization.
Plugins are managed by the `hpm` (hyperion plugin manager) command line tool.
- Table deltas storage & queries (in progress)
- Real-time streaming support (in progress)
- Plugin system (in progress)
- Control GUI
+2 -9
View File
@@ -4,7 +4,6 @@ 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`;
let description = `
<img height="64" src="https://eosrio.io/hyperion.png">
### Scalable Full History API Solution for EOSIO based blockchains
@@ -14,15 +13,9 @@ export function generateOpenApiConfig(config: HyperionConfig) {
#### Provided by [${config.api.provider_name}](${config.api.provider_url})
#### Health API: <a target="_blank" href="${health_link}">${health_link}</a>
`;
if (config.plugins) {
if (config.plugins.explorer) {
if (config.plugins.explorer.enabled) {
description += `#### Integrated Explorer: <a target="_blank" href="${explorer_link}">${explorer_link}</a>`
}
}
if(config.api.enable_explorer) {
description += `#### Integrated Explorer: <a target="_blank" href="${explorer_link}">${explorer_link}</a>`
}
return {
routePrefix: '/v2/docs',
exposeRoute: true,
-86
View File
@@ -1,86 +0,0 @@
import {HyperionConfig} from "../../interfaces/hyperionConfig";
import {createHash} from "crypto";
import {FastifyRequest} from "fastify";
export interface CacheConfig {
ttl: number;
}
export interface CachedEntry {
data: string;
exp: number;
}
export class CacheManager {
v1CacheConfigs: Map<string, CacheConfig> = new Map<string, CacheConfig>();
v1Caches: Map<string, Map<string, CachedEntry>> = new Map<string, Map<string, CachedEntry>>();
constructor(conf: HyperionConfig) {
if (conf.api.v1_chain_cache) {
conf.api.v1_chain_cache.forEach(value => {
this.v1CacheConfigs.set(value.path, {
ttl: value.ttl
});
this.v1Caches.set(value.path, new Map<string, CachedEntry>());
});
}
setInterval(() => {
try {
// remove expired entries
let removeCount = 0;
const now = Date.now();
this.v1Caches.forEach((pathCacheMap: Map<string, CachedEntry>, pathKey: string) => {
pathCacheMap.forEach((cache: CachedEntry, entryKey: string, map: Map<string, CachedEntry>) => {
if (cache.exp < now) {
map.delete(entryKey);
removeCount++;
}
});
});
if (removeCount > 0) {
console.log(`${removeCount} expired cache entries removed`);
}
} catch (e) {
console.log(e);
}
}, 5000);
}
hashPayload(input: string): string {
return createHash('sha256').update(input).digest().toString('hex');
}
setCachedData(hash: string, path: string, payload: any): void {
if (this.v1CacheConfigs.has(path) && this.v1Caches.has(path)) {
this.v1Caches.get(path).set(hash, {
data: payload,
exp: this.v1CacheConfigs.get(path).ttl + Date.now()
});
}
}
getCachedData(request: FastifyRequest): [string | null, string, string] {
const urlParts = request.url.split("?");
const pathComponents = urlParts[0].split('/');
const path = pathComponents.at(-1);
let payload = '';
if (request.method === 'POST') {
payload = JSON.stringify(request.body);
} else if (request.method === 'GET') {
payload = request.url;
}
const hashedString = this.hashPayload(payload);
if (this.v1Caches.has(path)) {
const entry = this.v1Caches.get(path).get(hashedString);
if (entry && entry.exp > Date.now()) {
return [entry.data, hashedString, path];
} else {
return [null, hashedString, path];
}
} else {
return [null, hashedString, path];
}
}
}
+21 -674
View File
@@ -1,370 +1,13 @@
import {createHash} from "crypto";
import * as _ from "lodash";
import {FastifyInstance, FastifyReply, FastifyRequest, FastifySchema, HTTPMethods} from "fastify";
import got from "got";
import {checkDeltaFilter, checkFilter, hLog} from "../../helpers/common_functions";
import {Socket} from "socket.io";
const deltaQueryFields = ['code', 'table', 'scope', 'payer'];
export async function streamPastDeltas(fastify: FastifyInstance, socket, data) {
const search_body = {
query: {bool: {must: []}},
sort: {block_num: 'asc'},
};
await addBlockRangeOpts(data, search_body, fastify);
deltaQueryFields.forEach(f => {
addTermMatch(data, search_body, f);
});
const onDemandDeltaFilters = [];
if (data.filters.length > 0) {
data.filters.forEach(f => {
if (f.field && f.value) {
if (f.field.startsWith('@') && !f.field.startsWith('data')) {
const _q = {};
_q[f.field] = f.value;
search_body.query.bool.must.push({'term': _q});
} else {
onDemandDeltaFilters.push(f);
}
}
});
}
const responseQueue = [];
let counter = 0;
let total = 0;
let totalFiltered = 0;
let longScroll = false;
// console.log(JSON.stringify(search_body, null, 2));
const init_response = await fastify.elastic.search({
index: fastify.manager.chain + '-delta-*',
scroll: '30s',
size: fastify.manager.config.api.stream_scroll_batch || 500,
body: search_body,
});
const scrollLimit = fastify.manager.config.api.stream_scroll_limit;
if (scrollLimit && scrollLimit !== -1 && init_response.body.hits.total.value > scrollLimit) {
const errorMsg = `Requested ${init_response.body.hits.total.value} deltas, limit is ${scrollLimit}.`;
socket.emit('message', {
reqUUID: socket.data.reqUUID,
type: 'delta_trace', mode: 'history', messages: [],
error: errorMsg
});
return {status: false, error: errorMsg};
}
if (init_response.body.hits.total.value > 10000) {
total = init_response.body.hits.total.value;
longScroll = true;
hLog(`Attention! Long scroll (deltas) is running!`);
}
// emit first block
if (init_response.body.hits.hits.length > 0) {
emitTraceInit(socket, init_response.body.hits.hits[0]._source.block_num, init_response.body.hits.total.value);
}
responseQueue.push(init_response);
let lastTransmittedBlock = 0;
let pendingScrollId = '';
while (responseQueue.length) {
let filterCount = 0;
const {body} = responseQueue.shift();
pendingScrollId = body['_scroll_id'];
const enqueuedMessages = [];
counter += body['hits']['hits'].length;
for (const doc of body['hits']['hits']) {
let allow = false;
if (onDemandDeltaFilters.length > 0) {
allow = onDemandDeltaFilters.every(filter => {
return checkDeltaFilter(filter, doc._source);
});
} else {
allow = true;
}
if (allow) {
enqueuedMessages.push(doc._source);
} else {
filterCount++;
}
// set last block
if (doc._source.block_num > lastTransmittedBlock) {
lastTransmittedBlock = doc._source.block_num;
}
}
totalFiltered += filterCount;
if (socket.connected) {
if (enqueuedMessages.length > 0) {
socket.emit('message', {
reqUUID: socket.data.reqUUID,
type: 'delta_trace',
mode: 'history',
messages: enqueuedMessages,
filtered: filterCount
});
}
} else {
hLog('LOST CLIENT');
break;
}
if (longScroll) {
hLog(`Progress: ${counter + totalFiltered}/${total}`);
}
if (body['hits'].total.value === counter) {
hLog(`${counter} past deltas streamed to ${socket.id} (${totalFiltered} filtered)`);
break;
}
const next_response = await fastify.elastic.scroll({
body: {scroll_id: body['_scroll_id'], scroll: '30s'}
});
responseQueue.push(next_response);
}
// destroy scroll context
await fastify.elastic.clearScroll({scroll_id: pendingScrollId});
return {status: true, lastTransmittedBlock};
}
export function emitTraceInit(socket: Socket, firstBlock: number, totalResults: number) {
socket.emit('message', {
reqUUID: socket.data.reqUUID,
type: 'trace_init',
mode: 'history',
first_block: firstBlock,
results: totalResults
});
}
export async function streamPastActions(fastify: FastifyInstance, socket, data) {
const search_body = {query: {bool: {must: []}}, sort: {global_sequence: 'asc'}};
await addBlockRangeOpts(data, search_body, fastify);
if (data.account !== '') {
search_body.query.bool.must.push({
bool: {
should: [
{term: {'notified': data.account}},
{term: {'act.authorization.actor': data.account}},
],
},
});
}
if (data.contract !== '*' && data.contract !== '') {
search_body.query.bool.must.push({'term': {'act.account': data.contract}});
}
if (data.action !== '*' && data.action !== '') {
search_body.query.bool.must.push({'term': {'act.name': data.action}});
}
const onDemandFilters = [];
if (data.filters.length > 0) {
data.filters.forEach(f => {
if (f.field && f.value) {
if (f.field.startsWith('@') && !f.field.startsWith('act.data')) {
const _q = {};
_q[f.field] = f.value;
search_body.query.bool.must.push({'term': _q});
} else {
onDemandFilters.push(f);
}
}
});
}
const responseQueue = [];
let counter = 0;
let total = 0;
let totalFiltered = 0;
let longScroll = false;
const init_response = await fastify.elastic.search({
index: fastify.manager.chain + '-action-*',
scroll: '30s',
size: fastify.manager.config.api.stream_scroll_batch || 500,
body: search_body,
});
const scrollLimit = fastify.manager.config.api.stream_scroll_limit;
if (scrollLimit && scrollLimit !== -1 && init_response.body.hits.total.value > scrollLimit) {
const errorMsg = `Requested ${init_response.body.hits.total.value} actions, limit is ${scrollLimit}.`;
socket.emit('message', {
reqUUID: socket.data.reqUUID,
type: 'action_trace', mode: 'history', messages: [],
error: `Requested ${init_response.body.hits.total.value} actions, limit is ${scrollLimit}.`
});
return {status: false, error: errorMsg};
}
if (init_response.body.hits.total.value > 10000) {
total = init_response.body.hits.total.value;
longScroll = true;
hLog(`Attention! Long scroll (actions) is running!`);
}
// emit first block
if (init_response.body.hits.hits.length > 0) {
emitTraceInit(socket, init_response.body.hits.hits[0]._source.block_num, init_response.body.hits.total.value);
}
responseQueue.push(init_response);
let lastTransmittedBlock = 0;
let pendingScrollId = '';
while (responseQueue.length) {
let filterCount = 0;
const {body} = responseQueue.shift();
pendingScrollId = body['_scroll_id'];
const enqueuedMessages = [];
counter += body['hits']['hits'].length;
for (const doc of body['hits']['hits']) {
let allow = false;
if (onDemandFilters.length > 0) {
allow = onDemandFilters.every(filter => {
return checkFilter(filter, doc._source);
});
} else {
allow = true;
}
if (allow) {
enqueuedMessages.push(doc._source);
} else {
filterCount++;
}
// set last block
if (doc._source.block_num > lastTransmittedBlock) {
lastTransmittedBlock = doc._source.block_num;
}
}
totalFiltered += filterCount;
if (socket.connected) {
if (enqueuedMessages.length > 0) {
socket.emit('message', {
reqUUID: socket.data.reqUUID,
type: 'action_trace',
mode: 'history',
messages: enqueuedMessages,
filtered: filterCount
});
}
} else {
hLog('LOST CLIENT');
break;
}
if (longScroll) {
hLog(`Progress: ${counter + totalFiltered}/${total}`);
}
if (body['hits'].total.value === counter) {
hLog(`${counter} past actions streamed to ${socket.id} (${totalFiltered} filtered)`);
break;
}
const next_response = await fastify.elastic.scroll({
body: {scroll_id: body['_scroll_id'], scroll: '30s'}
});
responseQueue.push(next_response);
}
// destroy scroll context
await fastify.elastic.clearScroll({scroll_id: pendingScrollId});
return {status: true, lastTransmittedBlock};
}
export function addTermMatch(data, search_body, field) {
if (data[field] && data[field] !== '*' && data[field] !== '') {
const termQuery = {};
termQuery[field] = data[field];
search_body.query.bool.must.push({'term': termQuery});
}
}
export async function addBlockRangeOpts(data, search_body, fastify: FastifyInstance) {
let timeRange;
let blockRange;
let head;
if (typeof data['start_from'] === 'string' && data['start_from'] !== '') {
if (!timeRange) {
timeRange = {"@timestamp": {}};
}
timeRange["@timestamp"]['gte'] = data['start_from'];
}
if (typeof data['read_until'] === 'string' && data['read_until'] !== '') {
if (!timeRange) {
timeRange = {"@timestamp": {}};
}
timeRange["@timestamp"]['lte'] = data['read_until'];
}
if (typeof data['start_from'] === 'number' && data['start_from'] !== 0) {
if (!blockRange) {
blockRange = {"block_num": {}};
}
if (data['start_from'] < 0) {
if (!head) {
head = (await fastify.eosjs.rpc.get_info()).head_block_num;
}
blockRange["block_num"]['gte'] = head + data['start_from'];
} else {
blockRange["block_num"]['gte'] = data['start_from'];
}
}
if (typeof data['read_until'] === 'number' && data['read_until'] !== 0) {
if (!blockRange) {
blockRange = {"block_num": {}};
}
if (data['read_until'] < 0) {
if (!head) {
head = (await fastify.eosjs.rpc.get_info()).head_block_num;
}
blockRange["block_num"]['lte'] = head + data['read_until'];
} else {
blockRange["block_num"]['lte'] = data['read_until'];
}
}
if (timeRange) {
search_body.query.bool.must.push({
range: timeRange,
});
}
if (blockRange) {
search_body.query.bool.must.push({
range: blockRange,
});
}
}
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"},
hot_only: {type: "boolean"},
lib: {type: "number"},
last_indexed_block: {type: "number"},
last_indexed_block_time: {type: "string"},
total: {
type: "object",
properties: {
@@ -386,7 +29,7 @@ export function extendResponseSchema(responseProps: any) {
};
}
export function extendQueryStringSchema(queryParams: any, required?: string[]) {
export function extendQueryStringSchema(queryParams: any) {
const params = {
limit: {
description: 'limit of [n] results per page',
@@ -404,14 +47,10 @@ export function extendQueryStringSchema(queryParams: any, required?: string[]) {
params[p] = queryParams[p];
}
}
const schema = {
return {
type: 'object',
properties: params
}
if (required && required.length > 0) {
schema["required"] = required;
}
return schema;
}
export async function getCacheByHash(redis, key, chain) {
@@ -441,15 +80,14 @@ export function mergeDeltaMeta(delta: any) {
return delta;
}
export function setCacheByHash(fastify, hash, response, expiration?: number) {
export function setCacheByHash(fastify, hash, response) {
if (fastify.manager.config.api.enable_caching) {
let exp;
if (expiration) {
exp = expiration;
} else {
exp = fastify.manager.config.api.cache_life;
}
fastify.redis.set(hash, JSON.stringify(response), 'EX', exp).catch(console.log);
fastify.redis
.set(hash,
JSON.stringify(response),
'EX',
fastify.manager.config.api.cache_life)
.catch(console.log);
}
}
@@ -460,11 +98,9 @@ export function getRouteName(filename: string) {
export function addApiRoute(
fastifyInstance: FastifyInstance,
method: HTTPMethods | HTTPMethods[],
method: HTTPMethod | HTTPMethod[],
routeName: string,
routeBuilder: (fastify: FastifyInstance, route: string) => (request: FastifyRequest, reply: FastifyReply) => Promise<void>,
schema: FastifySchema
) {
routeBuilder: (fastify: FastifyInstance, route: string) => (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => Promise<void>, schema: RouteSchema) {
fastifyInstance.route({
url: '/' + routeName,
method,
@@ -477,7 +113,8 @@ export async function getCachedResponse(server: FastifyInstance, route: string,
const chain = server.manager.chain;
let resp, hash;
if (server.manager.config.api.enable_caching) {
[resp, hash] = await getCacheByHash(server.redis, route + JSON.stringify(key), chain);
const keystring = JSON.stringify(key);
[resp, hash] = await getCacheByHash(server.redis, route + keystring, chain);
if (resp) {
resp = JSON.parse(resp);
resp['cached'] = true;
@@ -498,10 +135,8 @@ export function getTrackTotalHits(query) {
} else if (query.track === 'false') {
trackTotalHits = false;
} else {
const parsed = parseInt(query.track, 10);
if (parsed > 0) {
trackTotalHits = parsed;
} else {
trackTotalHits = parseInt(query.track, 10);
if (isNaN(trackTotalHits)) {
throw new Error('failed to parse track param');
}
}
@@ -513,12 +148,6 @@ function bigint2Milliseconds(input: bigint) {
return parseFloat((parseInt(input.toString()) / 1000000).toFixed(3));
}
const defaultRouteCacheMap = {
get_resource_usage: 3600,
get_creator: 3600 * 24,
health: 10
}
export async function timedQuery(
queryFunction: (fastify: FastifyInstance, request: FastifyRequest) => Promise<any>,
fastify: FastifyInstance, request: FastifyRequest, route: string): Promise<any> {
@@ -527,309 +156,27 @@ export async function timedQuery(
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.method === 'POST' ? request.body : request.query
);
const [cachedResponse, hash] = await getCachedResponse(fastify, route, request.query);
let lastIndexedBlockNumber = null;
let lastIndexedBlockTime = null;
const lastIndexedBlock = await fastify.redis.get(`${fastify.manager.chain}:last_idx_block`);
if (lastIndexedBlock) {
const arr = lastIndexedBlock.split("@");
if (arr.length === 2) {
lastIndexedBlockNumber = parseInt(arr[0], 10);
lastIndexedBlockTime = arr[1];
}
}
if (cachedResponse && !request.query["ignoreCache"]) {
if (cachedResponse) {
// add cached query time
cachedResponse['query_time_ms'] = bigint2Milliseconds(process.hrtime.bigint() - t0);
if (lastIndexedBlock) {
cachedResponse['last_indexed_block'] = lastIndexedBlockNumber;
cachedResponse['last_indexed_block_time'] = lastIndexedBlockTime;
}
return cachedResponse;
}
// call query function
const response = await queryFunction(fastify, request);
// save response to cache
// save response to cash
if (hash) {
let EX = null;
if (defaultRouteCacheMap[route]) {
EX = defaultRouteCacheMap[route];
}
setCacheByHash(fastify, hash, response, EX);
setCacheByHash(fastify, hash, response);
}
// add normal query time
if (response) {
response['query_time_ms'] = bigint2Milliseconds(process.hrtime.bigint() - t0);
if (lastIndexedBlock) {
response['last_indexed_block'] = lastIndexedBlockNumber;
response['last_indexed_block_time'] = lastIndexedBlockTime;
}
return response;
} else {
return {};
}
}
export function chainApiHandler(fastify: FastifyInstance) {
return async (request: FastifyRequest, reply: FastifyReply) => {
// check cache
const [cachedData, hash, path] = fastify.cacheManager.getCachedData(request);
if (cachedData) {
// console.log('cache hit:', path, hash);
reply.headers({'hyperion-cached': true}).send(cachedData);
} else {
// call actual request
const apiResponse = await handleChainApiRedirect(request, reply, fastify);
// console.log('cache miss:', path, hash);
fastify.cacheManager.setCachedData(hash, path, apiResponse);
}
}
}
export async function handleChainApiRedirect(
request: FastifyRequest,
reply: FastifyReply,
fastify: FastifyInstance
): Promise<string> {
const urlParts = request.url.split("?");
let reqUrl = fastify.chain_api + urlParts[0];
// const pathComponents = urlParts[0].split('/');
// const path = pathComponents.at(-1);
if (urlParts[0] === '/v1/chain/push_transaction' && fastify.push_api && fastify.push_api !== "") {
reqUrl = fastify.push_api + urlParts[0];
}
const opts = {};
if (request.method === 'POST') {
if (request.body) {
if (typeof request.body === 'string') {
opts['body'] = request.body;
} else if (typeof request.body === 'object') {
opts['body'] = JSON.stringify(request.body);
}
} else {
opts['body'] = "";
}
} else if (request.method === 'GET') {
opts['json'] = request.query;
}
try {
const apiResponse = await got.post(reqUrl, opts);
reply.headers({"Content-Type": "application/json"});
if (request.method === 'HEAD') {
reply.headers({"Content-Length": apiResponse.body.length});
reply.send("");
return '';
} else {
reply.send(apiResponse.body);
return apiResponse.body;
}
} catch (error) {
if (error.response) {
reply.status(error.response.statusCode).send(error.response.body);
} else {
console.log(error);
reply.status(500).send();
}
if (fastify.manager.config.api.chain_api_error_log) {
try {
if (error.response) {
const error_msg = JSON.parse(error.response.body).error.details[0].message;
console.log(`endpoint: ${request.raw.url} | status: ${error.response.statusCode} | error: ${error_msg}`);
} else {
console.log(error);
}
// if (request.req.url === '/v1/chain/push_transaction') {
// const packedTrx = JSON.parse(opts['body']).packed_trx;
// const trxBuffer = Buffer.from(packedTrx, 'hex');
// const trxData = await fastify.eosjs.api.deserializeTransactionWithActions(trxBuffer);
// console.log(trxData);
// }
} catch (e) {
console.log(e);
}
}
return '';
}
}
export function addChainApiRoute(fastify: FastifyInstance, routeName, description, props?, required?) {
const baseSchema = {
description: description,
summary: description,
tags: ['chain']
};
addApiRoute(
fastify,
['GET', 'HEAD'],
routeName,
chainApiHandler,
{
...baseSchema,
querystring: props ? {
type: 'object',
properties: props,
required: required
} : undefined
}
);
addApiRoute(
fastify,
'POST',
routeName,
chainApiHandler,
{
...baseSchema,
body: props ? {
type: ['object', 'string'],
properties: props,
required: required
} : undefined
}
);
}
export function addSharedSchemas(fastify: FastifyInstance) {
fastify.addSchema({
$id: "WholeNumber",
title: "Integer",
description: "Integer or String",
anyOf: [
{
type: "string",
pattern: "^\\d+$"
},
{
type: "integer"
}
],
});
fastify.addSchema({
$id: "Symbol",
type: "string",
description: "A symbol composed of capital letters between 1-7.",
pattern: "^([A-Z]{1,7})$",
title: "Symbol"
});
fastify.addSchema({
$id: "Signature",
type: "string",
description: "String representation of an EOSIO compatible cryptographic signature",
pattern: "^SIG_([RK]1|WA)_[1-9A-HJ-NP-Za-km-z]+$",
title: "Signature"
})
fastify.addSchema({
$id: "AccountName",
description: "String representation of an EOSIO compatible account name",
"anyOf": [
{
"type": "string",
"description": "String representation of privileged EOSIO name type",
"pattern": "^(eosio[\\.][a-z1-5]{1,6})([a-j]{1})?$",
"title": "NamePrivileged"
},
{
"type": "string",
"description": "String representation of basic EOSIO name type, must be 12 characters and contain only a-z and 0-5",
"pattern": "^([a-z]{1}[a-z1-5]{11})([a-j]{1})?$",
"title": "NameBasic"
},
{
"type": "string",
"description": "String representation of EOSIO bid name type, 1-12 characters and only a-z and 0-5 are allowed",
"pattern": "^([a-z1-5]{1,12})([a-j]{1})?$",
"title": "NameBid"
},
{
"type": "string",
"description": "String representation of EOSIO name type",
"pattern": "^([a-z1-5]{1}[a-z1-5\\.]{0,10}[a-z1-5]{1})([a-j]{1})?$",
"title": "NameCatchAll"
}
],
"title": "Name"
});
fastify.addSchema({
$id: "Expiration",
"description": "Time that transaction must be confirmed by.",
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$",
"title": "DateTime"
});
fastify.addSchema({
$id: "BlockExtensions",
"type": "array",
"items": {
"anyOf": [
{"type": "integer"},
{"type": "string"}
]
},
"title": "Extension"
})
fastify.addSchema({
$id: "ActionItems",
"type": "object",
"additionalProperties": false,
"minProperties": 5,
"required": [
"account",
"name",
"authorization",
"data",
"hex_data"
],
"properties": {
"account": {$ref: 'AccountName#'},
"name": {$ref: 'AccountName#'},
"authorization": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"minProperties": 2,
"required": [
"actor",
"permission"
],
"properties": {
"actor": {$ref: 'AccountName#'},
"permission": {$ref: 'AccountName#'}
},
"title": "Authority"
}
},
"data": {
"type": "object",
"additionalProperties": true
},
"hex_data": {
"type": "string"
}
},
"title": "Action"
});
}
+12 -22
View File
@@ -2,32 +2,22 @@ import * as Fastify from "fastify";
import {IncomingMessage, Server, ServerResponse} from "http";
// fastify plugins
import * as fastifyElasticsearch from 'fastify-elasticsearch';
import fastifySwagger from '@fastify/swagger';
import fastifyCors from '@fastify/cors';
import formBodyPlugin from '@fastify/formbody';
import fastifyRedis from '@fastify/redis';
import fastifyRateLimit from '@fastify/rate-limit';
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(fastifyElasticsearch, params.fastify_elasticsearch);
if (params.fastify_swagger) {
server.register(fastifySwagger, params.fastify_swagger);
}
server.register(fastifyCors);
server.register(formBodyPlugin);
server.register(fastifyRedis, params.fastify_redis);
if (params.fastify_rate_limit) {
server.register(fastifyRateLimit, params.fastify_rate_limit);
}
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);
}
+21
View File
@@ -0,0 +1,21 @@
const fp = require('fastify-plugin');
const {Api} = require("eosjs");
const {ConnectionManager} = require('../../connections/manager');
const manager = new ConnectionManager();
module.exports = fp(async (fastify, options, next) => {
const rpc = 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'
});
+6 -4
View File
@@ -1,17 +1,19 @@
import {FastifyInstance, FastifyPluginOptions} from "fastify";
import * as fp from 'fastify-plugin';
import {FastifyInstance} from "fastify";
import {Api} from "eosjs/dist";
import fp from "fastify-plugin";
export default fp(async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => {
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: options.chain_id,
chainId: chain_data.chain_id,
textDecoder: new TextDecoder(),
textEncoder: new TextEncoder(),
});
fastify.decorate('eosjs', {api, rpc});
next();
}, {
fastify: '>=2.0.0',
name: 'fastify-eosjs'
+30
View File
@@ -0,0 +1,30 @@
const fp = require('fastify-plugin');
const WebSocket = require('ws');
module.exports = fp((fastify, opts, next) => {
const wss = new WebSocket.Server({
server: fastify.server
});
const ws = new WebSocket('ws://127.0.0.1:7001', [], {
perMessageDeflate: false
});
ws.on('open', function open() {
console.log('router connected');
});
fastify.decorate('wss', wss);
fastify.decorate('ws', ws);
fastify.addHook('onClose', (fastify, done) => {
fastify.ws.close(done);
fastify.wss.close(done);
});
next();
}, {
fastify: '2.x',
name: 'fastify-websocket'
});
+22 -87
View File
@@ -1,121 +1,56 @@
import * as fastify_static from "fastify-static";
import {join} from "path";
import {FastifyError, FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {createReadStream} from "fs";
import {addSharedSchemas, handleChainApiRedirect} from "./helpers/functions";
import autoLoad from '@fastify/autoload';
import got from "got";
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: FastifyRequest, reply: FastifyReply) => {
schema: {hide: true},
handler: async (request, reply) => {
reply.redirect(redirectTo);
}
});
}
function addRoute(server: FastifyInstance, handlersPath: string, prefix: string) {
server.register(autoLoad, {
server.register(AutoLoad, {
dir: join(__dirname, 'routes', handlersPath),
ignorePattern: /.*(handler|schema).js/,
dirNameRoutePrefix: false,
options: {prefix}
});
}
export function registerRoutes(server: FastifyInstance) {
// build internal map of routes
const routeSet = new Set<string>();
server.decorate('routeSet', routeSet);
const ignoreList = [
'/v2',
'/v2/history',
'/v2/state',
'/v1/chain/*',
'/v1/chain'
];
server.addHook('onRoute', opts => {
if (!ignoreList.includes(opts.url)) {
if (opts.url.startsWith('/v')) {
routeSet.add(opts.url);
}
}
});
// Register fastify api routes
addRoute(server, 'v2', '/v2');
addRoute(server, 'v2-history', '/v2/history');
addRoute(server, 'v2-state', '/v2/state');
addRoute(server, 'v2-stats', '/v2/stats');
// legacy routes
addRoute(server, 'v1-history', '/v1/history');
addRoute(server, 'v1-trace', '/v1/trace_api');
// addRoute(server,'v1-chain', '/v1/chain');
addSharedSchemas(server);
// chain api redirects
addRoute(server, 'v1-chain', '/v1/chain');
// other v1 requests
server.route({
url: '/v1/chain/*',
method: ["GET", "POST"],
schema: {
summary: "Wildcard chain api handler",
tags: ["chain"]
},
handler: async (request: FastifyRequest, reply: FastifyReply) => {
await handleChainApiRedirect(request, reply, server);
}
});
// /v1/node/get_supported_apis
server.route({
url: '/v1/node/get_supported_apis',
method: ["GET"],
schema: {
summary: "Get list of supported APIs",
tags: ["node"]
},
handler: async (request: FastifyRequest, reply: FastifyReply) => {
const data = await got.get(`${server.chain_api}/v1/node/get_supported_apis`).json() as any;
if (data.apis && data.apis.length > 0) {
const apiSet = new Set(server.routeSet);
data.apis.forEach((a) => apiSet.add(a));
reply.send({apis: [...apiSet]});
} else {
reply.send({apis: [...server.routeSet], error: 'nodeos did not send any data'});
}
}
});
server.addHook('onError', (request: FastifyRequest, reply: FastifyReply, error: FastifyError, done) => {
console.log(`[${request.headers['x-real-ip'] || request.ip}] ${request.method} ${request.url} failed >> ${error.message}`);
done();
});
if (server.manager.config.features.streaming) {
// steam client lib
server.get(
'/stream-client.js',
{schema: {tags: ['internal']}},
(request: FastifyRequest, reply: FastifyReply) => {
const stream = createReadStream('./hyperion-stream-client.js');
reply.type('application/javascript').send(stream);
});
// Serve integrated explorer
if(server.manager.config.api.enable_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('./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');
addRedirect(server, '/v1/chain', '/v2/docs/index.html#/chain');
addRedirect(server, '/explorer', '/v2/explore');
addRedirect(server, '/explore', '/v2/explore');
}
+227
View File
@@ -0,0 +1,227 @@
const async = require('async');
const {Serialize} = require('eosjs');
const {ConnectionManager} = require('../../connections/manager');
let abi, types;
let connected = false;
let tables = new Map();
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const ws_opts = {compress: true, binary: true, mask: true, fin: true};
const maxMessagesInFlight = 1;
const blockReadingQueue = async.cargo(async.ensureAsync(processIncomingBlockArray), maxMessagesInFlight);
class BlockStore {
localBlocks;
pendingBlocks;
pendingRequests;
constructor() {
this.pendingRequests = [];
this.localBlocks = new Map();
this.pendingBlocks = new Map();
}
addBlock(num, data) {
if (this.pendingBlocks.has(num)) {
this.localBlocks.set(num, data);
this.pendingBlocks.get(num)['callback']();
this.pendingBlocks.delete(num);
}
}
requestRange(from_block, to_block, onBlock, onCompletion) {
// console.log(`Requesting blocks from ${from_block} to ${to_block}`);
const range = to_block - from_block;
// console.log(`Range size: ${range}`);
let requested = range;
for (let i = 0; i < range; i++) {
const blk_num = from_block + i;
// console.log('Looking on local cache for block ' + blk_num);
if (this.localBlocks.has(blk_num)) {
onBlock(this.localBlocks.get(blk_num), true);
requested--;
} else {
this.pendingBlocks.set(blk_num, {
callback: () => {
onBlock(this.localBlocks.get(blk_num), false);
}
});
}
}
if (requested > 0) {
requestBlockRange(from_block, to_block);
const checker = setInterval(() => {
if (this.pendingBlocks.size === 0) {
onCompletion();
clearInterval(checker);
}
}, 30);
} else {
onCompletion();
}
}
}
const blockStore = new BlockStore();
const baseRequest = {
max_messages_in_flight: maxMessagesInFlight,
have_positions: [],
irreversible_only: false,
fetch_block: true,
fetch_traces: true,
fetch_deltas: false
};
const manager = new ConnectionManager();
const shipClient = manager.shipClient;
startWS();
function createSerialBuffer(array_data) {
return new Serialize.SerialBuffer({textEncoder, textDecoder, array: array_data});
}
function serialize(type, value) {
const buffer = createSerialBuffer();
Serialize.getType(types, type).serialize(buffer, value);
return buffer.asUint8Array();
}
function deserialize(type, array) {
const buffer = new Serialize.SerialBuffer({textEncoder, textDecoder, array});
const sState = new Serialize.SerializerState({bytesAsUint8Array: true});
return Serialize.getType(types, type).deserialize(buffer, sState);
}
function processIncomingBlockArray(payload, cb) {
processIncomingBlocks(payload).then(() => {
cb();
}).catch((err) => {
console.log('FATAL ERROR READING BLOCKS', err);
process.exit(1);
})
}
function handleLostConnection() {
console.log('connection lost');
connected = false;
startWS();
}
function ackBlocks(num) {
shipClient.send(serialize('request',
['get_blocks_ack_request_v0', {num_messages: num}]
), ws_opts, (err) => {
if (err) console.warn(err);
});
}
async function processIncomingBlocks(block_array) {
if (abi) {
ackBlocks(block_array.length);
for (const block of block_array) {
try {
await onMessage(block);
} catch (e) {
console.log(e);
}
}
} else {
await onMessage(block_array[0]);
}
return true;
}
function processFirstABI(data) {
abi = JSON.parse(data);
types = Serialize.getTypesFromAbi(Serialize.createInitialTypes(), abi);
abi.tables.map(table => tables.set(table.name, table.type));
}
function deserializeType(obj) {
return deserialize(obj[0], obj[1], textEncoder, textDecoder, types);
}
function onMessage(data) {
if (abi) {
const res = deserialize('result', data)[1];
if (res['this_block']) {
const blk_num = res['this_block']['block_num'];
const block_data = {
block: null,
traces: [],
deltas: []
};
if (res.block && res.block.length) {
block_data.block = deserialize('signed_block', res.block, textEncoder, textDecoder, types);
if (block_data.block === null) {
console.log(res);
} else {
block_data.block.id = res['this_block']['block_id'];
block_data.block.block_num = blk_num;
block_data.block.transactions = [];
if (res['traces'] && res['traces'].length) {
const trx_traces = deserialize('transaction_trace[]', res['traces'], textEncoder, textDecoder, types).map(trace => trace[1]);
for (const trace of trx_traces) {
trace.partial = trace.partial[1];
trace.actions = [];
for (const action_trace of trace.action_traces) {
// console.log(action_trace);
// action_trace.receipt = action_trace.receipt[1];
const action = action_trace[1];
action.receipt = action.receipt[1];
action['act']['data'] = Buffer.from(action['act']['data']).toString('hex');
trace.actions.push(action);
}
delete trace.action_traces;
if (trace.actions[0]['act']['account'] === 'eosio' && trace.actions[0]['act']['name'] === 'onblock') {
// ingore onblock
} else {
block_data.block.transactions.push(trace);
}
}
}
}
}
// if (res['deltas'] && res['deltas'].length) {
// block_data.deltas = deserialize('table_delta[]', res['deltas'], textEncoder, textDecoder, types);
// }
blockStore.addBlock(blk_num, block_data.block);
}
} else {
processFirstABI(data);
return 1;
}
}
function send(req_data) {
shipClient.send(serialize('request', req_data, textEncoder, textDecoder, types));
}
function requestBlockRange(start, finish) {
const request = baseRequest;
request.start_block_num = start;
request.end_block_num = finish;
// console.log(request);
if (connected) {
send(['get_blocks_request_v0', request]);
} else {
startWS(() => {
send(['get_blocks_request_v0', request]);
});
}
}
function startWS(onConnect) {
if (onConnect) {
shipClient.connect(blockReadingQueue.push, handleLostConnection, null, onConnect);
} else {
shipClient.connect(blockReadingQueue.push, handleLostConnection);
}
connected = true;
}
module.exports = {blockStore};
+72
View File
@@ -0,0 +1,72 @@
const {getCacheByHash} = require("../../helpers/functions");
const {getAbiSnapshotSchema} = require("../../schemas");
const enable_caching = process.env.ENABLE_CACHING === 'true';
let cache_life = 30;
if (process.env.CACHE_LIFE) {
cache_life = parseInt(process.env.CACHE_LIFE);
}
async function getAbiSnapshot(fastify, request) {
const t0 = Date.now();
const {redis, elastic} = fastify;
const [cachedResponse, hash] = await getCacheByHash(redis, JSON.stringify(request.query));
if (cachedResponse) {
return JSON.parse(cachedResponse);
}
const response = {
query_time: 0,
block_num: null
};
const code = request.query.contract;
const block = request.query.block;
const should_fetch = request.query.fetch;
if (should_fetch) {
console.log(should_fetch);
}
const mustArray = [];
mustArray.push({"term": {"account": code}});
if (block) {
mustArray.push({"range": {"block": {"lte": parseInt(block)}}});
}
const results = await elastic.search({
index: process.env.CHAIN + '-abi',
size: 1,
body: {
query: {bool: {must: mustArray}},
sort: [{block: {order: "desc"}}]
}
});
if (results['body']['hits']['hits'].length > 0) {
if (should_fetch) {
response['abi'] = JSON.parse(results['body']['hits']['hits'][0]['_source']['abi']);
} else {
response['present'] = true;
}
response.block_num = results['body']['hits']['hits'][0]['_source']['block'];
} else {
response['present'] = false;
response['error'] = 'abi not found for ' + code + ' until block ' + block;
}
response['query_time'] = Date.now() - t0;
if (enable_caching) {
redis.set(hash, JSON.stringify(response), 'EX', cache_life);
}
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get('/get_abi_snapshot', {
schema: getAbiSnapshotSchema.GET
}, async (request, reply) => {
reply.send(await getAbiSnapshot(fastify, request));
});
next()
};
+378
View File
@@ -0,0 +1,378 @@
const prettyjson = require("prettyjson");
const {getActionsSchema} = require("../../schemas");
const {getCacheByHash, mergeActionMeta} = require("../../helpers/functions");
const maxActions = 1000;
const route = '/get_actions';
const terms = [
"notified.keyword",
"act.authorization.actor"
];
const extendedActions = new Set([
"transfer",
"newaccount",
"updateauth",
"buyram",
"buyrambytes"
]);
const primaryTerms = [
"notified",
"block_num",
"global_sequence",
"producer",
"@timestamp",
"creator_action_ordinal",
"action_ordinal",
"cpu_usage_us",
"net_usage_words"
];
const enable_caching = process.env.ENABLE_CACHING === 'true';
let cache_life = 30;
if (process.env.CACHE_LIFE) {
cache_life = parseInt(process.env.CACHE_LIFE);
}
function getTrackTotalHits(query) {
let trackTotalHits = 15000;
if (query.track) {
if (query.track === 'true') {
trackTotalHits = true;
} else if (query.track === 'false') {
trackTotalHits = false;
} else {
trackTotalHits = parseInt(query.track, 10);
if (trackTotalHits !== trackTotalHits) {
throw new Error('failed to parse track param');
}
}
}
return trackTotalHits;
}
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
};
}
}
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});
}
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
}
}
});
}
}
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});
}
}
}
}
}
}
}
}
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;
}
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) {
[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;
}
}
}
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};
}
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;
}
function applyAccountFilters(query, queryStruct) {
if (query.account) {
queryStruct.bool.must.push({"bool": {should: makeShouldArray(query)}});
}
}
async function getActions(fastify, request) {
const t0 = Date.now();
const {redis, elastic, eosjs} = fastify;
const query = request.query;
let cachedResponse, hash;
if (enable_caching) {
[cachedResponse, hash] = await getCacheByHash(redis, route + JSON.stringify(query));
if (cachedResponse) {
cachedResponse = JSON.parse(cachedResponse);
cachedResponse['query_time'] = Date.now() - t0;
cachedResponse['cached'] = true;
return cachedResponse;
}
}
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);
// console.log(prettyjson.render(queryStruct));
// Perform search
const pResults = await Promise.all([eosjs.rpc.get_info(), elastic['search']({
"index": process.env.CHAIN + '-action-*',
"from": skip || 0,
"size": (limit > maxActions ? maxActions : limit) || 10,
"body": query_body
})]);
const results = pResults[1]['body']['hits'];
const response = {
query_time: null,
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);
}
}
}
response['query_time'] = Date.now() - t0;
if (enable_caching) {
redis.set(hash, JSON.stringify(response), 'EX', cache_life);
}
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get('/get_actions', {
schema: getActionsSchema.GET
}, async (request) => {
return await getActions(fastify, request);
});
next()
};
+49
View File
@@ -0,0 +1,49 @@
const {getBlocksSchema} = require("../../schemas");
const {blockStore} = require('../blockStore');
async function getBlocks(fastify, request, reply) {
const t0 = Date.now();
let from_block, to_block;
const {eosjs} = fastify;
if (request.query.from) {
from_block = parseInt(request.query.from);
if (request.query.to) {
to_block = parseInt(request.query.to);
} else {
to_block = from_block + 1;
}
} else {
const head = (await eosjs.rpc.get_info()).head_block_num;
from_block = head;
to_block = head + 1;
}
if (to_block > from_block) {
if (to_block - from_block > 20) {
reply.status(403).send('request over limit');
} else {
const response = {query_time: null, cached: false, blocks: []};
console.log('Local Block store size:', blockStore.localBlocks.size);
blockStore.requestRange(from_block, to_block, (block, cached) => {
if (cached) {
response.cached = true;
}
response.blocks.push(block);
}, () => {
response['query_time'] = Date.now() - t0;
reply.send(response);
});
}
} else {
reply.status(403).send('invalid block range');
}
}
module.exports = function (fastify, opts, next) {
fastify.get('/get_blocks', {
schema: getBlocksSchema.GET
}, async (request, reply) => {
await getBlocks(fastify, request, reply);
});
next();
};
+62
View File
@@ -0,0 +1,62 @@
const {getCreatedAccountsSchema} = require("../../schemas");
const {getCacheByHash} = require("../../helpers/functions");
const route = '/get_created_accounts';
async function getCreatedAccounts(fastify, request) {
const t0 = Date.now();
const {redis, elastic} = fastify;
const [cachedResponse, hash] = await getCacheByHash(redis, JSON.stringify(request.query));
if (cachedResponse) {
return cachedResponse;
}
const results = await elastic['search']({
"index": process.env.CHAIN + '-action-*',
"body": {
"query": {
"bool": {
must: [
{term: {"act.authorization.actor": request.query.account.toLowerCase()}},
{term: {"act.name": "newaccount"}},
{term: {"act.account": "eosio"}}
]
}
},
sort: {
"global_sequence": "desc"
}
}
});
const response = {
"accounts": []
};
if (results['body']['hits']['hits'].length > 0) {
const actions = results['body']['hits']['hits'];
for (let action of actions) {
action = action._source;
const _tmp = {};
if (action['act']['data']['newact']) {
_tmp['name'] = action['act']['data']['newact'];
} else if (action['@newaccount']['newact']) {
_tmp['name'] = action['@newaccount']['newact'];
} else {
console.log(action);
}
_tmp['trx_id'] = action['trx_id'];
_tmp['timestamp'] = action['@timestamp'];
response.accounts.push(_tmp);
}
}
response['query_time'] = Date.now() - t0;
redis.set(hash, JSON.stringify(response), 'EX', 30);
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get(route, {
schema: getCreatedAccountsSchema.GET
}, async (request) => {
return await getCreatedAccounts(fastify, request);
});
next()
};
+108
View File
@@ -0,0 +1,108 @@
const {getCreatorSchema} = require("../../schemas");
const {getCacheByHash} = require("../../helpers/functions");
const route = '/get_creator';
async function getActionByGS(client, gs) {
console.log(gs);
const results = await client['search']({
index: process.env.CHAIN + '-action-*',
body: {
query: {
bool: {
must: [
{term: {"global_sequence": gs}}
]
}
}
}
});
return results['body']['hits']['hits'][0]['_source'];
}
async function getCreator(fastify, request) {
const {redis, elastic, eosjs} = fastify;
const [cachedResponse, hash] = await getCacheByHash(redis, route + JSON.stringify(request.query) + 'v3');
if (cachedResponse) {
return cachedResponse;
}
const newact = request.query.account;
const response = {
account: newact,
creator: '',
timestamp: '',
block_num: 0,
trx_id: '',
};
let account_data = null;
try {
account_data = await eosjs.rpc.get_account(newact);
} catch (e) {
const err = new Error();
err.statusCode = 404;
err.message = 'account not found';
throw err;
}
response.timestamp = account_data['created'];
const queryBody = {
size: 1000,
query: {
bool: {
must: [
{term: {"act.name": "newaccount"}},
{term: {"act.account": "eosio"}},
{term: {"@timestamp": account_data['created']}}
]
}
}
};
const results = await elastic['search']({
"index": process.env.CHAIN + '-action-*',
"body": queryBody
});
for (const action of results['body']['hits']['hits']) {
const actData = action._source.act.data;
let valid = false;
response.block_num = action._source.block_num;
if (actData.newact === newact) {
response.creator = actData.creator;
response['trx_id'] = action._source['trx_id'];
valid = true;
} else {
if (action._source['@newaccount']) {
if (action._source['@newaccount']['newact'] === newact) {
response.creator = actData.creator;
response['trx_id'] = action._source['trx_id'];
valid = true;
}
}
}
// if (action._source.parent !== 0 && valid) {
//
// // Find indirect creator by global seq
// const creationAction = await getActionByGS(elastic, action._source.parent);
//
// if (creationAction.act.name === 'transfer') {
// response['indirect_creator'] = creationAction['@transfer']['from'];
// response['trx_id'] = creationAction['trx_id'];
// } else {
// response['indirect_creator'] = creationAction.act.authorization[0].actor;
// response['trx_id'] = creationAction['trx_id'];
// }
// }
}
redis.set(hash, JSON.stringify(response), 'EX', 600);
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get(route, {
schema: getCreatorSchema.GET
}, async (request) => {
return await getCreator(fastify, request);
});
next()
};
+99
View File
@@ -0,0 +1,99 @@
const {getDeltasSchema} = require("../../schemas");
const {getCacheByHash} = require("../../helpers/functions");
const route = '/get_deltas';
const maxDeltas = 200;
async function getDeltas(fastify, request) {
const t0 = Date.now();
console.log(request.query);
const {redis, elastic} = fastify;
const [cachedResponse, hash] = await getCacheByHash(redis, route + JSON.stringify(request.query) + 'v2');
// if (cachedResponse) {
// return cachedResponse;
// }
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;
}
}
}
}
let prefix = process.env.CHAIN;
if (process.env.CHAIN === 'mainnet') {
prefix = 'eos';
}
const results = await elastic.search({
"index": prefix + '-delta-*',
"from": skip || 0,
"size": (limit > maxDeltas ? maxDeltas : limit) || 10,
"body": {
query: {bool: {must: mustArray}},
sort: {
"block_num": sort_direction
}
}
});
const response = {
query_time: null,
total: results['body']['hits']['total'],
deltas: results['body']['hits']['hits'].map((d) => {
delete d._source.present;
return d._source;
})
};
response['query_time'] = Date.now() - t0;
// redis.set(hash, JSON.stringify(response));
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get(route, {
schema: getDeltasSchema.GET
}, async (request) => {
return await getDeltas(fastify, request);
});
next()
};
+200
View File
@@ -0,0 +1,200 @@
const {getCacheByHash} = require("../../helpers/functions");
const {getTransactedAccountsSchema} = require("../../schemas");
async function getTransactedAccounts(fastify, request) {
const t0 = Date.now();
const {redis, elastic} = fastify;
const [cachedResponse, hash] = await getCacheByHash(redis, JSON.stringify(request.query));
if (cachedResponse) {
return cachedResponse;
}
const {account, min, max, direction, limit, contract, symbol} = request.query;
let _limit = 100;
if (limit) {
_limit = parseInt(limit, 10);
if (_limit > 500) {
_limit = 500;
}
}
let _min;
if (min) {
_min = parseInt(min, 10);
if (_min < 0) {
_min = 0;
}
}
let _max;
if (max) {
_max = parseInt(max, 10);
if (_max < 0) {
_max = null;
}
}
const response = {
query_time: null,
account: account
};
const must_array = [
{term: {"notified": account}},
{term: {"act.name": "transfer"}}
];
if (min || max) {
const _range = {
"@transfer.amount": {}
};
if (min) {
_range["@transfer.amount"]["gte"] = _min;
}
if (max) {
_range["@transfer.amount"]["lt"] = _max;
}
must_array.push({range: _range});
}
if (contract) {
must_array.push({term: {"act.account": contract}});
response['contract'] = contract;
}
if (symbol) {
must_array.push({term: {"@transfer.symbol": symbol}});
response['symbol'] = symbol;
}
const sum_agg = {
"total_amount": {
sum: {
field: "@transfer.amount"
}
}
};
let filter_array = [];
if (request.query['after'] || request.query['before']) {
let _lte = "now";
let _gte = 0;
if (request.query['before']) {
_lte = request.query['before'];
}
if (request.query['after']) {
_gte = request.query['after'];
}
filter_array.push({
range: {
"@timestamp": {
"gte": _gte,
"lte": _lte
}
}
});
} else {
filter_array.push({
range: {
"block_num": {
"gte": 0
}
}
});
}
if (direction === 'out' || direction === 'both') {
const outResults = await elastic.search({
index: process.env.CHAIN + '-action-*',
body: {
size: 0,
aggs: {
"receivers": {
terms: {
field: "@transfer.to",
size: _limit,
order: {
"total_amount": "desc"
}
},
aggs: sum_agg
}
},
query: {
bool: {
must: must_array,
filter: filter_array,
must_not: [
{term: {"@transfer.to": account}}
]
}
}
}
});
response['total_out'] = 0;
response['outputs'] = outResults['body']['aggregations']['receivers']['buckets'].map((bucket) => {
const _sum = parseFloat(bucket.total_amount.value.toFixed(4));
response['total_out'] += _sum;
return {
account: bucket.key,
sum: _sum,
transfers: bucket['doc_count'],
average: parseFloat((bucket.total_amount.value / bucket['doc_count']).toFixed(4))
};
});
response['total_out'] = parseFloat(response['total_out'].toFixed(4));
}
if (direction === 'in' || direction === 'both') {
const inResults = await elastic.search({
index: process.env.CHAIN + '-action-*',
body: {
size: 0,
aggs: {
"senders": {
terms: {
field: "@transfer.from",
size: _limit,
order: {
"total_amount": "desc"
}
},
aggs: sum_agg
}
},
query: {
bool: {
must: must_array,
filter: filter_array,
must_not: [
{term: {"@transfer.from": account}}
]
}
}
}
});
response['total_in'] = 0;
response['inputs'] = inResults['body']['aggregations']['senders']['buckets'].map((bucket) => {
const _sum = parseFloat(bucket.total_amount.value.toFixed(4));
response['total_in'] += _sum;
return {
account: bucket.key,
sum: _sum,
transfers: bucket['doc_count'],
average: parseFloat((bucket.total_amount.value / bucket['doc_count']).toFixed(4))
};
});
response['total_in'] = parseFloat(response['total_in'].toFixed(4));
}
response['query_time'] = Date.now() - t0;
redis.set(hash, JSON.stringify(response), 'EX', 600);
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get('/get_transacted_accounts', {
schema: getTransactedAccountsSchema.GET
}, async (request, reply) => {
reply.send(await getTransactedAccounts(fastify, request));
});
next()
};
+55
View File
@@ -0,0 +1,55 @@
const {getTransactionSchema} = require("../../schemas");
const _ = require('lodash');
const {getCacheByHash} = require("../../helpers/functions");
async function getTransaction(fastify, request) {
const {redis, elastic, eosjs} = fastify;
const [cachedResponse, hash] = await getCacheByHash(redis, JSON.stringify(request.query));
if (cachedResponse) {
return cachedResponse;
}
const pResults = await Promise.all([eosjs.rpc.get_info(), elastic['search']({
"index": process.env.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;
const name = action.act.name;
if (action['@' + name]) {
action['act']['data'] = _.merge(action['@' + name], action['act']['data']);
delete action['@' + name];
}
response.actions.push(action);
}
}
redis.set(hash, JSON.stringify(response), 'EX', 30);
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get('/get_transaction', {
schema: getTransactionSchema.GET
}, async (request, reply) => {
reply.send(await getTransaction(fastify, request));
});
next()
};
+98
View File
@@ -0,0 +1,98 @@
const {getTransfersSchema} = require("../../schemas");
const _ = require('lodash');
const {getCacheByHash} = require("../../helpers/functions");
const route = '/get_transfers';
const maxActions = 1000;
function processActions(results) {
const action_traces = results['body']['hits']['hits'];
const actions = [];
let sum = 0;
for (const aTrace of action_traces) {
const action = aTrace['_source'];
const name = action.act.name;
if (action['@' + name]) {
if (action['@transfer']) {
sum += parseFloat(action['@transfer']['amount']);
}
action['act']['data'] = _.merge(action['@' + name], action['act']['data']);
delete action['@' + name];
}
actions.push(action);
}
return [sum, actions];
}
async function getTransfers(fastify, request) {
const {redis, elastic} = fastify;
const [cachedResponse, hash] = await getCacheByHash(redis, route + JSON.stringify(request.query));
if (cachedResponse) {
return cachedResponse;
}
const must_array = [];
must_array.push({"term": {"act.name": {"value": "transfer"}}});
if (request.query['from']) {
must_array.push({"term": {"@transfer.from": {"value": request.query['from'].toLowerCase()}}});
}
if (request.query['to']) {
must_array.push({"term": {"@transfer.to": {"value": request.query['to'].toLowerCase()}}});
}
if (request.query['symbol']) {
must_array.push({"term": {"@transfer.symbol": {"value": request.query['symbol'].toUpperCase()}}});
}
if (request.query['contract']) {
must_array.push({"term": {"act.account": {"value": request.query['contract'].toLowerCase()}}});
}
if (request.query['after'] || request.query['before']) {
let _lte = "now";
let _gte = 0;
if (request.query['before']) {
_lte = request.query['before'];
}
if (request.query['after']) {
_gte = request.query['after'];
}
must_array.push({
range: {
"@timestamp": {
"gte": _gte,
"lte": _lte
}
}
});
}
let limit, skip;
limit = parseInt(request.query.limit, 10);
if (limit < 1) {
return 'invalid limit parameter';
}
skip = parseInt(request.query.skip, 10);
if (skip < 0) {
return 'invalid skip parameter';
}
const body = {"query": {"bool": {"must": must_array}}};
const results = await elastic.search({
"index": process.env.CHAIN + '-action-*',
"from": skip || 0,
"size": (limit > maxActions ? maxActions : limit) || 10,
"body": body
});
const [sum, _actions] = processActions(results);
const response = {
"action_count": results['body']['hits']['total']['value'],
"total_amount": sum,
"actions": _actions
};
redis.set(hash, JSON.stringify(response), 'EX', 30);
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get(route, {
schema: getTransfersSchema.GET
}, async (request, reply) => {
reply.send(await getTransfers(fastify, request));
});
next()
};
+65
View File
@@ -0,0 +1,65 @@
const {getAccountSchema} = require("../../schemas");
const {getCacheByHash} = require("../../helpers/functions");
const route = '/get_account';
const got = require('got');
const localApi = `http://${process.env.SERVER_ADDR}:${process.env.SERVER_PORT}/v2`;
const getTokensApi = localApi + '/state/get_tokens';
const getActionsApi = localApi + '/history/get_actions';
const enable_caching = process.env.ENABLE_CACHING === 'true';
let cache_life = 30;
if (process.env.CACHE_LIFE) {
cache_life = parseInt(process.env.CACHE_LIFE);
}
async function getAccount(fastify, request) {
const t0 = Date.now();
const {redis, elastic, eosjs} = fastify;
// let cachedResponse, hash;
// if(enable_caching) {
// [cachedResponse, hash] = await getCacheByHash(redis, route + JSON.stringify(request.query));
// if (cachedResponse) {
// cachedResponse = JSON.parse(cachedResponse);
// cachedResponse['query_time'] = Date.now() - t0;
// cachedResponse['cached'] = true;
// return cachedResponse;
// }
// }
const response = {
query_time: null,
cached: false,
account: null,
actions: null,
tokens: null,
links: null
};
const account = request.query.account;
const reqQueue = [];
reqQueue.push(eosjs.rpc.get_account(account));
// 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;
response['query_time'] = Date.now() - t0;
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get(route, {
schema: getAccountSchema.GET
}, async (request) => {
return await getAccount(fastify, request);
});
next()
};
+97
View File
@@ -0,0 +1,97 @@
const {getKeyAccountsSchema} = require("../../schemas");
const {getCacheByHash} = require("../../helpers/functions");
const numeric = require('eosjs/dist/eosjs-numeric');
function invalidKey() {
const err = new Error();
err.statusCode = 400;
err.message = 'invalid public key';
throw err;
}
async function getKeyAccounts(fastify, public_Key) {
const {redis, elastic} = fastify;
let publicKey;
if (public_Key.startsWith("PUB_")) {
publicKey = public_Key;
} else if (public_Key.startsWith("EOS")) {
try {
publicKey = numeric.convertLegacyPublicKey(public_Key);
} catch (e) {
console.log(e);
invalidKey();
}
} else {
invalidKey();
}
const [cachedResponse, hash] = await getCacheByHash(redis, JSON.stringify(publicKey));
if (cachedResponse) {
return cachedResponse;
}
const _body = {
query: {
bool: {
should: [
{term: {"@updateauth.auth.keys.key.keyword": publicKey}},
{term: {"@newaccount.active.keys.key.keyword": publicKey}},
{term: {"@newaccount.owner.keys.key.keyword": publicKey}}
],
minimum_should_match: 1
}
},
sort: [{"global_sequence": {"order": "desc"}}]
};
const results = await elastic.search({
index: process.env.CHAIN + '-action-*',
body: _body
});
const response = {
account_names: []
};
if (results['body']['hits']['hits'].length > 0) {
response.account_names = results['body']['hits']['hits'].map((v) => {
if (v._source.act.name === 'newaccount') {
if (v._source['@newaccount'].newact) {
return v._source['@newaccount'].newact;
} else if (v._source.act.data.newact) {
return v._source.act.data.newact;
} else {
return null;
}
} else if (v._source.act.name === 'updateauth') {
return v._source.act.data.account;
} else {
return null;
}
});
}
if (response.account_names.length > 0) {
response.account_names = [...(new Set(response.account_names))];
redis.set(hash, JSON.stringify(response), 'EX', 30);
return response;
} else {
const err = new Error();
err.statusCode = 404;
err.message = 'no accounts associated with ' + public_Key;
throw err;
}
}
module.exports = function (fastify, opts, next) {
fastify.get('/get_key_accounts', {
schema: getKeyAccountsSchema.GET
}, async (request) => {
return getKeyAccounts(fastify, request.query.public_key);
});
fastify.post('/get_key_accounts', {
schema: getKeyAccountsSchema.POST
}, async (request) => {
return getKeyAccounts(fastify, request.body.public_key);
});
next();
};
+146
View File
@@ -0,0 +1,146 @@
const {getProposalsSchema} = require("../../schemas");
const {getCacheByHash} = require("../../helpers/functions");
const route = '/get_proposals';
const maxActions = 1000;
const enable_caching = process.env.ENABLE_CACHING === 'true';
let cache_life = 30;
if (process.env.CACHE_LIFE) {
cache_life = parseInt(process.env.CACHE_LIFE);
}
function getTrackOpt(request) {
let trackTotalHits = 10000;
if (request.query.track) {
if (request.query.track === 'true') {
trackTotalHits = true;
} else if (request.query.track === 'false') {
trackTotalHits = false;
} else {
trackTotalHits = parseInt(request.query.track, 10);
if (trackTotalHits !== trackTotalHits) {
throw new Error('failed to parse track param');
}
}
}
return trackTotalHits;
}
async function getProposals(fastify, request) {
const t0 = Date.now();
const {redis, elastic, eosjs} = fastify;
let cachedResponse, hash;
if (enable_caching) {
[cachedResponse, hash] = await getCacheByHash(redis, route + JSON.stringify(request.query));
if (cachedResponse) {
cachedResponse = JSON.parse(cachedResponse);
cachedResponse['query_time'] = Date.now() - t0;
cachedResponse['cached'] = true;
return cachedResponse;
}
}
// 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 = {
"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": {}
};
}
console.log(JSON.stringify(queryStruct));
const results = await elastic.search({
"index": process.env.CHAIN + '-table-proposals-*',
"from": skip || 0,
"size": (limit > maxActions ? maxActions : limit) || 10,
"body": {
"track_total_hits": getTrackOpt(request),
"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);
}
response['query_time'] = Date.now() - t0;
if (enable_caching) {
redis.set(hash, JSON.stringify(response), 'EX', cache_life);
}
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get(route, {
schema: getProposalsSchema.GET
}, async (request) => {
return await getProposals(fastify, request);
});
next();
};
+94
View File
@@ -0,0 +1,94 @@
const {getTokensSchema} = require("../../schemas");
const {getCacheByHash} = require("../../helpers/functions");
const route = '/get_tokens';
const enable_caching = process.env.ENABLE_CACHING === 'true';
let cache_life = 30;
if(process.env.CACHE_LIFE) {
cache_life = parseInt(process.env.CACHE_LIFE);
}
async function getTokens(fastify, request) {
const t0 = Date.now();
const {redis, elastic, eosjs} = fastify;
let cachedResponse, hash;
if(enable_caching) {
[cachedResponse, hash] = await getCacheByHash(redis, route + JSON.stringify(request.query));
if (cachedResponse) {
cachedResponse = JSON.parse(cachedResponse);
cachedResponse['query_time'] = Date.now() - t0;
cachedResponse['cached'] = true;
return cachedResponse;
}
}
const response = {
query_time: null,
cached: false,
'account': request.query.account,
'tokens': []
};
const results = await 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 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']
});
}
}
response['query_time'] = Date.now() - t0;
if(enable_caching) {
redis.set(hash, JSON.stringify(response), 'EX', cache_life);
}
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get(route, {
schema: getTokensSchema.GET
}, async (request) => {
return await getTokens(fastify, request);
});
next()
};
+90
View File
@@ -0,0 +1,90 @@
const {getVotersSchema} = require("../../schemas");
const {getCacheByHash} = require("../../helpers/functions");
const route = '/get_voters';
const maxActions = 1000;
async function getVoters(fastify, request) {
const t0 = Date.now();
const {redis, elastic} = fastify;
const [cachedResponse, hash] = await getCacheByHash(redis, route + JSON.stringify(request.query));
if (cachedResponse) {
return cachedResponse;
}
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 = {
"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": {}
};
}
let prefix = process.env.CHAIN;
if (process.env.CHAIN === 'mainnet') {
prefix = 'eos';
}
const results = await elastic.search({
"index": prefix + '-table-voters-*',
"from": skip || 0,
"size": (limit > maxActions ? maxActions : 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'];
response['query_time'] = Date.now() - t0;
redis.set(hash, JSON.stringify(response), 'EX', 30);
return response;
}
module.exports = function (fastify, opts, next) {
fastify.get(route, {
schema: getVotersSchema.GET
}, async (request) => {
return await getVoters(fastify, request);
});
next();
};
@@ -1,21 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Returns an object containing rows from the specified table.',
{
"code": {$ref: 'AccountName#'},
"action": {$ref: 'AccountName#'},
"binargs": {
"type": "string",
"pattern": "^(0x)(([0-9a-f][0-9a-f])+)?$",
"title": "Hex"
}
},
["code", "action", "binargs"]
);
next();
}
@@ -1,19 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Convert JSON object to binary',
{
"binargs": {
"type": "string",
"pattern": "^(0x)(([0-9a-f][0-9a-f])+)?$",
"title": "Hex"
}
},
["binargs"]
);
next();
}
-13
View File
@@ -1,13 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves the ABI for a contract based on its account name',
{"account_name": {$ref: 'AccountName#'}},
["account_name"]
);
next();
}
-15
View File
@@ -1,15 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Returns an object containing various details about a specific account on the blockchain.',
{
"account_name": {$ref: 'AccountName#'}
},
["account_name"]
);
next();
}
@@ -1,33 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retreives the activated protocol features for producer node',
{
"lower_bound": {
"type": "integer",
"description": "Lower bound"
},
"upper_bound": {
"type": "integer",
"description": "Upper bound"
},
"limit": {
"type": "integer",
"description": "The limit, default is 10"
},
"search_by_block_num": {
"type": "boolean",
"description": "Flag to indicate it is has to search by block number"
},
"reverse": {
"type": "boolean",
"description": "Flag to indicate it has to search in reverse"
}
}
);
next();
}
+134
View File
@@ -0,0 +1,134 @@
const schema = {
description: 'Returns an object containing various details about a specific block on the blockchain.',
summary: 'Returns an object containing various details about a specific block on the blockchain.',
tags: ['state'],
body: {
type: ['object', 'string'],
properties: {
"block_num_or_id": {
description: 'Provide a block number or a block id',
type: 'string'
},
},
required: ["block_num_or_id"]
}
};
const route = '/get_block';
const {blockStore} = require('../blockStore');
const {getCacheByHash} = require("../../helpers/functions");
let last_get_info = 0;
let chain_info;
async function getInfo(eosjs) {
if (Date.now() - last_get_info > 1000) {
chain_info = await eosjs.rpc.get_info();
last_get_info = Date.now();
}
}
async function processBlockData(block, eosjs) {
// Calculate total usage
let total_cpu = 0;
let total_net = 0;
block.transactions.forEach((trx) => {
total_cpu += trx['cpu_usage_us'];
total_net += trx['net_usage_words'];
});
block['total_cpu_usage_us'] = total_cpu;
block['total_net_usage_words'] = total_net;
// check irreversibility
await getInfo(eosjs);
block['last_irreversible_block_num'] = chain_info.last_irreversible_block_num;
block['irreversible'] = block['block_num'] <= block['last_irreversible_block_num'];
return block;
}
async function getBlock(fastify, request, reply) {
const {eosjs, redis} = fastify;
let block_num_or_id;
try {
if (typeof request.body === 'object') {
block_num_or_id = request.body['block_num_or_id'];
} else {
block_num_or_id = JSON.parse(request.body)['block_num_or_id'];
}
} catch (e) {
console.log(request.body);
console.log(e);
}
if (parseInt(block_num_or_id) === 0) {
await getInfo(eosjs);
block_num_or_id = chain_info.head_block_num;
}
if (block_num_or_id) {
let cachedResponse, hash;
const key = route + '_' + block_num_or_id;
[cachedResponse, hash] = await getCacheByHash(redis, key);
if (cachedResponse) {
cachedResponse = JSON.parse(cachedResponse);
if (cachedResponse['irreversible']) {
reply.send(cachedResponse);
return;
}
}
try {
let blockdata = await eosjs.rpc.get_block(block_num_or_id);
blockdata = await processBlockData(blockdata, eosjs);
const blockstring = JSON.stringify(blockdata);
if (blockdata['irreversible']) {
redis.set(hash, blockstring, 'EX', 86400);
} else {
redis.set(hash, blockstring, 'EX', 10);
}
reply.send(blockdata);
} catch (e) {
reply.code(400).send({
"code": 400,
"message": "Unknown Block",
"error": {
"code": 3100002,
"name": "unknown_block_exception",
"what": "Unknown block",
"details": [
{
"message": "Could not find block: " + block_num_or_id,
"file": "chain_plugin.cpp",
"line_number": 1852,
"method": "get_block"
}
]
}
});
}
} else {
reply.code(400).send({});
}
// TODO: fallback to state history
// let from_block;
// if (request.body['block_num_or_id']) {
// from_block = parseInt(request.body['block_num_or_id']);
// let response;
// console.log('Local Block store size:', blockStore.localBlocks.size);
// blockStore.requestRange(from_block, from_block + 1, (block) => {
// response = block;
// }, () => {
// reply.send(response);
// });
// }
}
module.exports = function (fastify, opts, next) {
fastify.post(route, {schema}, async (request, reply) => {
await getBlock(fastify, request, reply);
});
next();
};
-18
View File
@@ -1,18 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Returns an object containing various details about a specific block on the blockchain.',
{
"block_num_or_id": {
description: "Provide a `block number` or a `block id`",
type: 'string'
}
},
["block_num_or_id"]
);
next();
}
@@ -1,18 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves the block header state',
{
"block_num_or_id": {
"type": "string",
"description": "Provide a block_number or a block_id"
}
},
["block_num_or_id"]
);
next();
}
-20
View File
@@ -1,20 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves contract code',
{
"account_name": {$ref: 'AccountName#'},
"code_as_wasm": {
"type": "integer",
"default": 1,
"description": "This must be 1 (true)"
}
},
["account_name", "code_as_wasm"]
);
next();
}
@@ -1,17 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves the current balance',
{
"code": {$ref: 'AccountName#'},
"account": {$ref: 'AccountName#'},
"symbol": {$ref: 'Symbol#'}
},
["code", "account", "symbol"]
);
next();
}
@@ -1,19 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves currency stats',
{
"code": {$ref: 'AccountName#'},
"symbol": {
description: 'token symbol',
type: 'string',
}
},
["code", "symbol"]
);
next();
}
-11
View File
@@ -1,11 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Returns an object containing various details about the blockchain.'
);
next();
}
@@ -1,25 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves producers list',
{
"limit": {
"type": "string",
"description": "total number of producers to retrieve"
},
"lower_bound": {
"type": "string",
"description": "In conjunction with limit can be used to paginate through the results. For example, limit=10 and lower_bound=10 would be page 2"
},
"json": {
"type": "boolean",
"description": "return result in JSON format"
}
}
);
next();
}
-15
View File
@@ -1,15 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves raw ABI for a contract based on account name',
{
"account_name": {$ref: 'AccountName#'}
},
["account_name"]
);
next();
}
@@ -1,15 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves raw code and ABI for a contract based on account name',
{
"account_name": {$ref: 'AccountName#'}
},
["account_name"]
);
next();
}
@@ -1,27 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves the scheduled transaction',
{
"lower_bound": {
"type": "string",
"description": "Date/time string in the format YYYY-MM-DDTHH:MM:SS.sss",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}$",
"title": "DateTimeSeconds"
},
"limit": {
"description": "The maximum number of transactions to return",
"type": "integer"
},
"json": {
"description": "true/false whether the packed transaction is converted to json",
"type": "boolean"
}
}
);
next();
}
@@ -1,38 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Retrieves table scope',
{
"code": {
"type": "string",
"description": "`name` of the contract to return table data for"
},
"table": {
"type": "string",
"description": "Filter results by table"
},
"lower_bound": {
"type": "string",
"description": "Filters results to return the first element that is not less than provided value in set"
},
"upper_bound": {
"type": "string",
"description": "Filters results to return the first element that is greater than provided value in set"
},
"limit": {
"type": "integer",
"description": "Limit number of results returned."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of returned results"
}
},
["code"]
);
next();
}
@@ -1,43 +0,0 @@
import {FastifyInstance} from "fastify";
import {addChainApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addChainApiRoute(
fastify,
getRouteName(__filename),
'Returns an object containing rows from the specified table.',
{
"code": {
"type": "string",
"description": "The name of the smart contract that controls the provided table"
},
"table": {
"type": "string",
"description": "The name of the table to query"
},
"scope": {
"type": "string",
"description": "The account to which this data belongs"
},
"index_position": {
"type": "string",
"description": "Position of the index used, accepted parameters `primary`, `secondary`, `tertiary`, `fourth`, `fifth`, `sixth`, `seventh`, `eighth`, `ninth` , `tenth`"
},
"key_type": {
"type": "string",
"description": "Type of key specified by index_position (for example - `uint64_t` or `name`)"
},
"encode_type": {
"type": "string"
},
"upper_bound": {
"type": "string"
},
"lower_bound": {
"type": "string"
}
},
["code", "table"]
);
next();
}
@@ -1,39 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, chainApiHandler, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addApiRoute(
fastify,
'POST',
getRouteName(__filename),
chainApiHandler,
{
description: "This method expects a transaction in JSON format and will attempt to apply it to the blockchain.",
summary: "This method expects a transaction in JSON format and will attempt to apply it to the blockchain.",
tags: ['chain'],
body: {
type: ['object', 'string'],
properties: {
signatures: {
"type": "array",
"description": "array of signatures required to authorize transaction",
"items": {$ref: 'Signature#'}
},
compression: {
"type": "boolean",
"description": "Compression used, usually false"
},
packed_context_free_data: {
"type": "string",
"description": "json to hex"
},
packed_trx: {
"type": "string",
"description": "Transaction object json to hex"
}
}
}
}
);
next();
}
@@ -1,55 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, chainApiHandler, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addApiRoute(
fastify,
'POST',
getRouteName(__filename),
chainApiHandler,
{
description: "This method expects a transaction in JSON format and will attempt to apply it to the blockchain.",
summary: "This method expects a transaction in JSON format and will attempt to apply it to the blockchain.",
tags: ['chain'],
body: {
type: ['array', 'object', 'string'],
items: {
type: ['object', 'string'],
additionalProperties: false,
minProperties: 8,
required: [
"expiration",
"ref_block_num",
"ref_block_prefix",
"max_net_usage_words",
"max_cpu_usage_ms",
"delay_sec",
"context_free_actions",
"actions"
],
properties: {
"expiration": {$ref: 'Expiration#'},
"ref_block_num": {"type": "integer"},
"ref_block_prefix": {"type": "integer"},
"max_net_usage_words": {$ref: 'WholeNumber#'},
"max_cpu_usage_ms": {$ref: 'WholeNumber#'},
"delay_sec": {"type": "integer"},
"context_free_actions": {
"type": "array",
"items": {$ref: 'ActionItems#'}
},
"actions": {
"type": "array",
"items": {$ref: 'ActionItems#'}
},
"transaction_extensions": {
"type": "array",
"items": {$ref: 'BlockExtensions#'}
}
},
}
}
}
);
next();
}
@@ -1,39 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, chainApiHandler, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
addApiRoute(
fastify,
'POST',
getRouteName(__filename),
chainApiHandler,
{
description: "This method expects a transaction in JSON format and will attempt to apply it to the blockchain.",
summary: "This method expects a transaction in JSON format and will attempt to apply it to the blockchain.",
tags: ['chain'],
body: {
type: ['object', 'string'],
properties: {
signatures: {
"type": "array",
"description": "array of signatures required to authorize transaction",
"items": {$ref: 'Signature#'}
},
compression: {
"type": "boolean",
"description": "Compression used, usually false"
},
packed_context_free_data: {
"type": "string",
"description": "json to hex"
},
packed_trx: {
"type": "string",
"description": "Transaction object json to hex"
}
}
}
}
);
next();
}
+11 -182
View File
@@ -1,166 +1,22 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {mergeActionMeta, timedQuery} from "../../../helpers/functions";
import {Serialize} from "eosjs";
import {hLog} from "../../../../helpers/common_functions";
import * as AbiEOS from "@eosrio/node-abieos";
import {ApiResponse} from "@elastic/elasticsearch";
import {TextDecoder, TextEncoder} from "util";
import {JsonRpc} from "eosjs/dist";
import {terms} from "../../v2-history/get_actions/definitions";
const abi_remapping = {
"_Bool": "bool"
};
const txEnc = new TextEncoder();
const txDec = new TextDecoder();
async function fetchAbiHexAtBlockElastic(client, chain, contract_name, last_block, get_json) {
try {
const _includes = ["actions", "tables", "block"];
if (get_json) {
_includes.push("abi");
} else {
_includes.push("abi_hex");
}
const query = {
bool: {
must: [
{term: {account: contract_name}},
{range: {block: {lte: last_block}}}
]
}
};
const queryResult: ApiResponse = await client.search({
index: `${chain}-abi-*`,
body: {
size: 1, query,
sort: [{block: {order: "desc"}}],
_source: {includes: _includes}
}
});
const results = queryResult.body.hits.hits;
if (results.length > 0) {
const nextRefResponse: ApiResponse = await client.search({
index: `${chain}-abi-*`,
body: {
size: 1,
query: {
bool: {
must: [
{term: {account: contract_name}},
{range: {block: {gte: last_block}}}
]
}
},
sort: [{block: {order: "asc"}}],
_source: {includes: ["block"]}
}
});
const nextRef = nextRefResponse.body.hits.hits;
if (nextRef.length > 0) {
return {
valid_until: nextRef[0]._source.block,
...results[0]._source
};
}
return results[0]._source;
} else {
return null;
}
} catch (e) {
hLog(e);
return null;
}
}
async function getAbiFromHeadBlock(rpc: JsonRpc, code) {
let _abi;
try {
_abi = (await rpc.get_abi(code)).abi;
} catch (e) {
hLog(e);
}
return {abi: _abi, valid_until: null, valid_from: null};
}
async function getContractAtBlock(esClient, rpc, chain, accountName: string, block_num: number, check_action?: string) {
let savedAbi, abi;
savedAbi = await fetchAbiHexAtBlockElastic(esClient, chain, accountName, block_num, true);
if (savedAbi === null || (savedAbi.actions && !savedAbi.actions.includes(check_action))) {
savedAbi = await getAbiFromHeadBlock(rpc, accountName);
if (!savedAbi) return [null, null];
abi = savedAbi.abi;
} else {
try {
abi = JSON.parse(savedAbi.abi);
} catch (e) {
hLog(e);
return [null, null];
}
}
if (!abi) return [null, null];
const initialTypes = Serialize.createInitialTypes();
let types;
try {
types = Serialize.getTypesFromAbi(initialTypes, abi);
} catch (e) {
let remapped = false;
for (const struct of abi.structs) {
for (const field of struct.fields) {
if (abi_remapping[field.type]) {
field.type = abi_remapping[field.type];
remapped = true;
}
}
}
if (remapped) {
try {
types = Serialize.getTypesFromAbi(initialTypes, abi);
} catch (e) {
hLog('failed after remapping abi');
hLog(accountName, block_num, check_action);
hLog(e);
}
} else {
hLog(accountName, block_num);
hLog(e);
}
}
const actions = new Map();
for (const {name, type} of abi.actions) {
actions.set(name, Serialize.getType(types, type));
}
const result = {types, actions, tables: abi.tables};
if (check_action) {
if (actions.has(check_action)) {
try {
AbiEOS['load_abi'](accountName, JSON.stringify(abi));
} catch (e) {
hLog(e);
}
}
}
return [result, abi];
}
const terms = ["notified", "act.authorization.actor"];
const extendedActions = new Set(["transfer", "newaccount", "updateauth"]);
async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
if (typeof request.body === 'string') {
request.body = JSON.parse(request.body);
}
const reqBody = request.body as any;
const reqBody = request.body;
const should_array = [];
for (const entry of terms) {
const tObj = {term: {}};
tObj.term[entry] = reqBody.account_name;
should_array.push(tObj);
}
let code, method, pos, offset;
let code, method, pos, offset, parent;
let sort_direction = 'desc';
let filterObj = [];
if (reqBody.filter) {
@@ -182,8 +38,8 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
}
pos = parseInt(reqBody.pos || 0, 10);
offset = parseInt(reqBody.offset || 20, 10);
let from = 0;
let size = 0;
let from, size;
from = size = 0;
if (pos === -1) {
if (offset < 0) {
from = 0;
@@ -272,7 +128,6 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
}
}
};
// console.log(JSON.stringify(esOpts, null, 2));
const pResults = await Promise.all([fastify.eosjs.rpc.get_info(), fastify.elastic['search'](esOpts)]);
const results = pResults[1];
const response = {
@@ -297,13 +152,11 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
}
actions = actions.slice(index);
}
for (let i = 0; i < actions.length; i++) {
let action = actions[i];
actions.forEach((action, index) => {
action = action._source;
let act: any = {
"global_action_seq": action.global_sequence,
"account_action_seq": i,
"account_action_seq": index,
"block_num": action.block_num,
"block_time": action['@timestamp'],
"action_trace": {
@@ -319,31 +172,7 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
};
mergeActionMeta(action);
act.action_trace.act = action.act;
if (reqBody.hex_data) {
try {
const [contract, _] = await getContractAtBlock(
fastify.elastic,
fastify.eosjs.rpc,
fastify.manager.chain,
action.act.account,
action.block_num
);
action.act.hex_data = Serialize.serializeActionData(
contract,
action.act.account,
action.act.name,
action.act.data,
txEnc,
txDec
);
} catch (e: any) {
console.log(e);
}
}
// TODO: Optionally re-encode using the original ABI, will increase query time
// act.action_trace.act.hex_data = Buffer.from(flatstr(JSON.stringify(action.act.data))).toString('hex');
act.action_trace.act.hex_data = Buffer.from(JSON.stringify(action.act.data)).toString('hex');
if (action.act.account_ram_deltas) {
act.action_trace.account_ram_deltas = action.account_ram_deltas
}
@@ -353,13 +182,13 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
act.action_trace.receiver = receipt.receiver;
act.account_action_seq = receipt['recv_sequence'];
response.actions.push(act);
}
});
}
return response;
}
export function getActionsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getActions, fastify, request, route));
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ export default function (fastify: FastifyInstance, opts: any, next) {
addApiRoute(fastify, 'POST', getRouteName(__filename), getActionsHandler, {
description: 'legacy get actions query',
summary: 'get actions',
tags: ['history'],
tags: ['actions', 'history'],
body: {
type: ['object', 'string'],
properties: {
@@ -1,72 +1,72 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {timedQuery} from "../../../helpers/functions";
import {ApiResponse} from "@elastic/elasticsearch";
async function getControlledAccounts(fastify: FastifyInstance, request: FastifyRequest) {
if (typeof request.body === 'string') {
request.body = JSON.parse(request.body)
}
let controlling_account = request.body["controlling_account"];
const results: ApiResponse = await fastify.elastic.search({
index: fastify.manager.chain + '-action-*',
size: 100,
body: {
query: {
bool: {
should: [
{
term: {"@updateauth.auth.accounts.permission.actor": controlling_account}
},
{
bool: {
must: [
{term: {"act.account": "eosio"}},
{term: {"act.name": "newaccount"}},
{term: {"act.authorization.actor": controlling_account}}
]
}
}
],
minimum_should_match: 1
}
},
sort: [
{"global_sequence": {"order": "desc"}}
]
}
});
if (typeof request.body === 'string') {
request.body = JSON.parse(request.body)
}
let controlling_account = request.body.controlling_account;
const results: ApiResponse = await fastify.elastic.search({
index: fastify.manager.chain + '-action-*',
size: 100,
body: {
query: {
bool: {
should: [
{
term: {"@updateauth.auth.accounts.permission.actor": controlling_account}},
{
bool: {
must: [
{term: {"act.account": "eosio"}},
{term: {"act.name": "newaccount"}},
{term: {"act.authorization.actor": controlling_account}}
]
}
}
],
minimum_should_match: 1
}
},
sort: [
{"global_sequence": {"order": "desc"}}
]
}
});
const response = {
controlled_accounts: []
};
const response = {
controlled_accounts: []
};
const hits = results.body.hits.hits;
const hits = results.body.hits.hits;
if (hits.length > 0) {
response.controlled_accounts = hits.map((v) => {
if (v._source.act.name === 'newaccount') {
if (v._source['@newaccount'].newact) {
return v._source['@newaccount'].newact;
} else if (v._source.act.data.newact) {
return v._source.act.data.newact;
} else {
return null;
}
} else if (v._source.act.name === 'updateauth') {
return v._source.act.data.account;
} else {
return null;
}
});
}
if (response.controlled_accounts.length > 0) {
response.controlled_accounts = [...(new Set(response.controlled_accounts))];
}
return response;
if (hits.length > 0) {
response.controlled_accounts = hits.map((v) => {
if (v._source.act.name === 'newaccount') {
if (v._source['@newaccount'].newact) {
return v._source['@newaccount'].newact;
} else if (v._source.act.data.newact) {
return v._source.act.data.newact;
} else {
return null;
}
} else if (v._source.act.name === 'updateauth') {
return v._source.act.data.account;
} else {
return null;
}
});
}
if (response.controlled_accounts.length > 0) {
response.controlled_accounts = [...(new Set(response.controlled_accounts))];
}
return response;
}
export function getControlledAccountsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getControlledAccounts, fastify, request, route));
}
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getControlledAccounts, fastify, request, route));
}
}
@@ -6,7 +6,7 @@ export default function (fastify: FastifyInstance, opts: any, next) {
addApiRoute(fastify, 'POST', getRouteName(__filename), getControlledAccountsHandler, {
description: 'get controlled accounts by controlling accounts',
summary: 'get controlled accounts by controlling accounts',
tags: ['accounts'],
tags: ['state'],
body: {
type: ['object', 'string'],
properties: {
@@ -8,9 +8,9 @@ export default function (fastify: FastifyInstance, opts: any, next) {
addApiRoute(fastify, 'POST', getRouteName(__filename), getKeyAccountsHandler, {
description: 'get accounts by public key',
summary: 'get accounts by public key',
tags: ['accounts'],
tags: ['accounts', 'state'],
body: {
type: ['object', 'string'],
type: 'object',
properties: {"public_key": {description: 'public key', type: 'string'}},
required: ["public_key"]
},
@@ -1,25 +1,30 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {mergeActionMeta, timedQuery} from "../../../helpers/functions";
import {GetInfoResult} from "eosjs/dist/eosjs-rpc-interfaces";
import {createHash} from "crypto";
async function getTransaction(fastify: FastifyInstance, request: FastifyRequest) {
if (request.method === 'POST' && !request.body) {
throw new Error("missing POST body");
}
if (typeof request.body === 'string') {
request.body = JSON.parse(request.body)
}
const body: any = request.body;
const redis = fastify.redis;
const trxId = body.id.toLowerCase();
const conf = fastify.manager.config;
const cachedData = await redis.hgetall('trx_' + trxId);
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.body.id.toLowerCase()}}
]
}
},
"sort": {
"global_sequence": "asc"
}
}
})]);
const results = pResults[1];
const response: any = {
"id": body.id,
"id": request.body.id,
"trx": {
"receipt": {
"status": "executed",
@@ -36,89 +41,11 @@ async function getTransaction(fastify: FastifyInstance, request: FastifyRequest)
},
"block_num": 0,
"block_time": "",
"last_irreversible_block": undefined,
"last_irreversible_block": pResults[0].last_irreversible_block_num,
"traces": []
};
let hits;
// build get_info request with caching
const $getInfo = new Promise<GetInfoResult>(resolve => {
const key = `${fastify.manager.chain}_get_info`;
fastify.redis.get(key).then(value => {
if (value) {
resolve(JSON.parse(value));
} else {
fastify.eosjs.rpc.get_info().then(value1 => {
fastify.redis.set(key, JSON.stringify(value1), 'EX', 6);
resolve(value1);
}).catch((reason) => {
console.log(reason);
response.error = 'failed to get last_irreversible_block_num'
resolve(null);
});
}
});
});
// reconstruct hits from cached data
if (cachedData && Object.keys(cachedData).length > 0) {
const gsArr = [];
for (let cachedDataKey in cachedData) {
gsArr.push(cachedData[cachedDataKey]);
}
gsArr.sort((a, b) => {
return a.global_sequence - b.global_sequence;
});
hits = gsArr.map(value => {
return {
_source: JSON.parse(value)
};
});
const promiseResults = await Promise.all([
redis.ttl('trx_' + trxId),
$getInfo
]);
response.cache_expires_in = promiseResults[0];
response.last_irreversible_block = promiseResults[1].last_irreversible_block_num;
}
// search on ES if cache is not present
if (!hits) {
const _size = conf.api.limits.get_trx_actions || 100;
const blockHint = parseInt(body.block_num_hint, 10);
let indexPattern = '';
if (blockHint) {
const idxPart = Math.ceil(blockHint / conf.settings.index_partition_size).toString().padStart(6, '0');
indexPattern = fastify.manager.chain + `-action-${conf.settings.index_version}-${idxPart}`;
} else {
indexPattern = fastify.manager.chain + '-action-*';
}
let pResults;
try {
// build search request
const $search = fastify.elastic.search({
index: indexPattern,
size: _size,
body: {
query: {bool: {must: [{term: {trx_id: trxId}}]}},
sort: {global_sequence: "asc"}
}
});
// execute in parallel
pResults = await Promise.all([$getInfo, $search]);
} catch (e) {
console.log(e.message);
if (e.meta.statusCode === 404) {
return response;
}
}
hits = pResults[1]['body']['hits']['hits'];
response.last_irreversible_block = pResults[0].last_irreversible_block_num;
}
const hits = results['body']['hits']['hits'];
if (hits.length > 0) {
const actions = hits;
@@ -135,33 +62,108 @@ async function getTransaction(fastify: FastifyInstance, request: FastifyRequest)
"signatures": [],
"context_free_data": []
};
let traces = {};
let seqNum = 0;
for (let action of actions) {
action = action._source;
mergeActionMeta(action);
action.act['hex_data'] = Buffer.from(JSON.stringify(action.act.data)).toString('hex');
if (action.parent === 0) {
response.trx.trx.actions.push(action.act);
}
response.block_num = action.block_num;
response.block_time = action['@timestamp'];
for (const receipt of action.receipts) {
if (action.act_digest) {
receipt.act_digest = action.act_digest.toLowerCase();
seqNum += 1;
let trace = {
receipt: {
receiver: action.act.account,
global_sequence: String(action.global_sequence),
auth_sequence: [
action.act.authorization[0].actor,
seqNum
],
act_digest: '',
recv_sequence: seqNum,
code_sequence: 1,
abi_sequence: 1
},
act: action.act,
account_ram_deltas: action.account_ram_deltas || [],
context_free: false,
block_num: action.block_num,
block_time: action['@timestamp'],
console: "",
elapsed: 0,
except: null,
inline_traces: [],
producer_block_id: "",
trx_id: request.body.id,
notified: action.notified
};
let hash = createHash('sha256');
hash.update(JSON.stringify(action.act));
trace.receipt.act_digest = hash.digest('hex');
traces[action.global_sequence] = trace;
}
actions.forEach(action => {
action = action._source;
for (let i = 0; i < traces[action.global_sequence].notified.length; i++) {
if (traces[action.global_sequence].notified[i] === action.act.account) {
traces[action.global_sequence].notified.splice(i, 1);
break;
}
response.traces.push({
receipt: receipt,
act: action.act,
}
if (action.parent !== 0 && action.parent) {
if (traces[action.parent]) {
for (let i = 0; i < traces[action.parent].notified.length; i++) {
if (traces[action.parent].notified[i] === action.act.account) {
traces[action.parent].notified.splice(i, 1);
break;
}
}
traces[action.parent].inline_traces.push(traces[action.global_sequence]);
}
}
});
actions.forEach(action => {
action = action._source;
response.traces.push(traces[action.global_sequence]);
traces[action.global_sequence].notified.forEach((note, index) => {
seqNum += 1;
let trace = {
receipt: {
receiver: note,
global_sequence: String(action.global_sequence + index + 1),
auth_sequence: [
action.act.authorization[0].actor,
seqNum
],
act_digest: traces[action.global_sequence].receipt.act_digest,
recv_sequence: seqNum,
code_sequence: 1,
abi_sequence: 1
},
account_ram_deltas: action.account_ram_deltas || [],
context_free: false,
act: action.act,
block_num: action.block_num,
block_time: action['@timestamp'],
console: "",
context_free: false,
elapsed: 0,
except: null,
inline_traces: [],
producer_block_id: "",
trx_id: body.id
});
}
}
trx_id: request.body.id,
};
traces[action.global_sequence].inline_traces.unshift(trace);
response.traces.push(trace);
});
delete traces[action.global_sequence].notified;
});
} else {
const errmsg = "Transaction " + body.id.toLowerCase() + " not found in history and no block hint was given";
const errmsg = "Transaction " + request.body.id.toLowerCase() + " not found in history and no block hint was given";
return {
code: 500,
message: "Internal Service Error",
@@ -184,7 +186,7 @@ async function getTransaction(fastify: FastifyInstance, request: FastifyRequest)
}
export function getTransactionHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getTransaction, fastify, request, route));
}
}
@@ -6,13 +6,10 @@ export default function (fastify: FastifyInstance, opts: any, next) {
addApiRoute(fastify, 'POST', getRouteName(__filename), getTransactionHandler, {
description: 'get all actions belonging to the same transaction',
summary: 'get transaction by id',
tags: ['history'],
tags: ['transactions', 'history'],
body: {
type: ['object', 'string'],
properties: {
id: {description: 'transaction id', type: 'string'},
block_num_hint: {description: 'block number hint', type: 'integer'},
},
properties: {id: {description: 'transaction id', type: 'string'}},
required: ["id"]
}
});
-126
View File
@@ -1,126 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
import * as _ from "lodash";
interface getBlockTraceResponse {
id: string;
number: number;
previous_id: string;
status: string;
timestamp: string;
producer: string;
transactions: any[];
}
async function getBlockTrace(fastify: FastifyInstance, request: FastifyRequest) {
if (typeof request.body === 'string') {
request.body = JSON.parse(request.body);
}
const reqBody: any = request.body;
const targetBlock = parseInt(reqBody.block_num);
let searchBody;
if (reqBody.block_id) {
searchBody = {
query: {
bool: {
must: [
{term: {block_id: reqBody.block_id}}
]
}
}
};
} else {
if (targetBlock > 0) {
searchBody = {
query: {
bool: {
must: [
{term: {block_num: targetBlock}}
]
}
}
};
} else if (targetBlock < 0) {
searchBody = {
query: {match_all: {}},
sort: {block_num: "desc"}
}
}
}
const response = {transactions: []} as getBlockTraceResponse;
if (searchBody) {
const getBlockHeader = await fastify.elastic.search({
index: fastify.manager.chain + "-block-*",
size: 1,
body: searchBody
});
if (getBlockHeader.body.hits.hits.length === 1) {
const block = getBlockHeader.body.hits.hits[0]._source;
const info = await fastify.eosjs.rpc.get_info();
response.id = block.block_id;
response.number = block.block_num;
response.previous_id = block.prev_id;
response.status = info.last_irreversible_block_num > block.block_num ? "irreversible" : "pending"
response.timestamp = block['@timestamp'];
response.producer = block.producer;
// lookup all actions on block
const getActionsResponse = await fastify.elastic.search({
index: fastify.manager.chain + "-action-*",
size: fastify.manager.config.api.limits.get_actions || 1000,
body: {
query: {
bool: {
must: [
{term: {block_num: block.block_num}}
]
}
},
sort: {global_sequence: "asc"}
}
});
const hits = getActionsResponse.body.hits.hits;
if (hits.length > 0) {
_.forEach(_.groupBy(hits, (v) => v['_source']['trx_id']), (value, key) => {
const actArray = [];
for (const act of value) {
const action = act['_source']
for (const receipt of action.receipts) {
actArray.push({
receiver: receipt.receiver,
account: action.act.account,
action: action.act.name,
authorization: action.act.authorization.map(auth => {
return {account: auth.actor, permission: auth.permission};
}),
data: action.act.data
});
}
}
response.transactions.push({id: key, actions: actArray});
});
}
return response;
} else {
throw new Error('block not found!');
}
} else {
throw new Error("invalid block number or id");
}
}
export function getBlockTraceHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getBlockTrace, fastify, request, route));
}
}
-72
View File
@@ -1,72 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, extendResponseSchema, getRouteName} from "../../../helpers/functions";
import {getBlockTraceHandler} from "./get_block";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get block traces',
summary: 'get block traces',
tags: ['history'],
body: {
type: ['object', 'string'],
properties: {
"block_num": {
description: 'block number',
type: 'integer'
},
"block_id": {
description: 'block id',
type: 'string'
}
}
},
response: extendResponseSchema({
"id": {type: "string"},
"number": {type: "integer"},
"previous_id": {type: "string"},
"status": {type: "string"},
"timestamp": {type: "string"},
"producer": {type: "string"},
"transactions": {
type: "array",
items: {
type: 'object',
properties: {
"id": {type: "string"},
"actions": {
type: "array",
items: {
type: 'object',
properties: {
"receiver": {type: "string"},
"account": {type: "string"},
"action": {type: "string"},
"authorization": {
type: "array", items: {
type: 'object',
properties: {
"account": {type: "string"},
"permission": {type: "string"},
}
}
},
"data": {
additionalProperties: true
}
}
}
}
}
}
}
})
};
addApiRoute(
fastify,
'POST',
getRouteName(__filename),
getBlockTraceHandler,
schema
);
next();
}
@@ -1,33 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
async function checkTransaction(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const txId = query.id.toLowerCase();
const data = await fastify.redis.get(txId) as any;
if (data) {
const jsonObj = JSON.parse(data);
const response = {
id: txId,
status: jsonObj.status,
block_num: jsonObj.b,
root_action: jsonObj.a,
signatures: [],
};
if (jsonObj.s && jsonObj.s.length > 0) {
response.signatures = jsonObj.s;
}
return response;
} else {
return {
id: txId,
status: 'unknown'
};
}
}
export function checkTransactionHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(checkTransaction, fastify, request, route));
}
}
@@ -1,55 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, extendResponseSchema, getRouteName} from "../../../helpers/functions";
import {checkTransactionHandler} from "./check_transaction";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'check if a transaction was included in a block',
summary: 'check if a transaction was included in a block',
tags: ['history'],
querystring: {
type: 'object',
properties: {
"id": {
description: 'transaction id',
type: 'string',
minLength: 64,
maxLength: 64
}
},
required: ["id"]
},
response: extendResponseSchema({
"id": {type: "string"},
"status": {type: "string"},
"block_num": {type: "number"},
"root_action": {
type: "object",
properties: {
"account": {type: "string"},
"name": {type: "string"},
"authorization": {
type: "array",
items: {
type: 'object',
properties: {
"actor": {type: "string"},
"permission": {type: "string"}
}
}
},
"data": {type: "string"}
}
},
"signatures": {type: "array", items: {type: 'string'}}
})
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
checkTransactionHandler,
schema
);
next();
}
@@ -1,12 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
async function exportActions(fastify: FastifyInstance, request: FastifyRequest) {
}
export function exportActionsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(exportActions, fastify, request, route));
}
}
@@ -1,19 +0,0 @@
import {FastifyInstance, FastifySchema} from "fastify";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
import {getCreatorHandler} from "../get_creator/get_creator";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema: FastifySchema = {
description: 'request large action data export',
summary: 'request large action data export',
tags: ['history']
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getCreatorHandler,
schema
);
next();
}
@@ -1,51 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
async function getAbiSnapshot(fastify: FastifyInstance, request: FastifyRequest) {
const response = {
block_num: null
};
const query: any = request.query;
const code = query.contract;
const block = query.block;
const should_fetch = query.fetch;
const mustArray = [];
mustArray.push({"term": {"account": code}});
if (block) {
mustArray.push({"range": {"block": {"lte": parseInt(block)}}});
}
const results = await fastify.elastic.search({
index: fastify.manager.chain + '-abi-*',
size: 1,
body: {
query: {bool: {must: mustArray}},
sort: [{block: {order: "desc"}}]
}
});
if (results['body']['hits']['hits'].length > 0) {
if (should_fetch) {
response['abi'] = JSON.parse(results['body']['hits']['hits'][0]['_source']['abi']);
} else {
response['present'] = true;
}
response.block_num = results['body']['hits']['hits'][0]['_source']['block'];
} else {
response['present'] = false;
response['error'] = 'abi not found for ' + code + ' until block ' + block;
}
return response;
}
export function getAbiSnapshotHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getAbiSnapshot, fastify, request, route));
}
}
@@ -1,40 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
import {getAbiSnapshotHandler} from "./get_abi_snapshot";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'fetch contract abi at specific block',
summary: 'fetch abi at specific block',
tags: ['history'],
querystring: {
type: 'object',
properties: {
"contract": {
description: 'contract account',
type: 'string',
minLength: 1,
maxLength: 12
},
"block": {
description: 'target block',
type: 'integer',
minimum: 1
},
"fetch": {
description: 'should fetch the ABI',
type: 'boolean'
}
},
required: ["contract"]
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getAbiSnapshotHandler,
schema
);
next();
}
@@ -1,6 +1,5 @@
export const terms = [
"notified",
"receipts.receiver",
"notified.keyword",
"act.authorization.actor"
];
@@ -9,26 +8,17 @@ export const extendedActions = new Set([
"newaccount",
"updateauth",
"buyram",
"buyrambytes",
"delegatebw",
"undelegatebw",
"voteproducer"
"buyrambytes"
]);
export const primaryTerms = [
"notified",
"block_num",
"block_id",
"global_sequence",
"producer",
"@timestamp",
"creator_action_ordinal",
"action_ordinal",
"cpu_usage_us",
"net_usage_words",
"trx_id",
"receipts.receiver",
"receipts.global_sequence",
"receipts.recv_sequence",
"receipts.auth_sequence.account",
"receipts.auth_sequence.sequence"
"net_usage_words"
];
+41 -94
View File
@@ -1,4 +1,4 @@
import {primaryTerms, terms} from "./definitions";
import {extendedActions, primaryTerms, terms} from "./definitions";
export function addSortedBy(query, queryBody, sort_direction) {
if (query['sortedBy']) {
@@ -70,119 +70,68 @@ function addRangeQuery(queryStruct, prop, pkey, query) {
export function applyTimeFilter(query, queryStruct) {
if (query['after'] || query['before']) {
if (query['after']?.includes(' ')) {
query['after'] = query['after'].replace(' ', 'T');
}
if (query['before']?.includes(' ')) {
query['before'] = query['before'].replace(' ', 'T');
}
if (query['after']?.includes('T') || query['before']?.includes('T')) {
let _lte = "now";
let _gte = "0";
if (query['before']) {
try {
_lte = new Date(query['before']).toISOString();
} catch (e) {
throw new Error(e.message + ' [before]');
}
}
if (query['after']) {
try {
_gte = new Date(query['after']).toISOString();
} catch (e) {
throw new Error(e.message + ' [after]');
}
}
if (!queryStruct.bool['filter']) {
queryStruct.bool['filter'] = [];
}
queryStruct.bool['filter'].push({
range: {
"@timestamp": {
"gte": _gte,
"lte": _lte
}
}
});
} else {
// search by block number
const rangeObj = {
range: {
block_num: {}
}
};
if (parseInt(query['after']) > 0) {
rangeObj.range.block_num['gte'] = query['after'];
}
if (parseInt(query['before']) > 0) {
rangeObj.range.block_num['lte'] = query['before'];
}
if (Object.keys(rangeObj.range.block_num).length > 0) {
if (!queryStruct.bool['filter']) {
queryStruct.bool['filter'] = [];
}
queryStruct.bool['filter'].push(rangeObj);
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, allowedExtraParams: Set<string>) {
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 && allowedExtraParams) {
pkey = allowedExtraParams.has(pair[0]) ? "@" + prop : prop;
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 _qObj = {};
const _termQuery = {};
const parts = query[prop].split(",");
if (parts.length > 1) {
processMultiVars(queryStruct, parts, prop);
} else if (parts.length === 1) {
// @transfer.memo special case
if (pkey === '@transfer.memo') {
_qObj[pkey] = {
query: parts[0]
};
if (query.match_fuzziness) {
_qObj[pkey].fuzziness = query.match_fuzziness;
}
if (query.match_operator) {
_qObj[pkey].operator = query.match_operator;
}
queryStruct.bool.must.push({
match: _qObj
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 {
const andParts = parts[0].split(" ");
if (andParts.length > 1) {
andParts.forEach(value => {
const _q = {};
_q[pkey] = value;
queryStruct.bool.must.push({term: _q});
});
if (parts[0].startsWith("!")) {
_termQuery[pkey] = parts[0].replace("!", "");
queryStruct.bool.must_not.push({term: _termQuery});
} else {
if (parts[0].startsWith("!")) {
_qObj[pkey] = parts[0].replace("!", "");
queryStruct.bool.must_not.push({term: _qObj});
} else {
_qObj[pkey] = parts[0];
queryStruct.bool.must.push({term: _qObj});
}
_termQuery[pkey] = parts[0];
queryStruct.bool.must.push({term: _termQuery});
}
}
}
@@ -230,7 +179,7 @@ export function applyCodeActionFilters(query, queryStruct) {
}
}
export function getSkipLimit(query, max?: number) {
export function getSkipLimit(query) {
let skip, limit;
skip = parseInt(query.skip, 10);
if (skip < 0) {
@@ -239,8 +188,6 @@ export function getSkipLimit(query, max?: number) {
limit = parseInt(query.limit, 10);
if (limit < 1) {
throw new Error('invalid limit parameter');
} else if (limit > max) {
throw new Error(`limit too big, maximum: ${max}`);
}
return {skip, limit};
}
@@ -1,4 +1,5 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {getTrackTotalHits, mergeActionMeta, timedQuery} from "../../../helpers/functions";
import {
addSortedBy,
@@ -11,8 +12,7 @@ import {
} from "./functions";
async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const maxActions = fastify.manager.config.api.limits.get_actions;
const query = request.query;
const queryStruct = {
"bool": {
must: [],
@@ -21,18 +21,12 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
}
};
const {skip, limit} = getSkipLimit(query, maxActions);
const {skip, limit} = getSkipLimit(query);
const sort_direction = getSortDir(query);
applyAccountFilters(query, queryStruct);
applyGenericFilters(query, queryStruct, fastify.allowedActionQueryParamSet);
applyGenericFilters(query, queryStruct);
applyTimeFilter(query, queryStruct);
applyCodeActionFilters(query, queryStruct);
// allow precise counting of total hits
const trackTotalHits = getTrackTotalHits(query);
@@ -46,37 +40,24 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
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
})
]);
let indexPattern = fastify.manager.chain + '-action-*';
if (query.hot_only) {
indexPattern = fastify.manager.chain + '-action';
}
const esOpts = {
"index": indexPattern,
"from": skip || 0,
"size": (limit > maxActions ? maxActions : limit) || 10,
"body": query_body
};
// console.log(JSON.stringify(esOpts, null, 2));
const esResults = await fastify.elastic.search(esOpts);
const results = esResults['body']['hits'];
const results = pResults[1]['body']['hits'];
const response: any = {
cached: false,
lib: 0,
lib: pResults[0].last_irreversible_block_num,
total: results['total']
};
if (query.hot_only) {
response.hot_only = true;
}
if (query.checkLib) {
response.lib = (await fastify.eosjs.rpc.get_info()).last_irreversible_block_num;
}
if (query.simple) {
response['simple_actions'] = [];
} else {
@@ -85,44 +66,21 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
if (results['hits'].length > 0) {
const actions = results['hits'];
for (let action of actions.map(a => a._source)) {
try {
if (action.act.data) {
if (action.act.data.account && action.act.data.name && action.act.data.authorization) {
action.act.data = action.act.data.data;
}
}
} catch (e: any) {
console.log(e);
}
for (let action of actions) {
action = action._source;
mergeActionMeta(action);
if (query.noBinary === true) {
for (const key in action['act']['data']) {
if (action['act']['data'].hasOwnProperty(key)) {
if (typeof action['act']['data'][key] === 'string' && action['act']['data'][key].length > 256) {
action['act']['data'][key] = action['act']['data'][key].slice(0, 32) + "...";
}
}
}
}
if (query.simple) {
let notified = new Set(action.receipts.map(r => r.receiver));
response.simple_actions.push({
block: action['block_num'],
irreversible: response.lib !== 0 ? action['block_num'] < response.lib : undefined,
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: [...notified].join(','),
notified: action['notified'].join(','),
contract: action['act']['account'],
action: action['act']['name'],
data: action['act']['data']
});
} else {
response.actions.push(action);
}
@@ -132,7 +90,7 @@ async function getActions(fastify: FastifyInstance, request: FastifyRequest) {
}
export function getActionsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getActions, fastify, request, route));
}
}
+40 -83
View File
@@ -1,79 +1,13 @@
import {FastifyInstance, FastifySchema} from "fastify";
import {FastifyInstance, RouteSchema} from "fastify";
import {getActionsHandler} from "./get_actions";
import {addApiRoute, extendQueryStringSchema, extendResponseSchema, getRouteName} from "../../../helpers/functions";
export const getActionResponseSchema = {
"@timestamp": {type: "string"},
"timestamp": {type: "string"},
"block_num": {type: "number"},
"block_id": {type: "string"},
"trx_id": {type: "string"},
"act": {
type: 'object',
properties: {
"account": {type: "string"},
"name": {type: "string"},
"authorization": {
type: "array",
items: {
type: "object",
properties: {
"actor": {type: "string"},
"permission": {type: "string"},
}
}
}
},
additionalProperties: true
},
"receipts": {
type: "array",
items: {
type: "object",
properties: {
"receiver": {type: "string"},
"global_sequence": {type: "number"},
"recv_sequence": {type: "number"},
"auth_sequence": {
type: "array",
items: {
type: "object",
properties: {
"account": {type: "string"},
"sequence": {type: "number"},
}
}
}
}
}
},
"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"},
"producer": {type: "string"},
"parent": {type: "number"},
"action_ordinal": {type: 'number'},
"creator_action_ordinal": {type: 'number'},
"signatures": {type: "array", items: {type: 'string'}}
};
export default function (fastify: FastifyInstance, opts: any, next) {
const schema: FastifySchema = {
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: ['history'],
tags: ['actions', 'history'],
querystring: extendQueryStringSchema({
"account": {
description: 'notified account',
@@ -106,19 +40,7 @@ export default function (fastify: FastifyInstance, opts: any, next) {
"simple": {
description: 'simplified output mode',
type: 'boolean'
},
"hot_only": {
description: 'search only the latest hot index',
type: 'boolean'
},
"noBinary": {
description: "exclude large binary data",
type: 'boolean'
},
"checkLib": {
description: "perform reversibility check",
type: 'boolean'
},
}
}),
response: extendResponseSchema({
"simple_actions": {
@@ -144,7 +66,42 @@ export default function (fastify: FastifyInstance, opts: any, next) {
type: "array",
items: {
type: 'object',
properties: getActionResponseSchema
properties: {
"@timestamp": {type: "string"},
"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'}
}
}
}
})
@@ -1,58 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
import {getSkipLimit} from "../get_actions/functions";
async function getCreatedAccounts(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const {skip, limit} = getSkipLimit(query);
const maxActions = fastify.manager.config.api.limits.get_created_accounts;
const results = await fastify.elastic.search({
"index": fastify.manager.chain + '-action-*',
"from": skip || 0,
"size": (limit > maxActions ? maxActions : limit) || 100,
"body": {
"query": {
"bool": {
must: [
{term: {"act.authorization.actor": query.account.toLowerCase()}},
{term: {"act.name": "newaccount"}},
{term: {"act.account": "eosio"}}
]
}
},
sort: {
"global_sequence": "desc"
}
}
});
const response = {accounts: []};
if (results['body']['hits']['hits'].length > 0) {
const actions = results['body']['hits']['hits'];
for (let action of actions) {
action = action._source;
const _tmp = {};
if (action['act']['data']['newact']) {
_tmp['name'] = action['act']['data']['newact'];
} else if (action['@newaccount']['newact']) {
_tmp['name'] = action['@newaccount']['newact'];
} else {
console.log(action);
}
_tmp['trx_id'] = action['trx_id'];
_tmp['timestamp'] = action['@timestamp'];
response.accounts.push(_tmp);
}
}
return response;
}
export function getCreatedAccountsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getCreatedAccounts, fastify, request, route));
}
}
@@ -1,44 +0,0 @@
import {FastifyInstance, FastifySchema} from "fastify";
import {getCreatedAccountsHandler} from "./get_created_accounts";
import {addApiRoute, extendQueryStringSchema, extendResponseSchema, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema: FastifySchema = {
description: 'get all accounts created by one creator',
summary: 'get created accounts',
tags: ['accounts'],
querystring: extendQueryStringSchema({
"account": {
description: 'creator account',
type: 'string',
minLength: 1,
maxLength: 12
}
}, ["account"]),
response: extendResponseSchema({
"query_time": {
type: "number"
},
"total": {
type: "object",
properties: {
"value": {type: "number"},
"relation": {type: "string"}
}
},
"accounts": {
type: "array",
items: {
type: 'object',
properties: {
'name': {type: 'string'},
'timestamp': {type: 'string'},
'trx_id': {type: 'string'}
}
}
}
})
};
addApiRoute(fastify, 'GET', getRouteName(__filename), getCreatedAccountsHandler, schema);
next();
}
@@ -1,106 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
async function getCreator(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const response = {
account: query.account,
creator: '',
timestamp: '',
block_num: 0,
trx_id: '',
};
if (query.account === fastify.manager.config.settings.eosio_alias) {
try {
const genesisBlock = await fastify.eosjs.rpc.get_block(1);
if (genesisBlock) {
response.timestamp = genesisBlock.timestamp;
}
} catch (e) {
console.log(e.message);
}
response.creator = '__self__';
response.block_num = 1;
response.trx_id = "";
return response;
}
const results = await fastify.elastic.search({
"index": fastify.manager.chain + '-action-*',
"body": {
size: 1,
query: {
bool: {
must: [{term: {"@newaccount.newact": query.account}}]
}
}
}
});
if (results['body']['hits']['hits'].length === 1) {
const result = results['body']['hits']['hits'][0]._source;
response.trx_id = result.trx_id;
response.block_num = result.block_num;
response.creator = result.act.data.creator;
response.timestamp = result['@timestamp'];
return response;
} else {
let accountInfo;
try {
accountInfo = await fastify.eosjs.rpc.get_account(query.account);
} catch (e) {
throw new Error("account not found");
}
if (accountInfo) {
try {
response.timestamp = accountInfo.created;
const blockHeader = await fastify.elastic.search({
"index": fastify.manager.chain + '-block-*',
"body": {
size: 1,
query: {
bool: {
must: [{term: {"@timestamp": response.timestamp}}]
}
}
}
});
const hits = blockHeader['body']['hits']['hits'];
if (hits.length > 0 && hits[0]._source) {
const blockId = blockHeader['body']['hits']['hits'][0]._source.block_id;
const blockData = await fastify.eosjs.rpc.get_block(blockId);
response.block_num = blockData.block_num;
for (const transaction of blockData["transactions"]) {
if (typeof transaction.trx !== 'string') {
const actions = transaction.trx.transaction.actions;
for (const act of actions) {
if (act.name === 'newaccount') {
if (act.data.name === query.account) {
response.creator = act.data.creator;
response.trx_id = transaction.id;
return response;
}
}
}
}
}
}
return response;
} catch (e) {
console.log(e.message);
throw new Error("account creation not found");
}
} else {
throw new Error("account not found");
}
}
}
export function getCreatorHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getCreator, fastify, request, route));
}
}
@@ -1,45 +0,0 @@
import {FastifyInstance, FastifySchema} from "fastify";
import {getCreatorHandler} from "./get_creator";
import {addApiRoute, extendResponseSchema, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema: FastifySchema = {
description: 'get account creator',
summary: 'get account creator',
tags: ['accounts'],
querystring: {
type: 'object',
properties: {
"account": {
description: 'created account',
type: 'string',
minLength: 1,
maxLength: 12
}
},
required: ["account"]
},
response: extendResponseSchema({
"account": {
type: "string"
},
"creator": {
type: "string"
},
"timestamp": {
type: "string"
},
"block_num": {
type: "integer"
},
"trx_id": {
type: "string"
},
"indirect_creator": {
type: "string"
}
})
};
addApiRoute(fastify, 'GET', getRouteName(__filename), getCreatorHandler, schema);
next();
}
+12 -35
View File
@@ -1,15 +1,14 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {mergeDeltaMeta, timedQuery} from "../../../helpers/functions";
import {applyTimeFilter} from "../get_actions/functions";
async function getDeltas(fastify: FastifyInstance, request: FastifyRequest) {
let skip, limit;
let sort_direction = 'desc';
const mustArray = [];
const query: any = request.query;
for (const param in query) {
if (Object.prototype.hasOwnProperty.call(query, param)) {
const value = query[param];
for (const param in request.query) {
if (Object.prototype.hasOwnProperty.call(request.query, param)) {
const value = request.query[param];
switch (param) {
case 'limit': {
limit = parseInt(value, 10);
@@ -35,31 +34,13 @@ async function getDeltas(fastify: FastifyInstance, request: FastifyRequest) {
}
break;
}
case 'before': {
break;
}
case 'after': {
break;
}
default: {
if (typeof value === 'string') {
const values = query[param].split(",");
if (values.length > 1) {
const terms = {};
terms[param] = values;
mustArray.push({terms: terms});
} else {
const term = {};
term[param] = values[0];
mustArray.push({term: term});
}
} else {
if (typeof value === 'number') {
const term = {};
term[param] = value;
mustArray.push({term: term});
}
}
const values = request.query[param].split(",");
const terms = {};
terms[param] = values;
const shouldArray = {terms: terms};
const boolStruct = {bool: {should: [shouldArray]}};
mustArray.push(boolStruct);
break;
}
}
@@ -67,16 +48,12 @@ async function getDeltas(fastify: FastifyInstance, request: FastifyRequest) {
}
const maxDeltas = fastify.manager.config.api.limits.get_deltas ?? 1000;
const queryStruct = {bool: {must: mustArray}};
applyTimeFilter(query, queryStruct);
const results = await fastify.elastic.search({
"index": fastify.manager.chain + '-delta-*',
"from": skip || 0,
"size": (limit > maxDeltas ? maxDeltas : limit) || 10,
"body": {
query: queryStruct,
query: {bool: {must: mustArray}},
sort: {
"block_num": sort_direction
}
@@ -93,7 +70,7 @@ async function getDeltas(fastify: FastifyInstance, request: FastifyRequest) {
}
export function getDeltasHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getDeltas, fastify, request, route));
}
}
+4 -17
View File
@@ -1,9 +1,9 @@
import {FastifyInstance, FastifySchema} from "fastify";
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: FastifySchema = {
const schema: RouteSchema = {
description: 'get state deltas',
summary: 'get state deltas',
tags: ['history'],
@@ -23,19 +23,7 @@ export default function (fastify: FastifyInstance, opts: any, next) {
"payer": {
description: 'payer account',
type: 'string'
},
"after": {
description: 'filter after specified date (ISO8601)',
type: 'string'
},
"before": {
description: 'filter before specified date (ISO8601)',
type: 'string'
},
"present": {
description: 'delta present flag',
type: 'number'
},
}
}),
response: extendResponseSchema({
"deltas": {
@@ -44,14 +32,13 @@ export default function (fastify: FastifyInstance, opts: any, next) {
type: 'object',
properties: {
"timestamp": {type: 'string'},
"present": {type: 'number'},
"code": {type: 'string'},
"scope": {type: 'string'},
"table": {type: 'string'},
"primary_key": {type: 'string'},
"payer": {type: 'string'},
"present": {type: 'boolean'},
"block_num": {type: 'number'},
"block_id": {type: 'string'},
"data": {
type: 'object',
additionalProperties: true
@@ -1,105 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
import {createHash} from "crypto";
import {base58ToBinary, binaryToBase58} from "eosjs/dist/eosjs-numeric";
import {Search} from "@elastic/elasticsearch/api/requestParams";
function convertToLegacyKey(block_signing_key: string) {
if (block_signing_key.startsWith("PUB_K1_")) {
const buf = base58ToBinary(37, block_signing_key.substr(7));
const data = buf.slice(0, buf.length - 4);
const merged = Buffer.concat([
data,
createHash('ripemd160')
.update(data)
.digest()
.slice(0, 4)
]);
return "EOS" + binaryToBase58(merged);
} else {
return block_signing_key;
}
}
async function getSchedule(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const response: any = {
producers: []
};
if (query.mode === 'activated') {
const searchParams: Search<any> = {
track_total_hits: true,
index: fastify.manager.chain + "-block-*",
size: 1,
body: {
query: {bool: {must: []}},
sort: {block_num: query.sort || "desc"}
}
};
if (query.version) {
searchParams.body.query.bool.must.push({"term": {"new_producers.version": {"value": query.version}}});
} else {
searchParams.body.query.bool.must.push({"exists": {"field": "new_producers.version"}});
}
const apiResponse = await fastify.elastic.search(searchParams);
const results = apiResponse.body.hits.hits;
if (results && results.length > 0) {
response.timestamp = results[0]._source["@timestamp"];
response.block_num = results[0]._source.block_num;
response.version = results[0]._source.new_producers.version;
response.producers = results[0]._source.new_producers.producers.map(prod => {
return {
...prod,
legacy_key: convertToLegacyKey(prod.block_signing_key)
}
});
}
} else {
// default "proposed" mode
const searchParams: Search<any> = {
track_total_hits: true,
index: fastify.manager.chain + "-schedule-*",
size: 1,
body: {
query: {bool: {must: []}},
sort: {version: query.sort || "desc"}
}
};
if (query.version) {
searchParams.body.query.bool.must.push({"term": {"version": {"value": query.version}}});
} else {
searchParams.body.query.bool.must.push({"exists": {"field": "version"}});
}
if (query.producer) {
searchParams.body.query.bool.must.push({"term": {"producers.name": {"value": query.producer}}});
}
if (query.key) {
searchParams.body.query.bool.must.push({"term": {"producers.keys": {"value": query.key}}});
}
const apiResponse = await fastify.elastic.search(searchParams);
const results = apiResponse.body.hits.hits;
if (results && results.length > 0) {
response.timestamp = results[0]._source["@timestamp"];
response.proposal_block_num = results[0]._source.block_num;
response.version = results[0]._source.version;
response.producers = results[0]._source.producers;
// sort producers by name
// response.producers.sort((a, b) => {
// if (a.name < b.name) {
// return -1;
// }
// if (a.name > b.name) {
// return 1;
// }
// return 0;
// });
}
}
return response;
}
export function getScheduleHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getSchedule, fastify, request, route));
}
}
@@ -1,68 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, extendResponseSchema, getRouteName} from "../../../helpers/functions";
import {getScheduleHandler} from "./get_schedule";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get producer schedule by version',
summary: 'get producer schedule by version',
tags: ['history'],
querystring: {
type: 'object',
properties: {
"producer": {
description: 'search by producer',
type: 'string'
},
"key": {
description: 'search by signing key',
type: 'string'
},
"after": {
description: 'filter after specified date (ISO8601)',
type: 'string'
},
"before": {
description: 'filter before specified date (ISO8601)',
type: 'string'
},
"mode": {
description: 'search mode (activated or proposed)',
type: 'string',
},
"version": {
description: 'schedule version',
type: 'integer',
minimum: 1
}
}
},
response: extendResponseSchema({
"timestamp": {type: "string"},
"block_num": {type: "number"},
"proposed_block_num": {type: "number"},
"version": {type: "number"},
"producers": {
type: "array",
items: {
type: "object",
properties: {
"producer_name": {type: "string"},
"name": {type: "string"},
"block_signing_key": {type: "string"},
"legacy_key": {type: "string"},
"keys": {type: "array", items: {type: "string"}}
}
}
},
})
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getScheduleHandler,
schema
);
next();
}
@@ -1,88 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {mergeDeltaMeta, timedQuery} from "../../../helpers/functions";
async function getTableState(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const response = {
...query,
results: [],
next_key: null
};
const mustArray: any[] = [
{"term": {"code": query.code}},
{"term": {"table": query.table}}
];
if (query.block_num) {
mustArray.push({"range": {"block_num": {"lte": query.block_num}}});
}
let after_key = "";
if (query.after_key) {
after_key = query.after_key;
}
const results = await fastify.elastic.search({
index: fastify.manager.chain + '-delta-*',
body: {
"query": {
"bool": {
"must": mustArray
}
},
"size": 0,
"sort": [
{"block_num": "desc"},
{"primary_key": "desc"}
],
"aggs": {
"scope_buckets": {
"composite": {
"size": 25,
"after": {
"scope_pk": after_key
},
"sources": [
{
"scope_pk": {
"terms": {
"script": {
"source": "return doc['scope'].value + '-' + doc['primary_key'].value",
"lang": "painless"
}
}
}
}
]
},
"aggs": {
"last_doc": {
"top_hits": {
"size": 1,
"sort": [{"block_num": "desc"}],
"_source": {
"excludes": ["code"]
}
}
}
}
}
}
}
});
const scope_buckets = results.body.aggregations.scope_buckets;
if (scope_buckets && scope_buckets.buckets.length > 0) {
if (scope_buckets.after_key) {
response.next_key = scope_buckets.after_key.scope_pk;
}
response.results = scope_buckets.buckets.map(bucket => {
const row = mergeDeltaMeta(bucket.last_doc.hits.hits[0]._source);
delete row.table;
return row;
});
}
return response;
}
export function getTableStateHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getTableState, fastify, request, route));
}
}
@@ -1,69 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, extendResponseSchema, getRouteName} from "../../../helpers/functions";
import {getTableStateHandler} from "./get_table_state";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get table state at a specific block height',
summary: 'get table state at a specific block height',
tags: ['history'],
querystring: {
type: 'object',
required: ["code","table"],
properties: {
"code": {
description: 'search by contract',
type: 'string'
},
"table": {
description: 'search by key',
type: 'string'
},
"block_num": {
description: 'target block',
type: 'integer',
minimum: 1
},
"after_key": {
description: 'last key for pagination',
type: 'string'
}
}
},
response: extendResponseSchema({
"code": {type: 'string'},
"table": {type: 'string'},
"block_num": {type: 'number'},
"after_key": {type: 'string'},
"next_key": {type: 'string'},
"results": {
type: "array",
items: {
type: 'object',
properties: {
"scope": {type: 'string'},
"primary_key": {type: 'string'},
"payer": {type: 'string'},
"timestamp": {type: 'string'},
"block_num": {type: 'number'},
"block_id": {type: 'string'},
"present": {type: 'number'},
"data": {
type: 'object',
additionalProperties: true
}
},
additionalProperties: true
}
}
})
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getTableStateHandler,
schema
);
next();
}
@@ -1,128 +1,45 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {mergeActionMeta, timedQuery} from "../../../helpers/functions";
import {GetInfoResult} from "eosjs/dist/eosjs-rpc-interfaces";
async function getTransaction(fastify: FastifyInstance, request: FastifyRequest) {
const redis = fastify.redis;
const query: any = request.query;
const trxId = query.id.toLowerCase();
const conf = fastify.manager.config;
const cachedData = await redis.hgetall('trx_' + trxId);
const response = {
query_time_ms: undefined,
executed: false,
cached: undefined,
cache_expires_in: undefined,
trx_id: query.id,
lib: undefined,
cached_lib: false,
actions: undefined,
generated: undefined,
error: undefined
};
let hits;
// build get_info request with caching
const $getInfo = new Promise<GetInfoResult>(resolve => {
const key = `${fastify.manager.chain}_get_info`;
fastify.redis.get(key).then(value => {
if (value) {
response.cached_lib = true;
resolve(JSON.parse(value));
} else {
fastify.eosjs.rpc.get_info().then(value1 => {
fastify.redis.set(key, JSON.stringify(value1), 'EX', 6);
response.cached_lib = false;
resolve(value1);
}).catch((reason) => {
console.log(reason);
response.error = 'failed to get last_irreversible_block_num'
resolve(null);
});
}
});
});
// reconstruct hits from cached data
if (cachedData && Object.keys(cachedData).length > 0) {
const gsArr = [];
for (let cachedDataKey in cachedData) {
gsArr.push(cachedData[cachedDataKey]);
}
gsArr.sort((a, b) => {
return a.global_sequence - b.global_sequence;
});
hits = gsArr.map(value => {
return {
_source: JSON.parse(value)
};
});
const promiseResults = await Promise.all([
redis.ttl('trx_' + trxId),
$getInfo
]);
response.cache_expires_in = promiseResults[0];
response.lib = promiseResults[1].last_irreversible_block_num;
}
// search on ES if cache is not present
if (!hits) {
const _size = conf.api.limits.get_trx_actions || 100;
const blockHint = parseInt(query.block_hint, 10);
let indexPattern = '';
if (blockHint) {
const idxPart = Math.ceil(blockHint / conf.settings.index_partition_size).toString().padStart(6, '0');
indexPattern = fastify.manager.chain + `-action-${conf.settings.index_version}-${idxPart}`;
} else {
indexPattern = fastify.manager.chain + '-action-*';
}
let pResults;
try {
// build search request
const $search = fastify.elastic.search({
index: indexPattern,
size: _size,
body: {
query: {bool: {must: [{term: {trx_id: trxId}}]}},
sort: {global_sequence: "asc"}
}
});
// execute in parallel
pResults = await Promise.all([$getInfo, $search]);
} catch (e) {
console.log(e.message);
if (e.meta.statusCode === 404) {
response.error = 'no data near block_hint'
return response;
}
}
hits = pResults[1]['body']['hits']['hits'];
response.lib = pResults[0].last_irreversible_block_num;
}
if (hits.length > 0) {
let highestBlockNum = 0;
for (let action of hits) {
if (action._source.block_num > highestBlockNum) {
highestBlockNum = action._source.block_num;
}
}
response.actions = [];
for (let action of hits) {
if (action._source.block_num === highestBlockNum) {
mergeActionMeta(action._source);
response.actions.push(action._source);
}
}
response.executed = true;
}
return response;
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) => {
reply.send(await timedQuery(getTransaction, fastify, request, route));
}
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getTransaction, fastify, request, route));
}
}
@@ -6,17 +6,13 @@ 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: ['history'],
tags: ['transactions', 'history'],
querystring: {
type: 'object',
properties: {
id: {
"id": {
description: 'transaction id',
type: 'string'
},
block_hint: {
description: 'block hint to speed up tx recovery',
type: 'integer'
}
},
required: ["id"]
@@ -1,128 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {mergeActionMeta, timedQuery} from "../../../helpers/functions";
async function getTransaction(fastify: FastifyInstance, request: FastifyRequest) {
const _size = fastify.manager.config.api.limits.get_trx_actions || 100;
const query: any = request.query;
let indexPattern = fastify.manager.chain + '-action-*';
if (query.hot_only) {
indexPattern = fastify.manager.chain + '-action';
}
const pResults = await Promise.all([
fastify.eosjs.rpc.get_info(),
fastify.elastic.search({
index: indexPattern,
size: _size,
body: {
query: {
bool: {
must: [
{term: {trx_id: query.id.toLowerCase()}}
]
}
},
sort: {
global_sequence: "asc"
}
}
}),
fastify.elastic.search({
index: fastify.manager.chain + '-gentrx-*',
size: _size,
body: {
query: {
bool: {
must: [
{term: {trx_id: query.id.toLowerCase()}}
]
}
}
}
})
]);
const results = pResults[1];
const genTrxRes = pResults[2];
const response = {
"executed": false,
"hot_only": false,
"trx_id": query.id,
"lib": pResults[0].last_irreversible_block_num,
"actions": [],
"generated": undefined
};
if (query.hot_only) {
response.hot_only = true;
}
const hits = results['body']['hits']['hits'];
if (hits.length > 0) {
// const producers = {};
// for (let hit of hits) {
// if (hit._source.producer) {
// if (producers[hit._source.producer]) {
// producers[hit._source.producer]++;
// } else {
// producers[hit._source.producer] = 1;
// }
// }
// }
// let useBlockNumber;
// if (Object.keys(producers).length > 1) {
// // multiple producers of the same tx id, forked actions are present, attempt to clean-up
// let trueProd = '';
// let highestActCount = 0;
// for (const prod in producers) {
// if (producers.hasOwnProperty(prod)) {
// if(producers[prod] === highestActCount) {
// useBlockNumber = true;
// } else if (producers[prod] > highestActCount) {
// highestActCount = producers[prod];
// trueProd = prod;
// }
// }
// }
// }
let highestBlockNum = 0;
for (let action of hits) {
if (action._source.block_num > highestBlockNum) {
highestBlockNum = action._source.block_num;
}
}
for (let action of hits) {
if (action._source.block_num === highestBlockNum) {
action = action._source;
mergeActionMeta(action);
response.actions.push(action);
}
}
response.executed = true;
}
const hits2 = genTrxRes['body']['hits']['hits'];
if (hits2 && hits2.length > 0) {
if (hits2[0]._source['@timestamp']) {
hits2[0]._source['timestamp'] = hits2[0]._source['@timestamp'];
delete hits2[0]._source['@timestamp'];
}
response.generated = hits2[0]._source;
}
return response;
}
export function getTransactionHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getTransaction, fastify, request, route));
}
}
@@ -1,29 +0,0 @@
import {FastifyInstance} from "fastify";
import {getTransactionHandler} from "./get_transaction_legacy";
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: ['history'],
querystring: {
type: 'object',
properties: {
"id": {
description: 'transaction id',
type: 'string'
}
},
required: ["id"]
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getTransactionHandler,
schema
);
next();
}
+11 -22
View File
@@ -1,53 +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 query: any = request.query;
const response = {
query_time: null,
cached: false,
account: null,
actions: null,
total_actions: 0,
tokens: null,
links: null
};
const account = query.account;
const account = request.query.account;
const reqQueue = [];
try {
response.account = await fastify.eosjs.rpc.get_account(account);
} catch (e) {
throw new Error("Account not found!");
}
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';
const getLinksApi = localApi + '/state/get_links';
// fetch recent actions
reqQueue.push(got.get(`${getActionsApi}?account=${account}&limit=20&noBinary=true&track=true`).json());
reqQueue.push(got.get(`${getActionsApi}?account=${account}&limit=10`));
// fetch account tokens
reqQueue.push(got.get(`${getTokensApi}?account=${account}`).json());
// fetch account permission links
reqQueue.push(got.get(`${getLinksApi}?account=${account}`).json());
reqQueue.push(got.get(`${getTokensApi}?account=${account}`));
const results = await Promise.all(reqQueue);
response.actions = results[0].actions;
response.total_actions = results[0].total.value;
response.tokens = results[1].tokens;
response.links = results[2].links;
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) => {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getAccount, fastify, request, route));
}
}
+10 -47
View File
@@ -1,58 +1,21 @@
import {FastifyInstance} from "fastify";
import {getAccountHandler} from "./get_account";
import {addApiRoute, extendQueryStringSchema, extendResponseSchema, getRouteName} from "../../../helpers/functions";
import {getActionResponseSchema} from "../../v2-history/get_actions";
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'],
querystring: extendQueryStringSchema({
"account": {
description: 'account name',
type: 'string',
minLength: 1,
maxLength: 12
tags: ['accounts', 'state'],
querystring: {
type: 'object',
properties: {
"account": {
description: 'account name',
type: 'string'
}
}
}, ["account"]),
response: extendResponseSchema({
"account": {
additionalProperties: true
},
"links": {
type: "array",
items: {
type: 'object',
properties: {
"timestamp": {type: "string"},
"permission": {type: "string"},
"code": {type: "string"},
"action": {type: "string"},
}
}
},
"tokens": {
type: "array",
items: {
type: 'object',
properties: {
"symbol": {type: "string"},
"precision": {type: "integer"},
"amount": {type: "number"},
"contract": {type: "string"},
}
}
},
"total_actions": {type: "number"},
"actions": {
type: "array",
items: {
type: 'object',
properties: getActionResponseSchema
}
},
})
}
};
addApiRoute(
fastify,
@@ -1,139 +1,85 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {timedQuery} from "../../../helpers/functions";
import {Numeric} from "eosjs/dist";
import {getSkipLimit} from "../../v2-history/get_actions/functions";
function invalidKey() {
const err: any = new Error();
err.statusCode = 400;
err.message = 'invalid public key';
throw err;
const err: any = new Error();
err.statusCode = 400;
err.message = 'invalid public key';
throw err;
}
async function getKeyAccounts(fastify: FastifyInstance, request: FastifyRequest) {
let publicKey;
if (typeof request.body === 'string' && request.method === 'POST') {
request.body = JSON.parse(request.body);
}
let publicKey;
const public_Key = request.req.method === 'POST' ? request.body.public_key : request.query.public_key;
const body: any = request.body;
const query: any = request.query;
if (public_Key.startsWith("PUB_")) {
publicKey = public_Key;
} else if (public_Key.startsWith("EOS")) {
try {
publicKey = Numeric.convertLegacyPublicKey(public_Key);
} catch (e) {
console.log(e);
invalidKey();
}
} else {
invalidKey();
}
const {skip, limit} = getSkipLimit(request.query);
const maxDocs = fastify.manager.config.api.limits.get_key_accounts ?? 1000;
const _body = {
query: {
bool: {
should: [
{term: {"@updateauth.auth.keys.key.keyword": publicKey}},
{term: {"@newaccount.active.keys.key.keyword": publicKey}},
{term: {"@newaccount.owner.keys.key.keyword": publicKey}}
],
minimum_should_match: 1
}
},
sort: [{"global_sequence": {"order": "desc"}}]
};
const public_Key = request.method === 'POST' ? body.public_key : query.public_key;
const results = await fastify.elastic.search({
index: fastify.manager.chain + '-action-*',
body: _body
});
if (public_Key.startsWith("PUB_")) {
publicKey = public_Key;
} else if (public_Key.startsWith("EOS")) {
try {
publicKey = Numeric.convertLegacyPublicKey(public_Key);
} catch (e) {
console.log(e.message);
invalidKey();
}
} else {
invalidKey();
}
const response = {
account_names: []
} as any;
if (request.method === 'GET' && query.details) {
response.permissions = [];
}
try {
const permTableResults = await fastify.elastic.search({
index: fastify.manager.chain + '-perm-*',
size: (limit > maxDocs ? maxDocs : limit) || 100,
from: skip || 0,
body: {
query: {
bool: {
must: [
{
term: {
"auth.keys.key.keyword": publicKey
}
}
],
}
}
}
});
if (permTableResults.body.hits.hits.length > 0) {
for (const perm of permTableResults.body.hits.hits) {
response.account_names.push(perm._source.owner);
if (request.method === 'GET' && query.details) {
response.permissions.push(perm._source);
}
}
}
} catch (e) {
console.log(e);
}
if (response.account_names.length > 0) {
response.account_names = [...(new Set(response.account_names))];
return response;
}
// Fallback to action search
const _body = {
query: {
bool: {
should: [
{term: {"@updateauth.auth.keys.key.keyword": publicKey}},
{term: {"@newaccount.active.keys.key.keyword": publicKey}},
{term: {"@newaccount.owner.keys.key.keyword": publicKey}}
],
minimum_should_match: 1
}
},
sort: [{"global_sequence": {"order": "desc"}}]
};
const results = await fastify.elastic.search({
index: fastify.manager.chain + '-action-*',
size: (limit > maxDocs ? maxDocs : limit) || 100,
from: skip || 0,
body: _body
});
if (results['body']['hits']['hits'].length > 0) {
response.account_names = results['body']['hits']['hits'].map((v) => {
if (v._source.act.name === 'newaccount') {
if (v._source['@newaccount'].newact) {
return v._source['@newaccount'].newact;
} else if (v._source.act.data.newact) {
return v._source.act.data.newact;
} else {
return null;
}
} else if (v._source.act.name === 'updateauth') {
return v._source.act.data.account;
} else {
return null;
}
});
}
if (response.account_names.length > 0) {
response.account_names = [...(new Set(response.account_names))];
}
return response;
const response = {
account_names: []
};
if (results['body']['hits']['hits'].length > 0) {
response.account_names = results['body']['hits']['hits'].map((v) => {
if (v._source.act.name === 'newaccount') {
if (v._source['@newaccount'].newact) {
return v._source['@newaccount'].newact;
} else if (v._source.act.data.newact) {
return v._source.act.data.newact;
} else {
return null;
}
} else if (v._source.act.name === 'updateauth') {
return v._source.act.data.account;
} else {
return null;
}
});
}
if (response.account_names.length > 0) {
response.account_names = [...(new Set(response.account_names))];
return response;
} else {
const err: any = new Error();
err.statusCode = 404;
err.message = 'no accounts associated with ' + public_Key;
throw err;
}
}
export function getKeyAccountsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getKeyAccounts, fastify, request, route));
}
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getKeyAccounts, fastify, request, route));
}
}
+12 -25
View File
@@ -1,6 +1,6 @@
import {FastifyInstance} from "fastify";
import {getKeyAccountsHandler} from "./get_key_accounts";
import {addApiRoute, extendQueryStringSchema, getRouteName} from "../../../helpers/functions";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
@@ -8,38 +8,25 @@ export default function (fastify: FastifyInstance, opts: any, next) {
const getSchema = {
description: 'get accounts by public key',
summary: 'get accounts by public key',
tags: ['accounts'],
querystring: extendQueryStringSchema({
"public_key": {
description: 'public key',
type: 'string'
tags: ['accounts', 'state'],
querystring: {
type: 'object',
properties: {
"public_key": {
description: 'public key',
type: 'string'
},
},
"details": {
description: 'include permission details',
type: 'boolean'
},
}, ["public_key"]),
required: ["public_key"]
},
response: {
200: {
type: 'object',
properties: {
"account_names": {
type: "array",
items: {type: "string"}
},
"permissions": {
type: "array",
items: {
type: "object",
properties: {
owner: {type: 'string'},
block_num: {type: 'integer'},
parent: {type: 'string'},
last_updated: {type: 'string'},
auth: {},
name: {type: 'string'},
present: {type: 'number'}
}
type: "string"
}
}
}
+50 -54
View File
@@ -1,71 +1,67 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {getTrackTotalHits, timedQuery} from "../../../helpers/functions";
import {ApiResponse} from "@elastic/elasticsearch";
import {getSkipLimit} from "../../v2-history/get_actions/functions";
async function getLinks(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const {account, code, action, permissions} = query;
const {skip, limit} = getSkipLimit(query);
const query = request.query;
const {account, code, action, permissions} = query;
const {skip, limit} = getSkipLimit(query);
const queryStruct = {
"bool": {
must: [],
must_not: []
}
};
const queryStruct = {
"bool": {
must: []
}
};
if (account) {
queryStruct.bool.must.push({'term': {'account': account}});
}
if (account) {
queryStruct.bool.must.push({'term': {'account': account}});
}
if (code) {
queryStruct.bool.must.push({'term': {'code': code}});
}
if (code) {
queryStruct.bool.must.push({'term': {'code': code}});
}
if (action) {
queryStruct.bool.must.push({'term': {'action': action}});
}
if (action) {
queryStruct.bool.must.push({'term': {'action': action}});
}
if (permissions) {
queryStruct.bool.must.push({'term': {'permissions': permissions}});
}
if (permissions) {
queryStruct.bool.must.push({'term': {'permissions': permissions}});
}
// only present deltas
queryStruct.bool.must_not.push({'term': {'present': 0}});
// Prepare query body
const query_body = {
track_total_hits: getTrackTotalHits(query),
query: queryStruct,
sort: {block_num: 'desc'}
};
// Prepare query body
const query_body = {
track_total_hits: getTrackTotalHits(query),
query: queryStruct,
sort: {block_num: 'desc'}
};
const maxLinks = fastify.manager.config.api.limits.get_links;
const results: ApiResponse = await fastify.elastic.search({
index: fastify.manager.chain + '-link-*',
from: skip || 0,
size: (limit > maxLinks ? maxLinks : limit) || 50,
body: query_body
});
const hits = results.body.hits.hits;
const response: any = {
cached: false,
total: results.body.hits.total,
links: []
};
for (const hit of hits) {
const link = hit._source;
link.timestamp = link['@timestamp'];
delete link['@timestamp'];
response.links.push(link);
}
return response;
const maxLinks = fastify.manager.config.api.limits.get_links;
const results: ApiResponse = await fastify.elastic.search({
index: fastify.manager.chain + '-link-*',
from: skip || 0,
size: (limit > maxLinks ? maxLinks : limit) || 10,
body: query_body
});
const hits = results.body.hits.hits;
const response: any = {
cached: false,
total: results.body.hits.total,
links: []
};
for (const hit of hits) {
const link = hit._source;
link.timestamp = link['@timestamp'];
delete link['@timestamp'];
response.links.push(link);
}
return response;
}
export function getLinksHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getLinks, fastify, request, route));
}
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getLinks, fastify, request, route));
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get permission links',
summary: 'get permission links',
tags: ['accounts'],
tags: ['accounts', 'state'],
querystring: {
type: 'object',
properties: {
@@ -1,17 +1,16 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {getTrackTotalHits, timedQuery} from "../../../helpers/functions";
async function getProposals(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
// Pagination
let skip, limit;
skip = parseInt(query.skip, 10);
skip = parseInt(request.query.skip, 10);
if (skip < 0) {
return 'invalid skip parameter';
}
limit = parseInt(query.limit, 10);
limit = parseInt(request.query.limit, 10);
if (limit < 1) {
return 'invalid limit parameter';
}
@@ -22,44 +21,41 @@ async function getProposals(fastify: FastifyInstance, request: FastifyRequest) {
}
};
// Filter by accounts
if (query.account) {
const accounts = query.account.split(',');
for(const acc of accounts) {
queryStruct.bool.must.push({
"bool": {
"should": [
{"term": {"requested_approvals.actor": acc}},
{"term": {"provided_approvals.actor": acc}}
]
}
});
}
// 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 (query.proposer) {
queryStruct.bool.must.push({"term": {"proposer": query.proposer}});
if (request.query.proposer) {
queryStruct.bool.must.push({"term": {"proposer": request.query.proposer}});
}
// Filter by proposal name
if (query.proposal) {
queryStruct.bool.must.push({"term": {"proposal_name": query.proposal}});
if (request.query.proposal) {
queryStruct.bool.must.push({"term": {"proposal_name": request.query.proposal}});
}
// Filter by execution status
if (typeof query.executed !== 'undefined') {
queryStruct.bool.must.push({"term": {"executed": query.executed}});
if (typeof request.query.executed !== 'undefined') {
queryStruct.bool.must.push({"term": {"executed": request.query.executed}});
}
// Filter by requested actors
if (query.requested) {
queryStruct.bool.must.push({"term": {"requested_approvals.actor": query.requested}});
if (request.query.requested) {
queryStruct.bool.must.push({"term": {"requested_approvals.actor": request.query.requested}});
}
// Filter by provided actors
if (query.provided) {
queryStruct.bool.must.push({"term": {"provided_approvals.actor": query.provided}});
if (request.query.provided) {
queryStruct.bool.must.push({"term": {"provided_approvals.actor": request.query.provided}});
}
// If no filter switch to full match
@@ -98,7 +94,7 @@ async function getProposals(fastify: FastifyInstance, request: FastifyRequest) {
}
export function getProposalsHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getProposals, fastify, request, route));
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get proposals',
summary: 'get proposals',
tags: ['system'],
tags: ['state'],
querystring: {
type: 'object',
properties: {
+38 -60
View File
@@ -1,90 +1,68 @@
import {ServerResponse} from "http";
import {timedQuery} from "../../../helpers/functions";
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {getSkipLimit} from "../../v2-history/get_actions/functions";
async function getTokens(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const response = {
account: query.account,
tokens: []
query_time: null,
cached: false,
'account': request.query.account,
'tokens': []
};
const {skip, limit} = getSkipLimit(request.query);
const maxDocs = fastify.manager.config.api.limits.get_tokens ?? 100;
const stateResult = await fastify.elastic.search({
"index": fastify.manager.chain + '-table-accounts-*',
"size": (limit > maxDocs ? maxDocs : limit) || 50,
"from": skip || 0,
const results = await fastify.elastic.search({
"index": fastify.manager.chain + '-action-*',
"body": {
size: 0,
query: {
bool: {
filter: [{term: {scope: query.account}}]
filter: [
{term: {"notified": request.query.account}},
{terms: {"act.name": ["transfer", "issue"]}}
]
}
},
aggs: {
tokens: {
terms: {
field: "act.account",
size: 1000
}
}
}
}
});
const testSet = new Set();
for (const hit of stateResult.body.hits.hits) {
const data = hit._source;
if (typeof data.present !== "undefined" && data.present === 0) {
continue;
}
let precision;
for (const bucket of results['body']['aggregations']['tokens']['buckets']) {
let token_data;
let errorMsg;
const key = `${data.code}_${data.symbol}`;
if (testSet.has(key)) {
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;
}
testSet.add(key);
if (!fastify.tokenCache) {
fastify.tokenCache = new Map<string, any>();
}
if (fastify.tokenCache.has(key)) {
precision = fastify.tokenCache.get(key).precision;
} else {
try {
token_data = await fastify.eosjs.rpc.get_currency_balance(data.code, query.account, data.symbol);
if (token_data.length > 0) {
const [amount, symbol] = token_data[0].split(" ");
const amount_arr = amount.split(".");
if (amount_arr.length === 2) {
precision = amount_arr[1].length;
fastify.tokenCache.set(key, {precision});
}
}
} catch (e) {
errorMsg = e.message;
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']
});
}
const resp: Record<string, any> = {
symbol: data.symbol,
precision: precision,
amount: parseFloat(data.amount),
contract: data.code,
};
if (errorMsg) {
resp.error = errorMsg;
}
response.tokens.push(resp);
}
return response;
}
export function getTokensHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getTokens, fastify, request, route));
}
}
+12 -11
View File
@@ -1,20 +1,21 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, extendQueryStringSchema, getRouteName} from "../../../helpers/functions";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
import {getTokensHandler} from "./get_tokens";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get tokens from an account',
summary: 'get all tokens',
tags: ['accounts'],
querystring: extendQueryStringSchema({
"account": {
description: 'account name',
type: 'string',
minLength: 1,
maxLength: 12
description: 'get account data',
summary: 'get account summary',
tags: ['accounts', 'state'],
querystring: {
type: 'object',
properties: {
"account": {
description: 'account name',
type: 'string'
}
}
}, ["account"])
}
};
addApiRoute(
fastify,
+58 -49
View File
@@ -1,63 +1,72 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {timedQuery} from "../../../helpers/functions";
import {getSkipLimit} from "../../v2-history/get_actions/functions";
async function getVoters(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const {skip, limit} = getSkipLimit(request.query);
let skip, limit;
const response = {
voter_count: 0,
'voters': []
};
let queryStruct: any = {
"bool": {
"must": []
}
};
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';
}
if (query.producer) {
for (const bp of query.producer.split(",")) {
queryStruct.bool.must.push({"term": {"producers": bp}});
}
}
const response = {
query_time: null,
voter_count: 0,
'voters': []
};
let queryStruct: any = {
"bool": {
"must": []
}
};
if (query.proxy === 'true') {
queryStruct.bool.must.push({"term": {"is_proxy": true}});
}
if (request.query.producer) {
for (const bp of request.query.producer.split(",")) {
queryStruct.bool.must.push({"term": {"producers": bp}});
}
}
if (queryStruct.bool.must.length === 0) {
queryStruct = {
"match_all": {}
};
}
if (request.query.proxy === 'true') {
queryStruct.bool.must.push({"term": {"is_proxy": true}});
}
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;
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) => {
reply.send(await timedQuery(getVoters, fastify, request, route));
}
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getVoters, fastify, request, route));
}
}
+19 -24
View File
@@ -1,36 +1,31 @@
import {FastifyInstance} from "fastify";
import {getVotersHandler} from "./get_voters";
import {addApiRoute, extendQueryStringSchema, extendResponseSchema, getRouteName} from "../../../helpers/functions";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get voters',
summary: 'get voters',
tags: ['system'],
querystring: extendQueryStringSchema({
"producer": {
description: 'filter by voted producer (comma separated)',
type: 'string',
minLength: 1,
maxLength: 12
}
}),
response: extendResponseSchema({
"voters": {
type: "array",
items: {
type: "object",
properties: {
"account": {type: "string"},
"weight": {type: "number"},
"last_vote": {type: "number"},
"data": {
additionalProperties: true
}
}
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,
@@ -1,120 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
async function getLastSeq(fastify: FastifyInstance, date: string) {
const req = await fastify.elastic.search({
index: fastify.manager.chain + '-action-*',
body: {
"size": 1,
"query": {
"bool": {
"must": [
{"range": {"@timestamp": {"lt": date}}}
]
}
},
"_source": {
"includes": ["global_sequence"]
},
"sort": [{"global_sequence": "desc"}]
}
});
return req.body.hits.hits[0]._source.global_sequence;
}
async function getTxCount(fastify: FastifyInstance, dateFrom: string, dateTo: string) {
const req = await fastify.elastic.count({
index: fastify.manager.chain + '-action-*',
body: {
"query": {
"bool": {
"must": [
{"term": {"creator_action_ordinal": 0}},
{"range": {"@timestamp": {"gt": dateFrom, "lt": dateTo}}}
]
}
}
}
});
return req.body.count;
}
async function getUniqueActors(fastify: FastifyInstance, dateFrom: string, dateTo: string) {
const req = await fastify.elastic.search({
index: fastify.manager.chain + '-action-*',
size: 0,
body: {
"aggs": {
"unique_actors": {
"cardinality": {
"field": "act.authorization.actor"
}
}
},
"query": {
"bool": {
"filter": [{"range": {"@timestamp": {"gt": dateFrom, "lt": dateTo}}}]
}
}
}
});
return req.body.aggregations.unique_actors.value;
}
async function getActionUsage(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
let now = new Date();
let expiration = 86400;
if (query.end_date) {
now = new Date(query.end_date);
}
now.setMinutes(0, 0, 0);
let period;
if (query.period === '1h') {
period = 3600000;
} else if (query.period === '24h' || query.period === '1d') {
period = 86400000;
} else {
return {};
}
if (!query.end_date) {
expiration = Math.floor((period - (Date.now() - now.getTime())) / 1000);
}
const response = {} as any;
const periodStart = new Date(now.getTime() - period);
const pStart = periodStart.toISOString();
const pEnd = now.toISOString();
const cacheKey = `${fastify.manager.chain}_act_stats_${pStart}_${pEnd}_${query.unique_actors}`;
const cachedValue = await fastify.redis.get(cacheKey);
if (cachedValue) {
const cachedResponse = JSON.parse(cachedValue);
cachedResponse.cached = true;
cachedResponse.cache_exp = expiration;
return cachedResponse;
}
const firstReq = await getLastSeq(fastify, pStart);
const lastReq = await getLastSeq(fastify, pEnd);
response.action_count = lastReq - firstReq;
response.tx_count = await getTxCount(fastify, pStart, pEnd);
if (query.unique_actors) {
response.unique_actors = await getUniqueActors(fastify, pStart, pEnd);
}
response.period = query.period;
response.from = pStart;
response.to = pEnd;
await fastify.redis.set(cacheKey, JSON.stringify(response), 'EX', expiration);
return response;
}
export function getActionUsageHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getActionUsage, fastify, request, route));
}
}
@@ -1,37 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
import {getActionUsageHandler} from "./get_action_usage";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get action and transaction stats for a given period',
summary: 'get action and transaction stats for a given period',
tags: ['stats'],
querystring: {
type: 'object',
required: ["period"],
properties: {
"period": {
description: 'analysis period',
type: 'string'
},
"end_date": {
description: 'final date',
type: 'string'
},
"unique_actors": {
description: 'compute unique actors',
type: 'boolean'
}
}
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getActionUsageHandler,
schema
);
next();
}
@@ -1,64 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
import {Search} from "@elastic/elasticsearch/api/requestParams";
import {applyTimeFilter} from "../../v2-history/get_actions/functions";
async function getMissedBlocks(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const response = {
stats: {
by_producer: {}
},
events: []
};
const searchParams: Search<any> = {
track_total_hits: true,
index: fastify.manager.chain + "-logs-*",
size: 100,
body: {
query: {
bool: {
must: [
{term: {"type": "missed_blocks"}}
]
}
},
sort: {
"@timestamp": "desc"
}
}
};
let minBlocks = 0;
if (query.min_blocks) {
minBlocks = parseInt(query.min_blocks);
searchParams.body.query.bool.must.push({range: {"missed_blocks.size": {gte: minBlocks}}})
}
if (query.producer) {
searchParams.body.query.bool.must.push({term: {"missed_blocks.producer": query.producer}});
}
applyTimeFilter(query, searchParams.body.query);
const apiResponse = await fastify.elastic.search(searchParams);
apiResponse.body.hits.hits.forEach(v => {
const ev = v._source;
response.events.push({
"@timestamp": ev["@timestamp"],
...ev.missed_blocks
});
if (!response.stats.by_producer[ev.missed_blocks.producer]) {
response.stats.by_producer[ev.missed_blocks.producer] = ev.missed_blocks.size;
} else {
response.stats.by_producer[ev.missed_blocks.producer] += ev.missed_blocks.size;
}
});
return response;
}
export function getMissedBlocksHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getMissedBlocks, fastify, request, route));
}
}
@@ -1,62 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, extendResponseSchema, getRouteName} from "../../../helpers/functions";
import {getMissedBlocksHandler} from "./get_missed_blocks";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get missed blocks',
summary: 'get missed blocks',
tags: ['stats'],
querystring: {
type: 'object',
properties: {
"producer": {
description: 'filter by producer',
type: 'string'
},
"after": {
description: 'filter after specified date (ISO8601)',
type: 'string'
},
"before": {
description: 'filter before specified date (ISO8601)',
type: 'string'
},
"min_blocks": {
description: 'min. blocks threshold',
type: 'integer',
minimum: 1
}
}
},
response: extendResponseSchema({
"stats": {
type: "object",
properties: {
"by_producer": {}
}
},
"events": {
type: "array",
items: {
type: "object",
properties: {
"@timestamp": {type: "string"},
"last_block": {type: "number"},
"schedule_version": {type: "number"},
"size": {type: "number"},
"producer": {type: "string"},
}
}
},
})
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getMissedBlocksHandler,
schema
);
next();
}
@@ -1,75 +0,0 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {timedQuery} from "../../../helpers/functions";
const percentiles = [1, 5, 25, 50, 75, 95, 99];
async function getResourceUsage(fastify: FastifyInstance, request: FastifyRequest) {
const query: any = request.query;
const searchBody: any = {
query: {
bool: {
must: [
{term: {"act.account": query.code}},
{term: {"act.name": query.action}},
{term: {"action_ordinal": {"value": 1}}},
{
range: {
"@timestamp": {
gte: "now-2d/d",
lt: "now/d"
}
}
}
]
}
},
track_total_hits: true,
size: 0,
aggs: {
cpu_extended_stats: {extended_stats: {field: "cpu_usage_us"}},
net_extended_stats: {extended_stats: {field: "net_usage_words"}},
cpu_percentiles: {
percentiles: {
field: "cpu_usage_us",
percents: percentiles
}
},
net_percentiles: {
percentiles: {
field: "net_usage_words",
percents: percentiles
}
}
}
};
if (request.query['@transfer.to']) {
searchBody.query.bool.must.push({term: {"@transfer.to": request.query['@transfer.to']}});
}
const results = await fastify.elastic.search({
index: fastify.manager.chain + "-action-*",
body: searchBody
});
if (results.body) {
const data = results.body.aggregations;
return {
cpu: {
stats: data.cpu_extended_stats,
percentiles: data.cpu_percentiles.values
},
net: {
stats: data.net_extended_stats,
percentiles: data.net_percentiles.values
}
};
} else {
return {};
}
}
export function getResourceUsageHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
reply.send(await timedQuery(getResourceUsage, fastify, request, route));
}
}
@@ -1,33 +0,0 @@
import {FastifyInstance} from "fastify";
import {addApiRoute, getRouteName} from "../../../helpers/functions";
import {getResourceUsageHandler} from "./get_resource_usage";
export default function (fastify: FastifyInstance, opts: any, next) {
const schema = {
description: 'get resource usage stats for a specific action',
summary: 'get resource usage stats for a specific action',
tags: ['stats'],
querystring: {
type: 'object',
required: ["code", "action"],
properties: {
"code": {
description: 'contract',
type: 'string'
},
"action": {
description: 'action name',
type: 'string'
}
}
}
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
getResourceUsageHandler,
schema
);
next();
}
+18 -72
View File
@@ -1,10 +1,10 @@
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
import {ServerResponse} from "http";
import {connect} from "amqplib";
import {timedQuery} from "../../../helpers/functions";
import {getFirstIndexedBlock, getLastIndexedBlockWithTotalBlocks} from "../../../../helpers/common_functions";
import {getLastIndexedBlock} from "../../../../helpers/common_functions";
async function checkRabbit(fastify: FastifyInstance): Promise<ServiceResponse<any>> {
async function checkRabbit(fastify: FastifyInstance) {
try {
const connection = await connect(fastify.manager.ampqUrl);
await connection.close();
@@ -15,20 +15,12 @@ async function checkRabbit(fastify: FastifyInstance): Promise<ServiceResponse<an
}
}
interface NodeosService {
head_block_num: number;
head_block_time: string;
time_offset: number;
last_irreversible_block: number;
chain_id: string;
}
async function checkNodeos(fastify: FastifyInstance): Promise<ServiceResponse<NodeosService>> {
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 + '+00:00').getTime());
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,
@@ -44,54 +36,13 @@ async function checkNodeos(fastify: FastifyInstance): Promise<ServiceResponse<No
}
}
interface ESService {
first_indexed_block: number;
last_indexed_block: number;
total_indexed_blocks: number;
active_shards: string;
missing_blocks: number;
missing_pct: string;
head_offset: number;
}
interface ServiceResponse<T> {
service: string;
time: number;
status: any
service_data?: T;
}
async function checkElastic(fastify: FastifyInstance): Promise<ServiceResponse<ESService>> {
async function checkElastic(fastify: FastifyInstance) {
try {
const esStatusCache = await fastify.redis.get(`${fastify.manager.chain}::es_status`);
let esStatus;
if (esStatusCache) {
esStatus = JSON.parse(esStatusCache);
} else {
esStatus = await fastify.elastic.cat.health({format: 'json', v: true});
await fastify.redis.set(`${fastify.manager.chain}::es_status`, JSON.stringify(esStatus), 'EX', 30 * 60);
}
let firstIndexedBlock: number;
const fib = await fastify.redis.get(`${fastify.manager.chain}::fib`);
if (fib) {
firstIndexedBlock = parseInt(fib);
} else {
firstIndexedBlock = await getFirstIndexedBlock(fastify.elastic, fastify.manager.chain);
await fastify.redis.set(`${fastify.manager.chain}::fib`, firstIndexedBlock);
}
let indexedBlocks = await getLastIndexedBlockWithTotalBlocks(fastify.elastic, fastify.manager.chain);
const lastIndexedBlock = indexedBlocks[0];
const totalIndexed = indexedBlocks[1] - 1;
const missingCounter = (lastIndexedBlock - firstIndexedBlock) - totalIndexed;
const missingPct = (missingCounter * 100 / indexedBlocks[1]).toFixed(2) + "%";
const data: ESService = {
active_shards: esStatus.body[0]['active_shards_percent'],
head_offset: null,
first_indexed_block: firstIndexedBlock,
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,
total_indexed_blocks: totalIndexed,
missing_blocks: missingCounter,
missing_pct: missingPct
active_shards: esStatus.body[0]['active_shards_percent']
};
let stat = 'OK';
esStatus.body.forEach(status => {
@@ -108,7 +59,7 @@ async function checkElastic(fastify: FastifyInstance): Promise<ServiceResponse<E
}
}
function createHealth(name: string, status, data?: any): ServiceResponse<any> {
function createHealth(name: string, status, data?: any) {
let time = Date.now();
return {
service: name,
@@ -118,27 +69,22 @@ function createHealth(name: string, status, data?: any): ServiceResponse<any> {
}
}
async function getHealthQuery(fastify: FastifyInstance) {
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,
health: [],
features: fastify.manager.config.features
features: fastify.manager.config.features,
health: []
};
response.health = await Promise.all([
checkRabbit(fastify),
checkNodeos(fastify),
checkElastic(fastify)
]);
const es = response.health.find(value => value.service === 'Elasticsearch');
const nodeos = response.health.find(value => value.service === 'NodeosRPC');
es.service_data.head_offset = nodeos.service_data.head_block_num - es.service_data.last_indexed_block;
response.health.push(await checkRabbit(fastify));
response.health.push(await checkNodeos(fastify));
response.health.push(await checkElastic(fastify));
return response;
}
export function healthHandler(fastify: FastifyInstance, route: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
return async (request: FastifyRequest, reply: FastifyReply<ServerResponse>) => {
reply.send(await timedQuery(getHealthQuery, fastify, request, route));
}
}
+13 -13
View File
@@ -1,18 +1,18 @@
import {FastifyInstance, FastifySchema} from "fastify";
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: FastifySchema = {
tags: ['status'],
summary: "API Service Health Report"
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
healthHandler,
schema
);
next();
const schema: RouteSchema = {
tags: ['status'],
summary: "API Service Health Report"
};
addApiRoute(
fastify,
'GET',
getRouteName(__filename),
healthHandler,
schema
);
next();
}

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