Индексирование данных смарт-контрактов с помощью протокола Graph


Введение

Умные контракты генерируют события, которые являются богатым источником данных, которые можно агрегировать. Например, при передаче токенов токен ERC20 генерирует событие передачи. Разработчик или пользователь может индексировать и агрегировать эти данные о событиях передачи, а затем выполнить запросы, чтобы узнать больше о производительности токена, например, о ведущих держателях, объеме транзакций и т.д.

Данные блокчейна могут быть организованы с помощью технологии децентрализованного индексирования, известной как Graph Protocol. В нем используется токен Graph Token (GRT), токен ERC20, в качестве финансового стимула для обеспечения безопасности сети. Протокол позволяет участвовать в четырех различных ролях, а именно:

  • Разработчик
  • Индексатор
  • Делегатор
  • Куратор

Разработчик

Подграф создается разработчиком, который пишет схему и создает сопоставления для извлечения данных в сущности схемы. Подграф развертывается в сети Graph, где индексатор индексирует его.

Индексатор

Узел в сети Graph Network управляется индексатором. Индексатор устанавливает GRT, чтобы он мог запускать узел и индексировать подграфы. Индексатор получает компенсацию в виде платы за запросы к сети.

Делегатор

Делегатор — это человек, который делает ставку GRT на индексатора. Делегатор не обязан запускать узел, но он может получить выгоду от платы за запросы, делая ставку GRT на индексатора.

Куратор

Куратор — это человек, у которого достаточно денег, чтобы сообщить индексаторам, какие подграфы стоит индексировать. Они проверяют качество подграфов и присваивают им вес, используя GRT в качестве сигналов на подграфы, чтобы индексаторы могли найти и проиндексировать подграф.

Подробнее о ролях вы можете прочитать здесь.

Создание подграфа

Перейдите в Graph Studio и подключите свой кошелек, подписав транзакцию. (Эт не будет взиматься). Затем нажмите на кнопку Create Subgraph для создания и выберите сеть блокчейна, которую вы хотите, чтобы подграф индексировал. Я выбрал сеть Ethereum Mainnet, потому что хочу индексировать токен Maker Dao, найденный по этому адресу. (0x6B175474E89094C44Da98b954EedeAC495271d0F).

Далее дайте своему подграфу уникальное имя (в моем случае я назвал свой daitoken) и нажмите на кнопку продолжить. Заполните поля, описывающие, что делает подграф, и нажмите на кнопку сохранить.

Затем мы продолжим и создадим подграф на нашей машине разработки.

Запишите имя подграфа. Оно понадобится при создании подграфа на нашей машине.

Установите инструмент Graph CLI в нашей системе, набрав в терминале приведенный ниже код:

На вашем компьютере должен быть установлен Node

 npm install -g @graphprotocol/graph-cli
Войдите в полноэкранный режим Выйти из полноэкранного режима

После установки CLI нам нужно будет инициировать подграф, используя имя slug, которое мы создали ранее в Graph Studio.

graph init --studio <slug_name>
Войти в полноэкранный режим Выйти из полноэкранного режима

Замените slug_name на имя созданного вами slug. Моим было daitoken, поэтому моя команда для инициализации будет такой:

graph init --studio daitoken
Войти в полноэкранный режим Выйти из полноэкранного режима

После выполнения вышеуказанной команды, инструмент задаст вам следующий вопрос:

  • Выберите протокол сети : вы должны выбрать Ethereum
  • Подтвердите имя slug: нажмите на enter
  • Каталог, в котором будет создан подграф
  • Выберите сеть Ethereum: mainnet
  • Адрес контракта (адрес контракта, который вы хотите проиндексировать) : 0x6B175474E89094C44Da98b954EedeAC495271d0F (это токен Maker Dai )Инструмент получит ABI из Etherscan, если это не удалось, вам придется скомпилировать контракт самостоятельно и передать местоположение ABI инструменту.
  • Название контракта : название контракта, который вы индексируете.

Наконец, инструмент сгенерирует и создаст ваш подграф. Перейдите в созданную папку и посмотрите на ее содержимое. Нас больше всего интересуют три файла:

  • schema.graphql
  • subgraph.yaml
  • src/mappings.ts

Создание схемы

