Введение
Умные контракты генерируют события, которые являются богатым источником данных, которые можно агрегировать. Например, при передаче токенов токен 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 в настоящее время индексирует наш развернутый подграф. Чтобы сделать наш подграф децентрализованным, мы должны опубликовать его в сети, где индексаторы подхватят его и проиндексируют. Код для этого учебника доступен здесь
Я надеюсь, что вы узнали что-то полезное из этого руководства. Спасибо за ваше время.