release
This commit is contained in:
+7
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+611
-1487
File diff suppressed because it is too large
Load Diff
+33
-16
@@ -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('База данных инициализирована')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
// })
|
||||
// })
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Parser, init } from '../src'
|
||||
|
||||
await init()
|
||||
const parser = new Parser()
|
||||
await parser.start()
|
||||
+11
-11
@@ -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
@@ -3,7 +3,7 @@
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"entryPoints": ["src/index.ts"],
|
||||
"out": "docs",
|
||||
"includeVersion": true,
|
||||
"hideGenerator": true,
|
||||
"plugin": [
|
||||
"typedoc-plugin-inline-sources"
|
||||
],
|
||||
"theme": "default",
|
||||
"excludePrivate": false
|
||||
}
|
||||
Reference in New Issue
Block a user