Сущности подграфа содержатся в схеме. Сущность аналогична таблице в базе данных, а ее поля аналогичны столбцам. Сущность может иметь связь с другой сущностью. Вот как представляется сущность:

  type Customer @entity {
    id: ID!
    name: String!
    address: Bytes!
  }

Войти в полноэкранный режим Выход из полноэкранного режима

Каждая сущность должна иметь поле id, которое не может быть null и обозначается знаком !, указывающим, что поле не может быть нулевым.

Если сущность не будет обновляться после ее создания, она обычно записывается как неизменяемая; это повышает производительность.

  //Immutable Customer Entity
  type Customer @entity(immutable: true) {
    id: ID!
    name: String!
    address: Bytes!
  }
Вход в полноэкранный режим Выход из полноэкранного режима

При представлении отношения «один ко многим» между сущностями одна сторона сохраняется, а многие стороны выводятся. Рассмотрим отношение «один-ко-многим» между сущностью Customer и сущностью Orders. Одна сторона отношения сохраняется в Orders, в то время как многие стороны выводятся в Customer.

   type Orders @entity {
     id : ID!
     amount: BigInt!
     token: String!
     buyer: Customer!
   }

    type Customer @entity {
    id: ID!
    name: String!
    address: Bytes!
    orders: [ Orders!]! @derivedFrom(field: "buyer")
  }
Перейдите в полноэкранный режим Выход из полноэкранного режима

Мы сохраняем значения только для полей id, name и address при отображении данных для сущности Customer. Когда мы запрашиваем сущность Customer, поле orders будет получено из сущности ‘Orders’.

Схема для токена Maker Dai будет состоять из сущности User, сущности UserCounter и сущности TransferCounter. Созданные сущности будут сопоставлены с данными внутри файла mappings.ts. Откройте файл schema.graphql и вставьте приведенный ниже код.

type User @entity {
  id: ID!
  address: String!
  balance: BigInt!
  transactionCount: Int!
}

type UserCounter @entity {
  id: ID!
  count: Int!
}

type TransferCounter @entity {
  id: ID!
  count: Int!
  totalTransferred: BigInt!
}

Войти в полноэкранный режим Выйти из полноэкранного режима

Сущность User содержит поля id, address, balance и transactionCount. Эти поля разрешаются в типы ID!, String, BigInt и Int соответственно. ! означает, что поле не может быть нулевым. Нас интересует сохранение пользователей токена, их баланс и количество совершенных ими транзакций. Нас также интересует общее количество пользователей и общее количество совершенных ими переводов. Далее мы отредактируем файл манифеста.

Обновление файла манифеста.

Откройте файл subgraph.yaml, который представляет собой файл конфигурации. Здесь мы перечислим сущности, созданные выше, а также определим events на интересующем нас смарт-контракте и handlers для извлечения данных в сущности.

specVersion: 0.0.2
schema:
  file: ./schema.graphql
dataSources:
  - kind: ethereum
    name: DaiToken
    network: mainnet
    source:
      address: "0x6B175474E89094C44Da98b954EedeAC495271d0F"
      abi: DaiToken
      startBlock: 8928158
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.5
      language: wasm/assemblyscript
      entities:
        - User
        - UserCounter
        - TransferCounter
      abis:
        - name: DaiToken
          file: ./abis/DaiToken.json
      eventHandlers:
        - event: Transfer(indexed address,indexed address,uint256)
          handler: handleTransfer
      file: ./src/mapping.ts
Вход в полноэкранный режим Выход из полноэкранного режима

Параметр startBlock, который находится под ключом source, указывает, с какого блока в блокчейне должна начинаться индексация. Если не указать startBlock, индексация начнется с блока genesis. Блок, на котором был создан контракт, можно получить в Etherscan.

Под ключом entities мы перечислили созданные нами сущности из :

Мы определили обработчик, который будет срабатывать при возникновении события Transfer в контракте. Этот обработчик называется handleTransfer и он будет определен внутри файла mapping.ts.

Наконец, выполните приведенную ниже команду для автоматической генерации типов:

 graph codegen
Войти в полноэкранный режим Выйти из полноэкранного режима

Когда мы изменим наши сущности, мы должны помнить о необходимости выполнения вышеуказанной команды, чтобы правильно сгенерировать типы.

Создание обработчика отображения

Откройте файл отображения, расположенный в директории src/mapping.ts. Картографирование подграфа написано на языке AssemblyScript, который похож на Typescript.


