Синхронізація бекенду з блокчейном
Tags: web3, blockchain, architecture, node
Published: 2023-03-01 07:18:37


Основна ідея Web3 базується на децентралізованому сховищі даних. В ідеалі всі ваші дані можна було б зберігати децентралізовано, але це неможливо — таке зберігання всіх даних було б надзвичайно дорогим (принаймні з існуючими технологіями). Замість цього доцільно використовувати децентралізоване сховище лише для найважливіших даних.

**Тоді де зберігати решту даних?**
— Off-chain. Тобто на вашому бекенді.

Завдяки такому підходу ви отримуєте щось на зразок цибулі: найважливіші дані надійно зберігаються в блокчейні (on-chain), тоді як менш важливі дані формуються навколо цього скелета як зовнішньої оболонки.

![image](https://raw.githubusercontent.com/bytesbay/articles/main/resources/0-ua-2.jpg)

Але такий спосіб зберігання даних значно ускладнює застосування, оскільки фактично дані однієї сутності часто розбиваються на дві бази даних.

Я наведу вам приклад. У вас є елемент, власник якого зберігається on-chain, а його метадані зберігаються off-chain.
Як кінцевий клієнт може отримати весь об’єкт із власником і метаданими?

Є кілька варіантів вирішення цієї проблеми:

1. Перший варіант полягає в тому, щоб створити ендпоінт на бекенді, який надсилає запит RPC-серверу для отримання on-chain об’єкта, а потім приєднує свої метадані до нього разом з даними з бази даних. Цей варіант швидкий у розробці, але має багато недоліків, які швидко проявляються. Зокрема, RPC-запити надзвичайно повільні та неефективні, тому UX усієї програми постраждає. Крім того, більшість провайдерів RPC (навіть платних) мають досить низькі рейт-ліміти, тож у міру зростання програми ви швидко натрапите на них.

![image](https://raw.githubusercontent.com/bytesbay/articles/main/resources/0-ua-3.jpg)

2. Другий варіант — передача збирання об’єкта на сторону клієнта. Клієнт робить RPC-запит, щоб отримати власника, а потім робить запит до вашого API, щоб отримати метадані. Очевидна перевага тут порівняно з першим варіантом полягає в тому, що ви не обмежені рейт-лімітами вашого RPC-провайдера. Серед недоліків — цей варіант значно ускладнює ваш фронтенд, та все ще не вирішує проблему продуктивності через те, що майже всі користувачі використовують RPC-сервери надзвичайно низької якості. Відомо, що ці сервери час від часу виходять з ладу або зависають на невизначений термін (особливо для BNB Smart Chain). Крім того, цей спосіб позбавляє вас можливості централізованого та зручного пошуку чи фільтрації товарів (наприклад, пошук у маркетплейсі).

![image](https://raw.githubusercontent.com/bytesbay/articles/main/resources/0-ua-4.jpg)

Я спробував обидва варіанти під час розробки, і можу сказати, що жоден метод не дуже добре масштабується. Загалом вони обоє надто «костильні».
Треба якось абстрагуватися від роботи з RPC і зробити свою базу єдиним джерелом правди. Цього можна досягти, перехоплюючи всі зміни on-chain (за допомогою подій) і негайно записуючи їх у базу даних. Це серйозно спрощує подальшу розробку dApp.

Хтось може подумати: “Чому б не використати **TheGraph**?” У цьому випадку він може замінити лише RPC-запити. Це не вирішує фундаментальної проблеми — дані все ще розʼєднанні.

Тому я розробив модуль **chain-syncer**, який синхронізує бекенд з блокчейном. Щоб встановити його, використовуйте команду:

```bash
$ npm i chain-syncer @chainsyncer/mongodb-adapter ethers
```

Розглянемо простий приклад використання модуля. Завдання полягає в тому, щоб мати значення поточного власника елемента в базі даних.

```js
// Example using Mongoose
// This is just an abstract example, dont try to copy-paste it

const Mongoose = require('mongoose');
const { ChainSyncer } = require('chain-syncer');
const { MongoDBAdapter } = require('@chainsyncer/mongodb-adapter');

// where will we store the pulled events
await Mongoose.connect(process.env['MONGO_SRV']);
const adapter = new MongoDBAdapter(Mongoose.connection.db);

const syncer = new ChainSyncer(adapter, { /* ... some options ... */ })

syncer.on('Items.Transfer', async (
  { global_index, from_address, block_number, block_timestamp, transaction_hash },
  from, 
  to, 
  token_id,
) => {

  // getting the item by id 
  const item = await Item.findOne({ _id: token_id });
  
  // updating owner address
  item.owner = to;
  
  // saving the item
  await item.save();

});
```

Що відбувається в цьому фрагменті коду? Модуль просканує всі блоки, починаючи з блоку, на якому розгорнуто контракт `Items` і витягне з них усі події контракту. Під час роботи сканера події, які вже були витягнуті, одночасно проходитимуть через наш обробник.
Як тільки модуль підтягує всі старі події, він починає відстежувати нові. Таким чином ми отримуємо відстеження власника кожного предмета в реальному часі.

![image](https://raw.githubusercontent.com/bytesbay/articles/main/resources/0-ua-5.jpg)

Після обробки події на ній ставиться прапорець і вона відправляється в архів.

На даний момент існує лише адаптер для MongoDB. Ваші розробки для різноманітних інших сховищ, таких як **Postgres**, MySQL і OracleDB, широко вітаються.
Також зараз розробляється розширення модуля (разом із пакетом Helm), яке зробить модуль подібним до RabbitMQ. Це означає, що його буде зручно використовувати в архітектурі мікросервісу.



У [наступній статті](https://dev.to/bytesbay/chainsyncer-cookbook-4okm) я напишу рекомендації по роботі з модулем і навіщо потрібен параметр `global_index`.



---

Друзі, не забуваємо донатити на 🇺🇦 ЗСУ. Залишу [посилання](https://aid.prytulafoundation.org/en/) для тих хто в танку.