This commit is contained in:
Alex Ant
2024-06-01 10:52:42 +05:00
parent 8bc064a1de
commit 7716262059
13 changed files with 876 additions and 1613 deletions
+7 -1
View File
@@ -1,3 +1,9 @@
NODE_ENV=development
API=http://127.0.0.1:8888
SHIP=ws://127.0.0.1:8080
MONGO_URI=mongodb://127.0.0.1:27017/cooperative
MONGO_EXPLORER_URI=mongodb://127.0.0.1:27017/cooperative
START_BLOCK=1
FINISH_BLOCK=0xFFFFFFFF
PORT=4000
ACTIVATE_PARSER=0
+123 -3
View File
@@ -6,11 +6,131 @@
[![JSDocs][jsdocs-src]][jsdocs-href]
[![License][license-src]][license-href]
Пакет производит распаковку блоков, сохраняя действия и дельты таблиц в базу данных mongodb. Действия, на которые осуществляется подписка, содержат информацию, которая используется командным кошельком для формирования документа.
Пакет производит распаковку блоков, сохраняя действия и дельты таблиц и выдавая их по API. Состоит из двух модулей: парсера и API. Парсер считывает данные из блокчейна и помещает их в базу. API получает данные по запросу и возвращает их с пагинацией.
## License
## Установка
```
pnpm install
```
[MIT](./LICENSE) License © 2024-PRESENT [Digital Cooperative](https://github.com/copenomics)
### Конфигурационный файл .env
```
NODE_ENV=development
- Определяет среду выполнения приложения.
API=http://127.0.0.1:8888
- Определяет URL-адрес API, к которому будет осуществляться доступ.
SHIP=ws://127.0.0.1:8080
- Определяет URL-адрес WebSocket-соединения, используемого для связи с другими узлами.
MONGO_EXPLORER_URI=mongodb://127.0.0.1:27017/cooperative
- Определяет URI-адрес MongoDB, используемый для подключения к базе данных.
START_BLOCK=1
- Определяет номер блока, с которого начинается парсинг блокчейна.
FINISH_BLOCK=0xFFFFFFFF
- Определяет номер блока, на котором заканчивается парсинг блокчейна. В данном случае, установлено значение "0xFFFFFFFF", что означает, что парсинг будет продолжаться до последнего доступного блока.
PORT=4000
- Определяет порт, на котором будет запущен сервер приложения.
ACTIVATE_PARSER=0
- Определяет флаг активации парсера. Если значение равно "1", парсер будет активирован.
```
### Конфигурация парсера
В конфиге src/config.ts находится массив таблиц и действий, на которые парсер осуществит подписку.
```
export const subsribedTables = [
{ code: 'registrator', table: 'users', 'scope': 'registrator' },
{ code: 'soviet', table: 'participants' },
]
```
- подписка будет осуществлена на изменения таблиц указанных контрактов. Параметр scope - не обязательный. Без его указания любые scope будут попадать в базу данных.
```
export const subsribedActions = [
{ code: 'soviet', action: 'votefor' },
{ code: 'soviet', action: 'voteagainst' },
]
```
- подписка будет осуществлена на действия указанных контрактов.
Парсер может быть расширен любыми кастомными действиями, которые будут выполняться перед добавлением записи в базу данных. Для этого, для таблиц и действий соответственно, в папках src/ActionParser/Actions и src/DeltaParser/Deltas необходимо создать файлы с методами обработки и добавить их к src/ActionParser/Actions или src/DeltaParser/DeltaFactory.
### Запуск
```
pnpm start
```
## API
### Получение таблиц
Конечная точка предоставляет информацию о изменении (дельтах) таблиц между блоками.
```
let params = {
page: 1,
limit: 10,
filter: { } - любые параметры фильтрации таблицы, включая данные в полях
};
axios.get('http://localhost:4000/get-tables', { params })
.then(response => {
console.log(response.data);
// {
// results: array,
// page: number,
// limit: number
// };
})
```
### Получение действий
Конечная точка предоставляет информацию о действиях, произошедших между блоками.
```
let params = {
page: 1,
limit: 10,
filter: { } // любые параметры фильтрации действий, включая данные в полях
};
axios.get('http://localhost:4000/get-actions', { params })
.then(response => {
console.log(response.data);
// {
// results: array,
// page: number,
// limit: number
// };
})
.catch(error => {
console.error(error);
});
```
### Получение текущего блока
Конечная точка предоставляет информацию о текущем блоке. Эта информация используется при формировании кооперативных документов.
```
axios.get('http://localhost:4000/get-current-block')
.then(response => {
console.log(response.data);
// number
})
.catch(error => {
console.error(error);
});
```
## Лицензия
[MIT](./LICENSE) License © 2024-PRESENT [CBS VOSKHOD](https://github.com/copenomics)
<!-- Badges -->
+8 -3
View File
@@ -42,18 +42,23 @@
"prepublishOnly": "nr build",
"release": "bumpp && npm publish",
"start": "esno src/index.ts",
"parse": "esno test/parse.ts",
"test": "vitest",
"typecheck": "tsc --noEmit",
"prepare": "simple-git-hooks"
"prepare": "simple-git-hooks",
"doc": "typedoc"
},
"dependencies": {
"@blockmatic/eosio-ship-reader": "^1.2.0",
"@types/express": "^4.17.21",
"dotenv": "^16.4.5",
"dotenv-expand": "^11.0.6",
"eosjs": "^22.1.0",
"express": "^4.19.2",
"express-async-errors": "^3.1.1",
"mongodb": "^6.5.0",
"node-fetch": "^3.3.2"
"node-fetch": "^3.3.2",
"typedoc": "^0.25.13",
"typedoc-plugin-inline-sources": "^1.0.2"
},
"devDependencies": {
"@antfu/eslint-config": "^2.16.0",
+611 -1487
View File
File diff suppressed because it is too large Load Diff
+33 -16
View File
@@ -24,21 +24,21 @@ export class Database {
async saveActionToDB(action: any): Promise<void> {
if (!this.actions)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
await this.actions.insertOne(action)
}
async saveDeltaToDB(delta: any): Promise<void> {
if (!this.deltas)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
await this.deltas.insertOne(delta)
}
async getDelta(filter: Filter<IDelta>): Promise<any> {
if (!this.deltas)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
const result = await this.deltas.findOne(filter as any)
@@ -47,13 +47,13 @@ export class Database {
async getTables(filter?: Filter<IDelta>, page: number = 1, limit: number = 10): Promise<ITableResult> {
if (!this.deltas)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
const pipeline = [
{ $match: filter },
{ $sort: { primary_key: -1 } },
{ $sort: { block_num: -1, _id: -1 } },
{ $group: { _id: '$primary_key', doc: { $first: '$$ROOT' } } },
{ $match: { 'doc.present': true } },
// { $match: { 'doc.present': true } },
{ $replaceRoot: { newRoot: '$doc' } },
]
@@ -65,7 +65,7 @@ export class Database {
])
return {
results: result as IDelta[], // Change the type to IDelta[]
results: result as IDelta[],
page,
limit,
}
@@ -73,13 +73,24 @@ export class Database {
async getActions(filter?: Filter<IAction>, page: number = 1, limit: number = 10): Promise<IActionResult> {
if (!this.actions)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
const query = filter || {}
const result = await this.actions.find(query as any)
.skip((page - 1) * limit)
.limit(limit)
.toArray()
const result = await this.actions.aggregate([
{ $match: query },
{ $sort: { block_num: -1, _id: -1 } },
{ $group: { _id: '$global_sequence', doc: { $first: '$$ROOT' } } },
{ $replaceRoot: { newRoot: '$doc' } },
{ $skip: (page - 1) * limit },
{ $limit: limit },
]).toArray()
// const result = await this.actions.find(query as any)
// .skip((page - 1) * limit)
// .sort({ block_num: -1, _id: -1 })
// .limit(limit)
// .toArray()
return {
results: result as unknown as IAction[],
@@ -90,7 +101,7 @@ export class Database {
async getAction(filter: Filter<Filter<IAction>>): Promise<any> {
if (!this.actions)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
const result = await this.actions.findOne(filter as any)
@@ -99,7 +110,7 @@ export class Database {
async getCurrentBlock(): Promise<number> {
if (!this.sync)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
const currentBlockDocument = await this.sync.findOne({ key: 'currentBlock' })
@@ -108,14 +119,14 @@ export class Database {
async updateCurrentBlock(block_num: number): Promise<void> {
if (!this.sync)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
await this.sync.updateOne({ key: 'currentBlock' }, { $set: { block_num } }, { upsert: true })
}
async purgeAfterBlock(since_block: number) {
if (!this.actions || !this.deltas)
throw new Error('Database not connected')
throw new Error('База данных не подключена')
await this.actions.deleteMany({ block_num: { $gt: since_block } })
await this.deltas.deleteMany({ block_num: { $gt: since_block } })
@@ -123,3 +134,9 @@ export class Database {
}
export const db = new Database()
export async function init() {
return db.connect().then(() => {
console.log('База данных инициализирована')
})
}
+14
View File
@@ -0,0 +1,14 @@
import { ActionsParser } from '../ActionParser'
import { BlockParser } from '../BlockParser/Parser/BlockParser'
import { db } from '../Database'
import { DeltasParser } from '../DeltaParser'
import { loadReader } from '../Reader'
export class Parser {
async start() {
const reader = await loadReader(db)
BlockParser(db, reader)
ActionsParser(db, reader)
DeltasParser(db, reader)
}
}
+5 -49
View File
@@ -12,53 +12,12 @@ function getEnvVar(key: string): string {
export const eosioApi = getEnvVar('API')
export const shipApi = getEnvVar('SHIP')
export const mongoUri = getEnvVar('MONGO_URI')
export const mongoUri = getEnvVar('MONGO_EXPLORER_URI')
export const startBlock = getEnvVar('START_BLOCK')
export const finishBlock = getEnvVar('FINISH_BLOCK')
/// ///////////////////// TEST
const subsribedTablesTest = [
// токены и балансы
// { code: 'eosio.token', table: 'stat' },
// { code: 'eosio.token', table: 'accounts' },
// документы
{ code: 'draft', table: 'drafts' },
{ code: 'draft', table: 'translations' },
// совет
{ code: 'soviet', table: 'decisions' },
{ code: 'soviet', table: 'boards' },
{ code: 'soviet', table: 'participants' },
// registrator.joincoop
{ code: 'soviet', table: 'joincoops' },
// регистратор
{ code: 'registrator', table: 'accounts' },
{ code: 'registrator', table: 'orgs' },
]
const subsribedActionsTest = [
// // токены и балансы
{ code: 'soviet', action: 'votefor' },
{ code: 'soviet', action: 'voteagainst' },
{ code: 'soviet', action: 'newdraft' },
{ code: 'soviet', action: 'newstatement' },
{ code: 'soviet', action: 'newdecision' },
// // registrator.joincoop
{ code: 'soviet', action: 'joincoop' },
{ code: 'soviet', action: 'joincoopdec' },
]
// --------------------------
/// //////////////////// PROD
const subsribedTablesProd = [
export const subsribedTables = [
// документы
{ code: 'draft', table: 'drafts' },
{ code: 'draft', table: 'translations' },
@@ -76,11 +35,11 @@ const subsribedTablesProd = [
{ code: 'registrator', table: 'orgs' },
]
const subsribedActionsProd = [
export const subsribedActions = [
{ code: 'soviet', action: 'votefor' },
{ code: 'soviet', action: 'voteagainst' },
{ code: 'soviet', action: 'newdraft' },
{ code: 'soviet', action: 'newstatement' },
{ code: 'soviet', action: 'newsubmitted' },
{ code: 'soviet', action: 'newresolved' },
{ code: 'soviet', action: 'newdecision' },
@@ -91,6 +50,3 @@ const subsribedActionsProd = [
]
// --------------------------
export const subsribedTables = getEnvVar('NODE_ENV') === 'production' ? subsribedTablesProd : subsribedTablesTest
export const subsribedActions = getEnvVar('NODE_ENV') === 'production' ? subsribedActionsProd : subsribedActionsTest
+43 -28
View File
@@ -1,35 +1,50 @@
import express from 'express'
import 'express-async-errors'
import type { Filter } from 'mongodb'
import { db } from './Database'
import { ActionsParser } from './ActionParser'
import { DeltasParser } from './DeltaParser'
import { loadReader } from './Reader'
import { BlockParser } from './BlockParser/Parser/BlockParser'
import type { IAction, IActionResult, IDelta, ITableResult } from './Types'
import { db, init } from './Database'
import type { IAction, IDelta } from './Types'
import { Parser } from './Parser'
export async function init() {
return db.connect().then(() => {
console.log('parser databases connected')
const app = express()
app.use(express.json()) // Для обработки JSON-запросов
app.use(express.urlencoded({ extended: true })) // Для обработки URL-encoded запросов
const port = process.env.PORT || 4000
export const parser = new Parser()
init().then(() => {
app.listen(port, () => {
console.log(`API обозревателя запущено на http://localhost:${port}`)
if (process.env.ACTIVATE_PARSER === '1')
parser.start()
})
}
})
export async function getTables(filter?: Filter<IDelta>, page: number = 1, limit: number = 10): Promise<ITableResult> {
return await db.getTables(filter, page, limit)
}
app.get('/get-tables', async (req: any, res: any) => {
const page = Number(req.query.page) || 1
const limit = Number(req.query.limit) || 10
const filter: Filter<IDelta> = req.query.filter ? JSON.parse(req.query.filter) : {}
const result = await db.getTables(filter, page, limit)
res.json(result)
})
export async function getActions(filter?: Filter<IAction>, page: number = 1, limit: number = 10): Promise<IActionResult> {
return await db.getActions(filter, page, limit)
}
app.get('/get-actions', async (req: any, res: any) => {
const page = Number(req.query.page) || 1
const limit = Number(req.query.limit) || 10
const filter: Filter<IAction> = req.query.filter ? JSON.parse(req.query.filter) : {}
const result = await db.getActions(filter, page, limit)
res.json(result)
})
export async function getCurrentBlock() {
return await db.getCurrentBlock()
}
app.get('/get-current-block', async (req: any, res: any) => {
const result = await db.getCurrentBlock()
res.json(result)
})
export class Parser {
async start() {
await init()
const reader = await loadReader(db)
BlockParser(db, reader)
ActionsParser(db, reader)
DeltasParser(db, reader)
}
}
// Глобальный обработчик ошибок
app.use((err: any, req: any, res: any, _next: any) => {
console.error(err)
res.status(500).send(err.message)
})
+9 -9
View File
@@ -1,12 +1,12 @@
import { getCurrentBlock, init } from '../src'
// import { describe, expect, it } from 'vitest'
describe('получаем текущий блок', () => {
it('получили блок', async () => {
await init()
const current_block = await getCurrentBlock()
// describe('получаем текущий блок', () => {
// it('получили блок', async () => {
// await init()
// const current_block = await getCurrentBlock()
console.log('current_block: , ', current_block)
// console.log('current_block: , ', current_block)
expect(current_block).toBeDefined()
})
})
// expect(current_block).toBeDefined()
// })
// })
-5
View File
@@ -1,5 +0,0 @@
import { Parser, init } from '../src'
await init()
const parser = new Parser()
await parser.start()
+11 -11
View File
@@ -1,17 +1,17 @@
import { getActions, getCurrentBlock, getTables, init } from '../src'
import type { IAction } from '../src/Types'
// import { getActions, getCurrentBlock, getTables, init } from '../src'
// import type { IAction } from '../src/Types'
await init()
const current_block = await getCurrentBlock()
// await init()
// const current_block = await getCurrentBlock()
console.log('current_block: ', current_block)
// console.log('current_block: ', current_block)
const filter = { 'value.username': 'dqcnrmjkrglx' }
// const filter = { 'value.username': 'dqcnrmjkrglx' }
const deltas = await getTables(filter, 1, 1)
console.log('deltas: ', deltas)
// const deltas = await getTables(filter, 1, 1)
// console.log('deltas: ', deltas)
const filter_actions = { name: 'newdecision' } as IAction
// const filter_actions = { name: 'newdecision' } as IAction
const actions = await getActions(filter_actions, 1, 1)
console.log('actions: ', actions)
// const actions = await getActions(filter_actions, 1, 1)
// console.log('actions: ', actions)
+1 -1
View File
@@ -3,7 +3,7 @@
"target": "ESNext",
"lib": ["ESNext"],
"module": "ESNext",
"moduleResolution": "Bundler",
"moduleResolution": "Node",
"resolveJsonModule": true,
"strict": true,
"strictNullChecks": true,
+11
View File
@@ -0,0 +1,11 @@
{
"entryPoints": ["src/index.ts"],
"out": "docs",
"includeVersion": true,
"hideGenerator": true,
"plugin": [
"typedoc-plugin-inline-sources"
],
"theme": "default",
"excludePrivate": false
}