@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 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;
@@ -0,0 +1,7 @@
1
+ declare class Parser {
2
+ start(): Promise<void>;
3
+ }
4
+
5
+ declare const parser: Parser;
6
+
7
+ export { parser };
@@ -0,0 +1,7 @@
1
+ declare class Parser {
2
+ start(): Promise<void>;
3
+ }
4
+
5
+ declare const parser: Parser;
6
+
7
+ export { parser };
@@ -0,0 +1,7 @@
1
+ declare class Parser {
2
+ start(): Promise<void>;
3
+ }
4
+
5
+ declare const parser: Parser;
6
+
7
+ export { parser };
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
+ }