@coopenomics/parser 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/index.cjs +386 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +377 -0
- package/package.json +89 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Anthony Fu <https://github.com/antfu>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# COOPARSER.
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
+
[![bundle][bundle-src]][bundle-href]
|
|
6
|
+
[![JSDocs][jsdocs-src]][jsdocs-href]
|
|
7
|
+
[![License][license-src]][license-href]
|
|
8
|
+
|
|
9
|
+
Пакет производит распаковку блоков, сохраняя действия и дельты таблиц и выдавая их по API. Состоит из двух модулей: парсера и API. Парсер считывает данные из блокчейна и помещает их в базу. API получает данные по запросу и возвращает их с пагинацией.
|
|
10
|
+
|
|
11
|
+
## Установка
|
|
12
|
+
```
|
|
13
|
+
pnpm install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Конфигурационный файл .env
|
|
17
|
+
```
|
|
18
|
+
NODE_ENV=development
|
|
19
|
+
- Определяет среду выполнения приложения.
|
|
20
|
+
|
|
21
|
+
API=http://127.0.0.1:8888
|
|
22
|
+
- Определяет URL-адрес API, к которому будет осуществляться доступ.
|
|
23
|
+
|
|
24
|
+
SHIP=ws://127.0.0.1:8080
|
|
25
|
+
- Определяет URL-адрес WebSocket-соединения, используемого для связи с другими узлами.
|
|
26
|
+
|
|
27
|
+
MONGO_EXPLORER_URI=mongodb://127.0.0.1:27017/cooperative
|
|
28
|
+
- Определяет URI-адрес MongoDB, используемый для подключения к базе данных.
|
|
29
|
+
|
|
30
|
+
START_BLOCK=1
|
|
31
|
+
- Определяет номер блока, с которого начинается парсинг блокчейна.
|
|
32
|
+
|
|
33
|
+
FINISH_BLOCK=0xFFFFFFFF
|
|
34
|
+
- Определяет номер блока, на котором заканчивается парсинг блокчейна. В данном случае, установлено значение "0xFFFFFFFF", что означает, что парсинг будет продолжаться до последнего доступного блока.
|
|
35
|
+
|
|
36
|
+
PORT=4000
|
|
37
|
+
- Определяет порт, на котором будет запущен сервер приложения.
|
|
38
|
+
|
|
39
|
+
ACTIVATE_PARSER=0
|
|
40
|
+
- Определяет флаг активации парсера. Если значение равно "1", парсер будет активирован.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Конфигурация парсера
|
|
44
|
+
В конфиге src/config.ts находится массив таблиц и действий, на которые парсер осуществит подписку.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
export const subsribedTables = [
|
|
48
|
+
{ code: 'registrator', table: 'users', 'scope': 'registrator' },
|
|
49
|
+
{ code: 'soviet', table: 'participants' },
|
|
50
|
+
]
|
|
51
|
+
```
|
|
52
|
+
- подписка будет осуществлена на изменения таблиц указанных контрактов. Параметр scope - не обязательный. Без его указания любые scope будут попадать в базу данных.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
export const subsribedActions = [
|
|
56
|
+
{ code: 'soviet', action: 'votefor' },
|
|
57
|
+
{ code: 'soviet', action: 'voteagainst' },
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
- подписка будет осуществлена на действия указанных контрактов.
|
|
61
|
+
|
|
62
|
+
Парсер может быть расширен любыми кастомными действиями, которые будут выполняться перед добавлением записи в базу данных. Для этого, для таблиц и действий соответственно, в папках src/ActionParser/Actions и src/DeltaParser/Deltas необходимо создать файлы с методами обработки и добавить их к src/ActionParser/Actions или src/DeltaParser/DeltaFactory.
|
|
63
|
+
|
|
64
|
+
### Запуск
|
|
65
|
+
```
|
|
66
|
+
pnpm start
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
### Получение таблиц
|
|
72
|
+
Конечная точка предоставляет информацию о изменении (дельтах) таблиц между блоками.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
let params = {
|
|
77
|
+
page: 1,
|
|
78
|
+
limit: 10,
|
|
79
|
+
filter: { } - любые параметры фильтрации таблицы, включая данные в полях
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
axios.get('http://localhost:4000/get-tables', { params })
|
|
83
|
+
.then(response => {
|
|
84
|
+
console.log(response.data);
|
|
85
|
+
// {
|
|
86
|
+
// results: array,
|
|
87
|
+
// page: number,
|
|
88
|
+
// limit: number
|
|
89
|
+
// };
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Получение действий
|
|
94
|
+
Конечная точка предоставляет информацию о действиях, произошедших между блоками.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
let params = {
|
|
98
|
+
page: 1,
|
|
99
|
+
limit: 10,
|
|
100
|
+
filter: { } // любые параметры фильтрации действий, включая данные в полях
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
axios.get('http://localhost:4000/get-actions', { params })
|
|
104
|
+
.then(response => {
|
|
105
|
+
console.log(response.data);
|
|
106
|
+
// {
|
|
107
|
+
// results: array,
|
|
108
|
+
// page: number,
|
|
109
|
+
// limit: number
|
|
110
|
+
// };
|
|
111
|
+
})
|
|
112
|
+
.catch(error => {
|
|
113
|
+
console.error(error);
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Получение текущего блока
|
|
118
|
+
Конечная точка предоставляет информацию о текущем блоке. Эта информация используется при формировании кооперативных документов.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
axios.get('http://localhost:4000/get-current-block')
|
|
122
|
+
.then(response => {
|
|
123
|
+
console.log(response.data);
|
|
124
|
+
// number
|
|
125
|
+
})
|
|
126
|
+
.catch(error => {
|
|
127
|
+
console.error(error);
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Лицензия
|
|
132
|
+
|
|
133
|
+
[MIT](./LICENSE) License © 2024-PRESENT [CBS VOSKHOD](https://github.com/copenomics)
|
|
134
|
+
|
|
135
|
+
<!-- Badges -->
|
|
136
|
+
|
|
137
|
+
[npm-version-src]: https://img.shields.io/npm/v/cooparser?style=flat&colorA=080f12&colorB=1fa669
|
|
138
|
+
[npm-version-href]: https://npmjs.com/package/cooparser
|
|
139
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/cooparser?style=flat&colorA=080f12&colorB=1fa669
|
|
140
|
+
[npm-downloads-href]: https://npmjs.com/package/cooparser
|
|
141
|
+
[bundle-src]: https://img.shields.io/bundlephobia/minzip/cooparser?style=flat&colorA=080f12&colorB=1fa669&label=minzip
|
|
142
|
+
[bundle-href]: https://bundlephobia.com/result?p=cooparser
|
|
143
|
+
[license-src]: https://img.shields.io/github/license/copenomics/cooparser.svg?style=flat&colorA=080f12&colorB=1fa669
|
|
144
|
+
[license-href]: https://github.com/copenomics/cooparser/blob/main/LICENSE
|
|
145
|
+
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
|
|
146
|
+
[jsdocs-href]: https://www.jsdocs.io/package/cooparser
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
require('express-async-errors');
|
|
5
|
+
const mongodb = require('mongodb');
|
|
6
|
+
const dotenv = require('dotenv');
|
|
7
|
+
const eosioShipReader = require('@blockmatic/eosio-ship-reader');
|
|
8
|
+
const fetch = require('node-fetch');
|
|
9
|
+
const Redis = require('ioredis');
|
|
10
|
+
|
|
11
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
12
|
+
|
|
13
|
+
const express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
14
|
+
const dotenv__default = /*#__PURE__*/_interopDefaultCompat(dotenv);
|
|
15
|
+
const fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
|
|
16
|
+
const Redis__default = /*#__PURE__*/_interopDefaultCompat(Redis);
|
|
17
|
+
|
|
18
|
+
dotenv__default.config();
|
|
19
|
+
function getEnvVar(key) {
|
|
20
|
+
const envVar = process.env[key];
|
|
21
|
+
if (envVar === void 0)
|
|
22
|
+
throw new Error(`Env variable ${key} is required`);
|
|
23
|
+
return envVar;
|
|
24
|
+
}
|
|
25
|
+
const node_env = getEnvVar("NODE_ENV");
|
|
26
|
+
const eosioApi = getEnvVar("API");
|
|
27
|
+
const shipApi = getEnvVar("SHIP");
|
|
28
|
+
const mongoUri = `${getEnvVar("MONGO_EXPLORER_URI")}${node_env === "test" ? "-test" : ""}`;
|
|
29
|
+
const startBlock = getEnvVar("START_BLOCK");
|
|
30
|
+
const finishBlock = getEnvVar("FINISH_BLOCK");
|
|
31
|
+
const redisPort = getEnvVar("REDIS_PORT");
|
|
32
|
+
const redisHost = getEnvVar("REDIS_HOST");
|
|
33
|
+
const redisPassword = getEnvVar("REDIS_PASSWORD");
|
|
34
|
+
const redisStreamLimit = Number(getEnvVar("REDIS_STREAM_LIMIT"));
|
|
35
|
+
const subsribedTables = [
|
|
36
|
+
// документы
|
|
37
|
+
{ code: "draft", table: "drafts" },
|
|
38
|
+
{ code: "draft", table: "translations" },
|
|
39
|
+
// совет
|
|
40
|
+
{ code: "soviet", table: "decisions" },
|
|
41
|
+
{ code: "soviet", table: "boards" },
|
|
42
|
+
{ code: "soviet", table: "participants" },
|
|
43
|
+
// registrator.joincoop
|
|
44
|
+
{ code: "soviet", table: "joincoops" },
|
|
45
|
+
// регистратор
|
|
46
|
+
{ code: "registrator", table: "accounts" },
|
|
47
|
+
{ code: "registrator", table: "orgs" }
|
|
48
|
+
];
|
|
49
|
+
const subsribedActions = [
|
|
50
|
+
{ code: "eosio.token", action: "transfer", notify: true },
|
|
51
|
+
{ code: "registrator", action: "confirmreg", notify: true },
|
|
52
|
+
{ code: "soviet", action: "votefor" },
|
|
53
|
+
{ code: "soviet", action: "voteagainst" },
|
|
54
|
+
{ code: "soviet", action: "newsubmitted" },
|
|
55
|
+
{ code: "soviet", action: "newresolved" },
|
|
56
|
+
{ code: "soviet", action: "newdecision" },
|
|
57
|
+
// // registrator.joincoop
|
|
58
|
+
{ code: "soviet", action: "joincoop" },
|
|
59
|
+
{ code: "soviet", action: "joincoopdec" },
|
|
60
|
+
{ code: "soviet", action: "updateboard", notify: true },
|
|
61
|
+
{ code: "soviet", action: "createboard", notify: true }
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
var __defProp = Object.defineProperty;
|
|
65
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
66
|
+
var __publicField = (obj, key, value) => {
|
|
67
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
class Database {
|
|
71
|
+
constructor() {
|
|
72
|
+
__publicField(this, "client");
|
|
73
|
+
__publicField(this, "db");
|
|
74
|
+
__publicField(this, "actions");
|
|
75
|
+
__publicField(this, "deltas");
|
|
76
|
+
__publicField(this, "sync");
|
|
77
|
+
this.client = new mongodb.MongoClient(mongoUri);
|
|
78
|
+
}
|
|
79
|
+
async connect() {
|
|
80
|
+
await this.client.connect();
|
|
81
|
+
this.db = this.client.db();
|
|
82
|
+
this.actions = this.db.collection("actions");
|
|
83
|
+
this.deltas = this.db.collection("deltas");
|
|
84
|
+
this.sync = this.db.collection("sync");
|
|
85
|
+
}
|
|
86
|
+
async saveActionToDB(action) {
|
|
87
|
+
if (!this.actions)
|
|
88
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
89
|
+
await this.actions.insertOne(action);
|
|
90
|
+
}
|
|
91
|
+
async saveDeltaToDB(delta) {
|
|
92
|
+
if (!this.deltas)
|
|
93
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
94
|
+
await this.deltas.insertOne(delta);
|
|
95
|
+
}
|
|
96
|
+
async getDelta(filter) {
|
|
97
|
+
if (!this.deltas)
|
|
98
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
99
|
+
const result = await this.deltas.findOne(filter);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
async getTables(filter, page = 1, limit = 10) {
|
|
103
|
+
if (!this.deltas)
|
|
104
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
105
|
+
const pipeline = [
|
|
106
|
+
{ $match: filter },
|
|
107
|
+
{ $sort: { block_num: -1 } },
|
|
108
|
+
// Сортировка по primary_key и block_num
|
|
109
|
+
{ $group: { _id: "$primary_key", doc: { $first: "$$ROOT" } } },
|
|
110
|
+
{ $replaceRoot: { newRoot: "$doc" } },
|
|
111
|
+
{ $sort: { block_num: -1 } },
|
|
112
|
+
{ $skip: (page - 1) * limit },
|
|
113
|
+
// Применяется внутри пайплайна
|
|
114
|
+
{ $limit: limit }
|
|
115
|
+
// Применяется внутри пайплайна
|
|
116
|
+
];
|
|
117
|
+
const result = await this.deltas.aggregate(pipeline).toArray();
|
|
118
|
+
return {
|
|
119
|
+
results: result,
|
|
120
|
+
page,
|
|
121
|
+
limit
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async getActions(filter, page = 1, limit = 10) {
|
|
125
|
+
if (!this.actions)
|
|
126
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
127
|
+
const query = filter || {};
|
|
128
|
+
const result = await this.actions.aggregate([
|
|
129
|
+
{ $match: query },
|
|
130
|
+
{ $sort: { block_num: -1 } },
|
|
131
|
+
// Сортировка по primary_key и block_num
|
|
132
|
+
{ $group: { _id: "$global_sequence", doc: { $first: "$$ROOT" } } },
|
|
133
|
+
{ $replaceRoot: { newRoot: "$doc" } },
|
|
134
|
+
{ $sort: { block_num: -1 } },
|
|
135
|
+
{ $skip: (page - 1) * limit },
|
|
136
|
+
{ $limit: limit }
|
|
137
|
+
]).toArray();
|
|
138
|
+
return {
|
|
139
|
+
results: result,
|
|
140
|
+
page,
|
|
141
|
+
limit
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async getAction(filter) {
|
|
145
|
+
if (!this.actions)
|
|
146
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
147
|
+
const result = await this.actions.findOne(filter);
|
|
148
|
+
return result ? result.value : null;
|
|
149
|
+
}
|
|
150
|
+
async getCurrentBlock() {
|
|
151
|
+
if (!this.sync)
|
|
152
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
153
|
+
const currentBlockDocument = await this.sync.findOne({ key: "currentBlock" });
|
|
154
|
+
return currentBlockDocument ? currentBlockDocument.block_num : 0;
|
|
155
|
+
}
|
|
156
|
+
async updateCurrentBlock(block_num) {
|
|
157
|
+
if (!this.sync)
|
|
158
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
159
|
+
await this.sync.updateOne({ key: "currentBlock" }, { $set: { block_num } }, { upsert: true });
|
|
160
|
+
}
|
|
161
|
+
async purgeAfterBlock(since_block) {
|
|
162
|
+
if (!this.actions || !this.deltas)
|
|
163
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
164
|
+
await this.actions.deleteMany({ block_num: { $gt: since_block } });
|
|
165
|
+
await this.deltas.deleteMany({ block_num: { $gt: since_block } });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const db = new Database();
|
|
169
|
+
async function init() {
|
|
170
|
+
return db.connect().then(() => {
|
|
171
|
+
console.log("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u0430");
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const redis = new Redis__default({
|
|
176
|
+
port: Number(redisPort),
|
|
177
|
+
host: redisHost,
|
|
178
|
+
password: redisPassword
|
|
179
|
+
// другие опции при необходимости
|
|
180
|
+
});
|
|
181
|
+
const streamName = "notifications";
|
|
182
|
+
async function publishEvent(type, event) {
|
|
183
|
+
const message = JSON.stringify({ type, event });
|
|
184
|
+
await redis.xadd(streamName, "*", "event", message);
|
|
185
|
+
await redis.xtrim(streamName, "MAXLEN", "~", redisStreamLimit);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class AnyAnyActionParser {
|
|
189
|
+
async process(db, action) {
|
|
190
|
+
db.saveActionToDB(action);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
class ActionParserFactory {
|
|
195
|
+
static create(accountName, actionName) {
|
|
196
|
+
switch (`${accountName}::${actionName}`) {
|
|
197
|
+
case "*::*":
|
|
198
|
+
return null;
|
|
199
|
+
default:
|
|
200
|
+
return new AnyAnyActionParser();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function ActionsParser(db, reader) {
|
|
206
|
+
const { actions$ } = reader;
|
|
207
|
+
actions$.subscribe(async (action) => {
|
|
208
|
+
console.log(`
|
|
209
|
+
ACTION - account: ${action.account}, name: ${action.name}, authorization: ${JSON.stringify(action.authorization)}, data: ${JSON.stringify(action.data)}`);
|
|
210
|
+
const parser = ActionParserFactory.create(action.account, action.name);
|
|
211
|
+
const source = subsribedActions.find((el) => el.action === action.name && el.code === action.account);
|
|
212
|
+
if (parser) {
|
|
213
|
+
await parser.process(db, action);
|
|
214
|
+
if (source?.notify)
|
|
215
|
+
await publishEvent("action", action);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
console.log("\u041F\u043E\u0434\u043F\u0438\u0441\u043A\u0430 \u043D\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function BlockParser(db, reader) {
|
|
222
|
+
const { blocks$, errors$, close$ } = reader;
|
|
223
|
+
errors$.subscribe(async (error) => {
|
|
224
|
+
console.log("\n\u041E\u0448\u0438\u0431\u043A\u0430 \u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438: ", error);
|
|
225
|
+
});
|
|
226
|
+
close$.subscribe(async (error) => {
|
|
227
|
+
console.error("\n\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F: ", error);
|
|
228
|
+
console.log("\u0412\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 10 \u0441\u0435\u043A\u0443\u043D\u0434");
|
|
229
|
+
setTimeout(() => process.exit(1), 1e4);
|
|
230
|
+
});
|
|
231
|
+
blocks$.subscribe(async (block) => {
|
|
232
|
+
db.updateCurrentBlock(block.block_num);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
class DeltaParser {
|
|
237
|
+
async process(db, delta) {
|
|
238
|
+
db.saveDeltaToDB(delta);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class DeltaParserFactory {
|
|
243
|
+
static create(code, scope, table) {
|
|
244
|
+
switch (`${code}::${scope}::${table}`) {
|
|
245
|
+
default:
|
|
246
|
+
return new DeltaParser();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function DeltasParser(db, reader) {
|
|
252
|
+
const { rows$ } = reader;
|
|
253
|
+
rows$.subscribe(async (delta) => {
|
|
254
|
+
console.log(`
|
|
255
|
+
DELTA - code: ${delta.code}, scope: ${delta.scope}, table: ${delta.table}, primary_key: ${delta.primary_key}, data: ${JSON.stringify(delta.value)}`);
|
|
256
|
+
const source = subsribedTables.find((el) => el.code === delta.code && el.table === delta.table);
|
|
257
|
+
const parser = DeltaParserFactory.create(delta.code, delta.scope, delta.table);
|
|
258
|
+
if (parser) {
|
|
259
|
+
await parser.process(db, delta);
|
|
260
|
+
if (source?.notify)
|
|
261
|
+
await publishEvent("delta", delta);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
console.log("\u041F\u043E\u0434\u043F\u0438\u0441\u043A\u0430 \u043D\u0430 \u0434\u0435\u043B\u044C\u0442\u044B \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const getInfo = () => fetch__default(`${eosioApi}/v1/chain/get_info`).then((res) => res.json());
|
|
268
|
+
function fetchAbi(account_name) {
|
|
269
|
+
return fetch__default(`${eosioApi}/v1/chain/get_abi`, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
account_name
|
|
273
|
+
})
|
|
274
|
+
}).then(async (res) => {
|
|
275
|
+
const response = await res.json();
|
|
276
|
+
return {
|
|
277
|
+
account_name,
|
|
278
|
+
abi: response.abi
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const table_rows_whitelist = () => subsribedTables;
|
|
284
|
+
const actions_whitelist = () => subsribedActions;
|
|
285
|
+
console.log(subsribedTables);
|
|
286
|
+
console.log(subsribedActions);
|
|
287
|
+
async function loadReader(db) {
|
|
288
|
+
let currentBlock = await db.getCurrentBlock();
|
|
289
|
+
const info = await getInfo();
|
|
290
|
+
if (currentBlock === 0)
|
|
291
|
+
currentBlock = Number(startBlock);
|
|
292
|
+
console.log("\u0421\u0442\u0430\u0440\u0442\u0443\u0435\u043C \u0441 \u0431\u043B\u043E\u043A\u0430: ", currentBlock);
|
|
293
|
+
console.log("\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u043C \u043D\u0430 \u0431\u043B\u043E\u043A\u0435: ", finishBlock);
|
|
294
|
+
console.log("\u0412\u044B\u0441\u043E\u0442\u0430 \u0446\u0435\u043F\u043E\u0447\u043A\u0438: ", info.head_block_num);
|
|
295
|
+
console.log("\u041E\u0447\u0438\u0449\u0430\u0435\u043C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0438 \u0434\u0435\u043B\u044C\u0442\u044B \u043F\u043E\u0441\u043B\u0435 \u0431\u043B\u043E\u043A\u0430: ", currentBlock);
|
|
296
|
+
await db.purgeAfterBlock(currentBlock);
|
|
297
|
+
const unique_contract_names = [...new Set(table_rows_whitelist().map((row) => row.code)), ...new Set(actions_whitelist().map((row) => row.code))];
|
|
298
|
+
const abisArr = await Promise.all(unique_contract_names.map((account_name) => fetchAbi(account_name)));
|
|
299
|
+
const contract_abis = () => {
|
|
300
|
+
const numap = /* @__PURE__ */ new Map();
|
|
301
|
+
abisArr.forEach(({ account_name, abi }) => numap.set(account_name, abi));
|
|
302
|
+
return numap;
|
|
303
|
+
};
|
|
304
|
+
const delta_whitelist = () => [
|
|
305
|
+
"account_metadata",
|
|
306
|
+
"contract_table",
|
|
307
|
+
"contract_row",
|
|
308
|
+
"contract_index64",
|
|
309
|
+
"resource_usage",
|
|
310
|
+
"resource_limits_state"
|
|
311
|
+
];
|
|
312
|
+
const eosioReaderConfig = {
|
|
313
|
+
ws_url: shipApi,
|
|
314
|
+
rpc_url: eosioApi,
|
|
315
|
+
ds_threads: 2,
|
|
316
|
+
ds_experimental: false,
|
|
317
|
+
delta_whitelist,
|
|
318
|
+
table_rows_whitelist,
|
|
319
|
+
actions_whitelist,
|
|
320
|
+
contract_abis,
|
|
321
|
+
request: {
|
|
322
|
+
start_block_num: currentBlock,
|
|
323
|
+
end_block_num: Number(finishBlock),
|
|
324
|
+
// info.head_block_num,
|
|
325
|
+
max_messages_in_flight: 50,
|
|
326
|
+
have_positions: [],
|
|
327
|
+
irreversible_only: true,
|
|
328
|
+
fetch_block: true,
|
|
329
|
+
fetch_traces: true,
|
|
330
|
+
fetch_deltas: true
|
|
331
|
+
},
|
|
332
|
+
auto_start: true
|
|
333
|
+
};
|
|
334
|
+
return await eosioShipReader.createEosioShipReader(eosioReaderConfig);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
class Parser {
|
|
338
|
+
async start() {
|
|
339
|
+
const reader = await loadReader(db);
|
|
340
|
+
try {
|
|
341
|
+
BlockParser(db, reader);
|
|
342
|
+
ActionsParser(db, reader);
|
|
343
|
+
DeltasParser(db, reader);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
console.error("\u041E\u0448\u0438\u0431\u043A\u0430: ", e);
|
|
346
|
+
this.start();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const app = express__default();
|
|
352
|
+
app.use(express__default.json());
|
|
353
|
+
app.use(express__default.urlencoded({ extended: true }));
|
|
354
|
+
const port = process.env.PORT || 4e3;
|
|
355
|
+
const parser = new Parser();
|
|
356
|
+
init().then(() => {
|
|
357
|
+
app.listen(port, () => {
|
|
358
|
+
console.log(`API \u043E\u0431\u043E\u0437\u0440\u0435\u0432\u0430\u0442\u0435\u043B\u044F \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E \u043D\u0430 http://localhost:${port}`);
|
|
359
|
+
if (process.env.ACTIVATE_PARSER === "1")
|
|
360
|
+
parser.start();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
app.get("/get-tables", async (req, res) => {
|
|
364
|
+
const page = Number(req.query.page) || 1;
|
|
365
|
+
const limit = Number(req.query.limit) || 10;
|
|
366
|
+
const filter = req.query.filter ? JSON.parse(req.query.filter) : {};
|
|
367
|
+
const result = await db.getTables(filter, page, limit);
|
|
368
|
+
res.json(result);
|
|
369
|
+
});
|
|
370
|
+
app.get("/get-actions", async (req, res) => {
|
|
371
|
+
const page = Number(req.query.page) || 1;
|
|
372
|
+
const limit = Number(req.query.limit) || 10;
|
|
373
|
+
const filter = req.query.filter ? JSON.parse(req.query.filter) : {};
|
|
374
|
+
const result = await db.getActions(filter, page, limit);
|
|
375
|
+
res.json(result);
|
|
376
|
+
});
|
|
377
|
+
app.get("/get-current-block", async (req, res) => {
|
|
378
|
+
const result = await db.getCurrentBlock();
|
|
379
|
+
res.json(result);
|
|
380
|
+
});
|
|
381
|
+
app.use((err, req, res, _next) => {
|
|
382
|
+
console.error("\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430: ", err);
|
|
383
|
+
res.status(500).send(err.message);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
exports.parser = parser;
|
package/dist/index.d.cts
ADDED
package/dist/index.d.mts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import 'express-async-errors';
|
|
3
|
+
import { MongoClient } from 'mongodb';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { createEosioShipReader } from '@blockmatic/eosio-ship-reader';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
7
|
+
import Redis from 'ioredis';
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
function getEnvVar(key) {
|
|
11
|
+
const envVar = process.env[key];
|
|
12
|
+
if (envVar === void 0)
|
|
13
|
+
throw new Error(`Env variable ${key} is required`);
|
|
14
|
+
return envVar;
|
|
15
|
+
}
|
|
16
|
+
const node_env = getEnvVar("NODE_ENV");
|
|
17
|
+
const eosioApi = getEnvVar("API");
|
|
18
|
+
const shipApi = getEnvVar("SHIP");
|
|
19
|
+
const mongoUri = `${getEnvVar("MONGO_EXPLORER_URI")}${node_env === "test" ? "-test" : ""}`;
|
|
20
|
+
const startBlock = getEnvVar("START_BLOCK");
|
|
21
|
+
const finishBlock = getEnvVar("FINISH_BLOCK");
|
|
22
|
+
const redisPort = getEnvVar("REDIS_PORT");
|
|
23
|
+
const redisHost = getEnvVar("REDIS_HOST");
|
|
24
|
+
const redisPassword = getEnvVar("REDIS_PASSWORD");
|
|
25
|
+
const redisStreamLimit = Number(getEnvVar("REDIS_STREAM_LIMIT"));
|
|
26
|
+
const subsribedTables = [
|
|
27
|
+
// документы
|
|
28
|
+
{ code: "draft", table: "drafts" },
|
|
29
|
+
{ code: "draft", table: "translations" },
|
|
30
|
+
// совет
|
|
31
|
+
{ code: "soviet", table: "decisions" },
|
|
32
|
+
{ code: "soviet", table: "boards" },
|
|
33
|
+
{ code: "soviet", table: "participants" },
|
|
34
|
+
// registrator.joincoop
|
|
35
|
+
{ code: "soviet", table: "joincoops" },
|
|
36
|
+
// регистратор
|
|
37
|
+
{ code: "registrator", table: "accounts" },
|
|
38
|
+
{ code: "registrator", table: "orgs" }
|
|
39
|
+
];
|
|
40
|
+
const subsribedActions = [
|
|
41
|
+
{ code: "eosio.token", action: "transfer", notify: true },
|
|
42
|
+
{ code: "registrator", action: "confirmreg", notify: true },
|
|
43
|
+
{ code: "soviet", action: "votefor" },
|
|
44
|
+
{ code: "soviet", action: "voteagainst" },
|
|
45
|
+
{ code: "soviet", action: "newsubmitted" },
|
|
46
|
+
{ code: "soviet", action: "newresolved" },
|
|
47
|
+
{ code: "soviet", action: "newdecision" },
|
|
48
|
+
// // registrator.joincoop
|
|
49
|
+
{ code: "soviet", action: "joincoop" },
|
|
50
|
+
{ code: "soviet", action: "joincoopdec" },
|
|
51
|
+
{ code: "soviet", action: "updateboard", notify: true },
|
|
52
|
+
{ code: "soviet", action: "createboard", notify: true }
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
var __defProp = Object.defineProperty;
|
|
56
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
57
|
+
var __publicField = (obj, key, value) => {
|
|
58
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
class Database {
|
|
62
|
+
constructor() {
|
|
63
|
+
__publicField(this, "client");
|
|
64
|
+
__publicField(this, "db");
|
|
65
|
+
__publicField(this, "actions");
|
|
66
|
+
__publicField(this, "deltas");
|
|
67
|
+
__publicField(this, "sync");
|
|
68
|
+
this.client = new MongoClient(mongoUri);
|
|
69
|
+
}
|
|
70
|
+
async connect() {
|
|
71
|
+
await this.client.connect();
|
|
72
|
+
this.db = this.client.db();
|
|
73
|
+
this.actions = this.db.collection("actions");
|
|
74
|
+
this.deltas = this.db.collection("deltas");
|
|
75
|
+
this.sync = this.db.collection("sync");
|
|
76
|
+
}
|
|
77
|
+
async saveActionToDB(action) {
|
|
78
|
+
if (!this.actions)
|
|
79
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
80
|
+
await this.actions.insertOne(action);
|
|
81
|
+
}
|
|
82
|
+
async saveDeltaToDB(delta) {
|
|
83
|
+
if (!this.deltas)
|
|
84
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
85
|
+
await this.deltas.insertOne(delta);
|
|
86
|
+
}
|
|
87
|
+
async getDelta(filter) {
|
|
88
|
+
if (!this.deltas)
|
|
89
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
90
|
+
const result = await this.deltas.findOne(filter);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
async getTables(filter, page = 1, limit = 10) {
|
|
94
|
+
if (!this.deltas)
|
|
95
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
96
|
+
const pipeline = [
|
|
97
|
+
{ $match: filter },
|
|
98
|
+
{ $sort: { block_num: -1 } },
|
|
99
|
+
// Сортировка по primary_key и block_num
|
|
100
|
+
{ $group: { _id: "$primary_key", doc: { $first: "$$ROOT" } } },
|
|
101
|
+
{ $replaceRoot: { newRoot: "$doc" } },
|
|
102
|
+
{ $sort: { block_num: -1 } },
|
|
103
|
+
{ $skip: (page - 1) * limit },
|
|
104
|
+
// Применяется внутри пайплайна
|
|
105
|
+
{ $limit: limit }
|
|
106
|
+
// Применяется внутри пайплайна
|
|
107
|
+
];
|
|
108
|
+
const result = await this.deltas.aggregate(pipeline).toArray();
|
|
109
|
+
return {
|
|
110
|
+
results: result,
|
|
111
|
+
page,
|
|
112
|
+
limit
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async getActions(filter, page = 1, limit = 10) {
|
|
116
|
+
if (!this.actions)
|
|
117
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
118
|
+
const query = filter || {};
|
|
119
|
+
const result = await this.actions.aggregate([
|
|
120
|
+
{ $match: query },
|
|
121
|
+
{ $sort: { block_num: -1 } },
|
|
122
|
+
// Сортировка по primary_key и block_num
|
|
123
|
+
{ $group: { _id: "$global_sequence", doc: { $first: "$$ROOT" } } },
|
|
124
|
+
{ $replaceRoot: { newRoot: "$doc" } },
|
|
125
|
+
{ $sort: { block_num: -1 } },
|
|
126
|
+
{ $skip: (page - 1) * limit },
|
|
127
|
+
{ $limit: limit }
|
|
128
|
+
]).toArray();
|
|
129
|
+
return {
|
|
130
|
+
results: result,
|
|
131
|
+
page,
|
|
132
|
+
limit
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async getAction(filter) {
|
|
136
|
+
if (!this.actions)
|
|
137
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
138
|
+
const result = await this.actions.findOne(filter);
|
|
139
|
+
return result ? result.value : null;
|
|
140
|
+
}
|
|
141
|
+
async getCurrentBlock() {
|
|
142
|
+
if (!this.sync)
|
|
143
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
144
|
+
const currentBlockDocument = await this.sync.findOne({ key: "currentBlock" });
|
|
145
|
+
return currentBlockDocument ? currentBlockDocument.block_num : 0;
|
|
146
|
+
}
|
|
147
|
+
async updateCurrentBlock(block_num) {
|
|
148
|
+
if (!this.sync)
|
|
149
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
150
|
+
await this.sync.updateOne({ key: "currentBlock" }, { $set: { block_num } }, { upsert: true });
|
|
151
|
+
}
|
|
152
|
+
async purgeAfterBlock(since_block) {
|
|
153
|
+
if (!this.actions || !this.deltas)
|
|
154
|
+
throw new Error("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0430");
|
|
155
|
+
await this.actions.deleteMany({ block_num: { $gt: since_block } });
|
|
156
|
+
await this.deltas.deleteMany({ block_num: { $gt: since_block } });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const db = new Database();
|
|
160
|
+
async function init() {
|
|
161
|
+
return db.connect().then(() => {
|
|
162
|
+
console.log("\u0411\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u0430");
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const redis = new Redis({
|
|
167
|
+
port: Number(redisPort),
|
|
168
|
+
host: redisHost,
|
|
169
|
+
password: redisPassword
|
|
170
|
+
// другие опции при необходимости
|
|
171
|
+
});
|
|
172
|
+
const streamName = "notifications";
|
|
173
|
+
async function publishEvent(type, event) {
|
|
174
|
+
const message = JSON.stringify({ type, event });
|
|
175
|
+
await redis.xadd(streamName, "*", "event", message);
|
|
176
|
+
await redis.xtrim(streamName, "MAXLEN", "~", redisStreamLimit);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class AnyAnyActionParser {
|
|
180
|
+
async process(db, action) {
|
|
181
|
+
db.saveActionToDB(action);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class ActionParserFactory {
|
|
186
|
+
static create(accountName, actionName) {
|
|
187
|
+
switch (`${accountName}::${actionName}`) {
|
|
188
|
+
case "*::*":
|
|
189
|
+
return null;
|
|
190
|
+
default:
|
|
191
|
+
return new AnyAnyActionParser();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function ActionsParser(db, reader) {
|
|
197
|
+
const { actions$ } = reader;
|
|
198
|
+
actions$.subscribe(async (action) => {
|
|
199
|
+
console.log(`
|
|
200
|
+
ACTION - account: ${action.account}, name: ${action.name}, authorization: ${JSON.stringify(action.authorization)}, data: ${JSON.stringify(action.data)}`);
|
|
201
|
+
const parser = ActionParserFactory.create(action.account, action.name);
|
|
202
|
+
const source = subsribedActions.find((el) => el.action === action.name && el.code === action.account);
|
|
203
|
+
if (parser) {
|
|
204
|
+
await parser.process(db, action);
|
|
205
|
+
if (source?.notify)
|
|
206
|
+
await publishEvent("action", action);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
console.log("\u041F\u043E\u0434\u043F\u0438\u0441\u043A\u0430 \u043D\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function BlockParser(db, reader) {
|
|
213
|
+
const { blocks$, errors$, close$ } = reader;
|
|
214
|
+
errors$.subscribe(async (error) => {
|
|
215
|
+
console.log("\n\u041E\u0448\u0438\u0431\u043A\u0430 \u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438: ", error);
|
|
216
|
+
});
|
|
217
|
+
close$.subscribe(async (error) => {
|
|
218
|
+
console.error("\n\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F: ", error);
|
|
219
|
+
console.log("\u0412\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 10 \u0441\u0435\u043A\u0443\u043D\u0434");
|
|
220
|
+
setTimeout(() => process.exit(1), 1e4);
|
|
221
|
+
});
|
|
222
|
+
blocks$.subscribe(async (block) => {
|
|
223
|
+
db.updateCurrentBlock(block.block_num);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class DeltaParser {
|
|
228
|
+
async process(db, delta) {
|
|
229
|
+
db.saveDeltaToDB(delta);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
class DeltaParserFactory {
|
|
234
|
+
static create(code, scope, table) {
|
|
235
|
+
switch (`${code}::${scope}::${table}`) {
|
|
236
|
+
default:
|
|
237
|
+
return new DeltaParser();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function DeltasParser(db, reader) {
|
|
243
|
+
const { rows$ } = reader;
|
|
244
|
+
rows$.subscribe(async (delta) => {
|
|
245
|
+
console.log(`
|
|
246
|
+
DELTA - code: ${delta.code}, scope: ${delta.scope}, table: ${delta.table}, primary_key: ${delta.primary_key}, data: ${JSON.stringify(delta.value)}`);
|
|
247
|
+
const source = subsribedTables.find((el) => el.code === delta.code && el.table === delta.table);
|
|
248
|
+
const parser = DeltaParserFactory.create(delta.code, delta.scope, delta.table);
|
|
249
|
+
if (parser) {
|
|
250
|
+
await parser.process(db, delta);
|
|
251
|
+
if (source?.notify)
|
|
252
|
+
await publishEvent("delta", delta);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
console.log("\u041F\u043E\u0434\u043F\u0438\u0441\u043A\u0430 \u043D\u0430 \u0434\u0435\u043B\u044C\u0442\u044B \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const getInfo = () => fetch(`${eosioApi}/v1/chain/get_info`).then((res) => res.json());
|
|
259
|
+
function fetchAbi(account_name) {
|
|
260
|
+
return fetch(`${eosioApi}/v1/chain/get_abi`, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
account_name
|
|
264
|
+
})
|
|
265
|
+
}).then(async (res) => {
|
|
266
|
+
const response = await res.json();
|
|
267
|
+
return {
|
|
268
|
+
account_name,
|
|
269
|
+
abi: response.abi
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const table_rows_whitelist = () => subsribedTables;
|
|
275
|
+
const actions_whitelist = () => subsribedActions;
|
|
276
|
+
console.log(subsribedTables);
|
|
277
|
+
console.log(subsribedActions);
|
|
278
|
+
async function loadReader(db) {
|
|
279
|
+
let currentBlock = await db.getCurrentBlock();
|
|
280
|
+
const info = await getInfo();
|
|
281
|
+
if (currentBlock === 0)
|
|
282
|
+
currentBlock = Number(startBlock);
|
|
283
|
+
console.log("\u0421\u0442\u0430\u0440\u0442\u0443\u0435\u043C \u0441 \u0431\u043B\u043E\u043A\u0430: ", currentBlock);
|
|
284
|
+
console.log("\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u043C \u043D\u0430 \u0431\u043B\u043E\u043A\u0435: ", finishBlock);
|
|
285
|
+
console.log("\u0412\u044B\u0441\u043E\u0442\u0430 \u0446\u0435\u043F\u043E\u0447\u043A\u0438: ", info.head_block_num);
|
|
286
|
+
console.log("\u041E\u0447\u0438\u0449\u0430\u0435\u043C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0438 \u0434\u0435\u043B\u044C\u0442\u044B \u043F\u043E\u0441\u043B\u0435 \u0431\u043B\u043E\u043A\u0430: ", currentBlock);
|
|
287
|
+
await db.purgeAfterBlock(currentBlock);
|
|
288
|
+
const unique_contract_names = [...new Set(table_rows_whitelist().map((row) => row.code)), ...new Set(actions_whitelist().map((row) => row.code))];
|
|
289
|
+
const abisArr = await Promise.all(unique_contract_names.map((account_name) => fetchAbi(account_name)));
|
|
290
|
+
const contract_abis = () => {
|
|
291
|
+
const numap = /* @__PURE__ */ new Map();
|
|
292
|
+
abisArr.forEach(({ account_name, abi }) => numap.set(account_name, abi));
|
|
293
|
+
return numap;
|
|
294
|
+
};
|
|
295
|
+
const delta_whitelist = () => [
|
|
296
|
+
"account_metadata",
|
|
297
|
+
"contract_table",
|
|
298
|
+
"contract_row",
|
|
299
|
+
"contract_index64",
|
|
300
|
+
"resource_usage",
|
|
301
|
+
"resource_limits_state"
|
|
302
|
+
];
|
|
303
|
+
const eosioReaderConfig = {
|
|
304
|
+
ws_url: shipApi,
|
|
305
|
+
rpc_url: eosioApi,
|
|
306
|
+
ds_threads: 2,
|
|
307
|
+
ds_experimental: false,
|
|
308
|
+
delta_whitelist,
|
|
309
|
+
table_rows_whitelist,
|
|
310
|
+
actions_whitelist,
|
|
311
|
+
contract_abis,
|
|
312
|
+
request: {
|
|
313
|
+
start_block_num: currentBlock,
|
|
314
|
+
end_block_num: Number(finishBlock),
|
|
315
|
+
// info.head_block_num,
|
|
316
|
+
max_messages_in_flight: 50,
|
|
317
|
+
have_positions: [],
|
|
318
|
+
irreversible_only: true,
|
|
319
|
+
fetch_block: true,
|
|
320
|
+
fetch_traces: true,
|
|
321
|
+
fetch_deltas: true
|
|
322
|
+
},
|
|
323
|
+
auto_start: true
|
|
324
|
+
};
|
|
325
|
+
return await createEosioShipReader(eosioReaderConfig);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
class Parser {
|
|
329
|
+
async start() {
|
|
330
|
+
const reader = await loadReader(db);
|
|
331
|
+
try {
|
|
332
|
+
BlockParser(db, reader);
|
|
333
|
+
ActionsParser(db, reader);
|
|
334
|
+
DeltasParser(db, reader);
|
|
335
|
+
} catch (e) {
|
|
336
|
+
console.error("\u041E\u0448\u0438\u0431\u043A\u0430: ", e);
|
|
337
|
+
this.start();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const app = express();
|
|
343
|
+
app.use(express.json());
|
|
344
|
+
app.use(express.urlencoded({ extended: true }));
|
|
345
|
+
const port = process.env.PORT || 4e3;
|
|
346
|
+
const parser = new Parser();
|
|
347
|
+
init().then(() => {
|
|
348
|
+
app.listen(port, () => {
|
|
349
|
+
console.log(`API \u043E\u0431\u043E\u0437\u0440\u0435\u0432\u0430\u0442\u0435\u043B\u044F \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E \u043D\u0430 http://localhost:${port}`);
|
|
350
|
+
if (process.env.ACTIVATE_PARSER === "1")
|
|
351
|
+
parser.start();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
app.get("/get-tables", async (req, res) => {
|
|
355
|
+
const page = Number(req.query.page) || 1;
|
|
356
|
+
const limit = Number(req.query.limit) || 10;
|
|
357
|
+
const filter = req.query.filter ? JSON.parse(req.query.filter) : {};
|
|
358
|
+
const result = await db.getTables(filter, page, limit);
|
|
359
|
+
res.json(result);
|
|
360
|
+
});
|
|
361
|
+
app.get("/get-actions", async (req, res) => {
|
|
362
|
+
const page = Number(req.query.page) || 1;
|
|
363
|
+
const limit = Number(req.query.limit) || 10;
|
|
364
|
+
const filter = req.query.filter ? JSON.parse(req.query.filter) : {};
|
|
365
|
+
const result = await db.getActions(filter, page, limit);
|
|
366
|
+
res.json(result);
|
|
367
|
+
});
|
|
368
|
+
app.get("/get-current-block", async (req, res) => {
|
|
369
|
+
const result = await db.getCurrentBlock();
|
|
370
|
+
res.json(result);
|
|
371
|
+
});
|
|
372
|
+
app.use((err, req, res, _next) => {
|
|
373
|
+
console.error("\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430: ", err);
|
|
374
|
+
res.status(500).send(err.message);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
export { parser };
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coopenomics/parser",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "2.2.0",
|
|
5
|
+
"private": false,
|
|
6
|
+
"packageManager": "pnpm@9.0.6",
|
|
7
|
+
"description": "",
|
|
8
|
+
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"homepage": "https://github.com/copenomics/cooparser#readme",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/copenomics/cooparser.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": "https://github.com/copenomics/cooparser/issues",
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.mjs",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.mjs",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"typesVersions": {
|
|
29
|
+
"*": {
|
|
30
|
+
"*": [
|
|
31
|
+
"./dist/*",
|
|
32
|
+
"./dist/index.d.ts"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"deploy-testnet": "git checkout testnet && git merge main && git push origin testnet && git checkout main",
|
|
41
|
+
"deploy-production": "git checkout production && git merge testnet && git push origin production && git checkout main",
|
|
42
|
+
"build": "unbuild",
|
|
43
|
+
"dev": "concurrently -n 'PARSER' -c 'bgBlue.white' \"nodemon --watch src --ext ts,js,env --exec 'esno' src/index.ts\"",
|
|
44
|
+
"dev:test": "NODE_ENV=test concurrently -n 'PARSER' -c 'bgBlue.white' \"nodemon --watch src --ext ts,js,env --exec 'esno' src/index.ts\"",
|
|
45
|
+
"lint": "eslint .",
|
|
46
|
+
"prepublishOnly": "nr build",
|
|
47
|
+
"release": "bumpp && npm publish",
|
|
48
|
+
"start": "esno src/index.ts",
|
|
49
|
+
"test": "vitest",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"doc": "typedoc"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@blockmatic/eosio-ship-reader": "^1.2.0",
|
|
55
|
+
"@types/express": "^4.17.21",
|
|
56
|
+
"@types/ws": "^8.5.13",
|
|
57
|
+
"dotenv": "^16.4.5",
|
|
58
|
+
"dotenv-expand": "^11.0.6",
|
|
59
|
+
"eosjs": "^22.1.0",
|
|
60
|
+
"express": "^4.19.2",
|
|
61
|
+
"express-async-errors": "^3.1.1",
|
|
62
|
+
"ioredis": "^5.4.1",
|
|
63
|
+
"mongodb": "^6.5.0",
|
|
64
|
+
"node-fetch": "^3.3.2",
|
|
65
|
+
"typedoc": "^0.25.13",
|
|
66
|
+
"typedoc-plugin-inline-sources": "^1.0.2",
|
|
67
|
+
"ws": "^8.18.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@antfu/eslint-config": "^2.16.0",
|
|
71
|
+
"@antfu/ni": "^0.21.12",
|
|
72
|
+
"@antfu/utils": "^0.7.7",
|
|
73
|
+
"@types/node": "^20.12.7",
|
|
74
|
+
"bumpp": "^9.4.0",
|
|
75
|
+
"concurrently": "^8.2.2",
|
|
76
|
+
"eslint": "^8.57.0",
|
|
77
|
+
"esno": "^4.7.0",
|
|
78
|
+
"nodemon": "^3.1.4",
|
|
79
|
+
"pnpm": "^8.15.7",
|
|
80
|
+
"rimraf": "^5.0.5",
|
|
81
|
+
"simple-git-hooks": "^2.11.1",
|
|
82
|
+
"ts-node": "^10.9.2",
|
|
83
|
+
"typescript": "^5.4.5",
|
|
84
|
+
"unbuild": "^2.0.0",
|
|
85
|
+
"vite": "^5.2.10",
|
|
86
|
+
"vitest": "^1.5.2"
|
|
87
|
+
},
|
|
88
|
+
"gitHead": "b05c17bee481d90c4cd82aa2a34ac428f8263a5f"
|
|
89
|
+
}
|