import { Transfer as TransferEvent } from "../generated/DaiToken/DaiToken";
import { User, UserCounter, TransferCounter } from "../generated/schema";
import { BigInt } from "@graphprotocol/graph-ts";

export function handleTransfer(event: TransferEvent): void {
  let day = event.block.timestamp.div(BigInt.fromI32(60 * 60 * 24));

  let userFrom = User.load(event.params.src.toHex());
  if (userFrom == null) {
    userFrom = newUser(event.params.src.toHex(), event.params.src.toHex());
  }
  userFrom.balance = userFrom.balance.minus(event.params.wad);
  userFrom.transactionCount = userFrom.transactionCount + 1;
  userFrom.save();

  let userTo = User.load(event.params.dst.toHex());
  if (userTo == null) {
    userTo = newUser(event.params.dst.toHex(), event.params.dst.toHex());

    // UserCounter
    let userCounter = UserCounter.load("singleton");
    if (userCounter == null) {
      userCounter = new UserCounter("singleton");
      userCounter.count = 1;
    } else {
      userCounter.count = userCounter.count + 1;
    }
    userCounter.save();
    userCounter.id = day.toString();
    userCounter.save();
  }
  userTo.balance = userTo.balance.plus(event.params.wad);
  userTo.transactionCount = userTo.transactionCount + 1;
  userTo.save();

  // Transfer counter total and historical
  let transferCounter = TransferCounter.load("singleton");
  if (transferCounter == null) {
    transferCounter = new TransferCounter("singleton");
    transferCounter.count = 0;
    transferCounter.totalTransferred = BigInt.fromI32(0);
  }
  transferCounter.count = transferCounter.count + 1;
  transferCounter.totalTransferred = transferCounter.totalTransferred.plus(
    event.params.wad
  );
  transferCounter.save();
  transferCounter.id = day.toString();
  transferCounter.save();
}

function newUser(id: string, address: string): User {
  let user = new User(id);
  user.address = address;
  user.balance = BigInt.fromI32(0);
  user.transactionCount = 0;
  return user;
}

Войти в полноэкранный режим Выход из полноэкранного режима

Мы импортировали наш тип Transfer и переименовали его в TransferEvent в верхней части файла. Для генерации типов нам пришлось использовать graph codegen. Мы также импортировали наши сущности User, UserCounter и TransferCounter.

import { Transfer as TransferEvent } from "../generated/DaiToken/DaiToken";
import { User, UserCounter, TransferCounter } from "../generated/schema";
Вход в полноэкранный режим Выход из полноэкранного режима

Внутри файла мы определили и экспортировали функцию со следующей сигнатурой.

 export function handleTransfer(event: TransferEvent): void 
 {}
Вход в полноэкранный режим Выйти из полноэкранного режима

Эта функция получает в качестве параметра TransferEvent. Эта функция будет вызываться для каждого события передачи, которое было создано в контракте. Функция не возвращает никаких результатов, поэтому тип возврата void.

Все данные, которые мы хотим сохранить в узле Graph, содержатся во входном параметре event. Мы извлекаем день из временной метки, разделив его на 86400, поскольку эта переменная будет использоваться в качестве идентификатора для сохранения уникальных транзакций и количества пользователей, достигнутых за каждый день.

let day = event.block.timestamp.div(BigInt.fromI32(60 * 60 * 24));
Вход в полноэкранный режим Выход из полноэкранного режима

Событие перевода содержит информацию о пользователе, совершившем перевод, получателях перевода и сумме перевода. Для представления этой информации мы создали переменные userFrom и userTo.

let userFrom = User.load(event.params.src.toHex());
Вход в полноэкранный режим Выход из полноэкранного режима

Мы загружаем объект User из хранилища Graph, если он существует, используя адрес кошелька пользователя, сохраненный в свойстве event.params.src. Используя метод .toHex(), мы преобразуем адрес кошелька в шестнадцатеричный.

Если userFrom равно null, мы используем служебную функцию newUser для создания и возврата нового «пользователя». Мы вычитаем сумму перевода из баланса в userFrom и увеличиваем счетчик транзакций userFrom на единицу перед сохранением сущности в хранилище.

Переменная userTo является бенефициаром передачи, мы проверяем, существует ли userTo, загружая данные из магазина.

 let userTo = User.load(event.params.dst.toHex());
Вход в полноэкранный режим Выход из полноэкранного режима

Если userTo равен null, мы создаем нового пользователя с помощью функции newUser. Поскольку этот адрес userTo является новым, мы хотим добавить его в сущность UserCounter. Мы хотим сохранить и увеличить поле UserCounter.count, а также сохранить историческое количество пользователей на каждый день.

    let userCounter = UserCounter.load("singleton");
Вход в полноэкранный режим Выйти из полноэкранного режима

Эта строка загружает UserCounter из магазина, используя id singleton. Если у нас нет UserCounter, она создает его и сохраняет текущий счетчик.

    if (userCounter == null) {
    //code removed
    userCounter.save();
    userCounter.id = day.toString();
    userCounter.save();
Вход в полноэкранный режим Выход из полноэкранного режима

После сохранения userCounter, созданного с помощью ключа singleton в качестве id, мы определили еще один id, который приравниваем к day.toString() и также сохраняем исторический счетчик дней.

// assuming this is the current user count which we have saved
 userCount = {
       id : "Singleton",
       count: 4567
    }
//assuming day.toString() = 18219
userCount.id = day.toString()

userCount.save()
//calling userCount.save() again will save the example entity //into the store. This is the historical count data
userCount = {
  id : "18219",
  count: 4567
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы сохраняем TransferCounter таким же образом, как и UserCounter. Мы сохраняем в хранилище ежедневный счетчик и сумму переводов, а также кумулятивную сумму суммы переводов и счетчика транзакций.

Развертывание подграфа в студии

Мы должны пройти аутентификацию с терминала, выполнив этот код:

 graph auth --studio <deployment_key>
Войти в полноэкранный режим Выйти из полноэкранного режима

Ваш ключ развертывания можно найти на приборной панели вашей студии Graph.

Далее мы собираем проект, выполнив команду:

 graph build
Войти в полноэкранный режим Выйти из полноэкранного режима

Чтобы развернуть граф в студии, выполните команду :

graph deploy --studio <slug_name>
Войти в полноэкранный режим Выйти из полноэкранного режима

slug_name — это имя подграфа. Оно должно совпадать с тем, что было задано в Graph Studio. Вам будет предложено ввести номер версии развернутого подграфа.

Вы не сможете удалить развернутый подграф.
Вы можете только изменить его и повторно опубликовать как новую версию.

После развертывания моего подграфа мне был предоставлен URL-адрес разработки для тестирования. Служба Graph’s hosted service начнет индексировать подграф. Пройдет некоторое время, прежде чем вы сможете начать делать запросы к подграфу.

Написание запросов к нашему подграфу

Наш развернутый подграф содержит три сущности, а именно User, UserCounter и TransferCounter. Мы можем использовать игровую площадку студии для написания запросов GraphQL для получения наших сущностей. Мы можем получить одну сущность или коллекцию сущностей.

Ниже приведены примеры запросов, которые мы можем сделать к нашему подграфу.

  • Запрос для получения первых 100 записей о пользователях
  {
    users(first: 100){
      id
      address
      balance
      transactionCount
   }
  }
Войти в полноэкранный режим Выйти из полноэкранного режима
  • запрос для получения 10 лучших держателей токенов Dai
  {
  users(first:10, orderBy: balance, orderDirection: desc){
    id
    balance
    transactionCount
    address
  }
}

Войти в полноэкранный режим Выйти из полноэкранного режима
  • запрос для получения общего количества пользователей, использовавших токен Dai
 {
   userCounter(id: "singleton"){
     id
     count
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима
  • запрос для получения данных об отдельном пользователе
  {
  user(id: "wallet address"){
    id
    balance
    transactionCount
    address
  }
}

Войти в полноэкранный режим Выйти из полноэкранного режима

Наконец, мы можем сделать наш подграф доступным для децентрализованной сети. Служба Graph hosted в настоящее время индексирует наш развернутый подграф. Чтобы сделать наш подграф децентрализованным, мы должны опубликовать его в сети, где индексаторы подхватят его и проиндексируют. Код для этого учебника доступен здесь

Я надеюсь, что вы узнали что-то полезное из этого руководства. Спасибо за ваше время.

Оцените статью
devanswers.ru
Добавить комментарий