Developing > Создание субграфа

Создание субграфа

Reading time: 41 min

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

Определение субграфа

Определение субграфа состоит из нескольких файлов:

Чтобы использовать свой субграф в децентрализованной сети The Graph, Вам необходимо создать ключ API. Рекомендуется добавить сигнал в свой субграф как минимум с 3000 GRT.

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

Установка Graph CLI

Ссылка на этот раздел

The Graph CLI написан на JavaScript, и для его использования необходимо установить либо yarn, либо npm; в дальнейшем предполагается, что у Вас есть yarn.

Получив yarn, установите Graph CLI, запустив следующие команды

Установка с помощью yarn:

yarn global add @graphprotocol/graph-cli

Установка с помощью npm:

npm install -g @graphprotocol/graph-cli

После установки команду graph init можно использовать для настройки нового проекта субграфа либо из существующего контракта, либо из примера субграфа. Эту команду можно использовать для создания субграфа в Subgraph Studio, передав в graph init --product subgraph-studio. Если у Вас уже есть смарт-контракт, развернутый в выбранной Вами сети, загрузка нового субграфа из этого контракта может быть хорошим способом начать работу.

Из существующего контракта

Ссылка на этот раздел

Следующая команда создает субграф, который индексирует все события существующего контракта. Он пытается получить ABI контракта из Etherscan и возвращается к запросу пути к локальному файлу. Если какой-либо из необязательных аргументов отсутствует, он проведет Вас через интерактивную форму.

graph init \
--product subgraph-studio
--from-contract <CONTRACT_ADDRESS> \
[--network <ETHEREUM_NETWORK>] \
[--abi <FILE>] \
<SUBGRAPH_SLUG> [<DIRECTORY>]

<SUBGRAPH_SLUG> - это идентификатор Вашего субграфа в Subgraph Studio, его можно найти на странице сведений о субграфе.

Из примера субграфа

Ссылка на этот раздел

Второй режим, который поддерживает graph init, - это создание нового проекта из примера субграфа. Это делает следующая команда:

graph init --studio <SUBGRAPH_SLUG>

Пример субграфа основан на контракте Gravity Дэни Гранта, который управляет пользовательскими аватарами и генерирует события NewGravatar или UpdateGravatar при создании или обновлении аватаров. Субграф обрабатывает эти события, записывая объекты Gravatar в хранилище Graph Node и обеспечивая их обновление в соответствии с событиями. В следующих разделах будут рассмотрены файлы, составляющие манифест субграфа для этого примера.

Добавление новых источников данных к существующему субграфу

Ссылка на этот раздел

Начиная с v0.31.0 graph-cli поддерживает добавление новых источников данных к существующему субграфу с помощью команды graph add.

graph add <address> [<subgraph-manifest default: "./subgraph.yaml">]
Опции:
--abi <path> Путь к контракту ABI (default: download from Etherscan)
--contract-name Имя контракта (default: Contract)
--merge-entities Следует ли объединять объекты с одинаковым именем (default: false)
--network-file <path> Путь к файлу конфигурации сети (default: "./networks.json")

Команда add извлечёт ABI из Etherscan (если путь к ABI не указан с помощью опции --abi) и создаст новый dataSource таким же образом, как graph init создает dataSource --from-contract, соответствующим образом обновляя схему и мэппинги.

Параметр --merge-entities определяет, как разработчик хотел бы обрабатывать конфликты имен entity и event:

  • Если true: новый dataSource должен использовать существующие eventHandlers & entities.
  • Если false: следует создать новую сущность и обработчик событий с помощью ${dataSourceName}{EventName}.

Контракт address будет записан в networks.json для соответствующей сети.

Примечание: При использовании интерактивного интерфейса командной строки после успешного запуска graph init Вам будет предложено добавить новый dataSource.

Манифест субграфа

Ссылка на этот раздел

Манифест субграфа subgraph.yaml определяет смарт-контракты, которые индексирует Ваш субграф, на какие события из этих контрактов следует обращать внимание и как сопоставлять данные событий с объектами, которые хранит и позволяет запрашивать Graph Node. Полную спецификацию манифестов субграфов можно найти здесь.

Для примера субграфа subgraph.yaml:

specVersion: 0.0.4
description: Gravatar for Ethereum
repository: https://github.com/graphprotocol/graph-tooling
schema:
file: ./schema.graphql
indexerHints:
prune: auto
dataSources:
- kind: ethereum/contract
name: Gravity
network: mainnet
source:
address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'
abi: Gravity
startBlock: 6175244
endBlock: 7175245
context:
foo:
type: Bool
data: true
bar:
type: String
data: 'bar'
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
abis:
- name: Gravity
file: ./abis/Gravity.json
eventHandlers:
- event: NewGravatar(uint256,address,string,string)
handler: handleNewGravatar
- event: UpdatedGravatar(uint256,address,string,string)
handler: handleUpdatedGravatar
callHandlers:
- function: createGravatar(string,string)
handler: handleCreateGravatar
blockHandlers:
- handler: handleBlock
- handler: handleBlockWithCall
filter:
kind: call
file: ./src/mapping.ts

Важными элементами манифеста, которые необходимо обновить, являются:

  • specVersion: a semver version that identifies the supported manifest structure and functionality for the subgraph. The latest version is 1.2.0. See specVersion releases section to see more details on features & releases.

  • description: понятное описание того, что представляет собой субграф. Это описание отображается в Graph Explorer при развертывании субграфа в Subgraph Studio.

  • repository: URL-адрес репозитория, в котором можно найти манифест субграфа. Это также отображается в The Graph Explorer.

  • features: список всех используемых имен функций.

  • indexerHints.prune: определяет срок хранения исторических данных блока для субграфа. См. сокращение в разделе indexerHints.

  • dataSources.source: адрес смарт-контракта, источники субграфа и ABI смарт-контракта для использования. Адрес необязателен; отсутствие этого параметра позволяет индексировать совпадающие события из всех контрактов.

  • dataSources.source.startBlock: необязательный номер блока, с которого источник данных начинает индексацию. В большинстве случаев мы предлагаем использовать блок, в котором был создан контракт.

  • dataSources.source.endBlock: необязательный номер блока, индексирование которого прекращается источником данных, включая этот блок. Минимальная требуемая версия спецификации: 0.0.9.

  • dataSources.context: пары «ключ-значение», которые можно использовать внутри мэппингов субграфов. Поддерживает различные типы данных, такие как Bool, String, Int, Int8, BigDecimal, Bytes, List и BigInt. Для каждой переменной нужно указать ее type и data. Эти контекстные переменные затем становятся доступными в файлах мэппинга, предлагая больше настраиваемых параметров для разработки субграфов.

  • dataSources.mapping.entities: объекты, которые источник данных записывает в хранилище. Схема для каждого объекта определена в файле schema.graphql.

  • dataSources.mapping.abis: один или несколько именованных файлов ABI для исходного контракта, а также любых других смарт-контрактов, с которыми Вы взаимодействуете из мэппингов.

  • DataSources.mapping.EventHandlers: перечисляет события смарт—контракта, на которые реагирует этот субграф, и обработчики в мэппинге —./src/mapping.ts в примере - которые преобразуют эти события в объекты в хранилище.

  • DataSources.mapping.callHandlers: перечисляет функции смарт-контракта, на которые реагирует этот субграф, и обработчики в мэппинге, которые преобразуют входные и выходные данные для вызовов функций в объекты в хранилище.

  • dataSources.mapping.blockHandlers: перечисляет блоки, на которые реагирует этот субграф, и обработчики в мэппинг, которые запускаются при добавлении блока в чейн. Без фильтра обработчик блока будет запускаться для каждого блока. Дополнительный фильтр вызовов может быть предоставлен путем добавления в обработчик поля filter с kind: call . Обработчик будет запущен только в том случае, если блок содержит хотя бы один вызов контракта источника данных.

Один субграф может индексировать данные из нескольких смарт-контрактов. Добавьте в массив dataSources запись для каждого контракта, данные которого нужно проиндексировать.

Порядок запуска обработчиков

Ссылка на этот раздел

Триггеры для источника данных внутри блока упорядочиваются с помощью следующего процесса:

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

Эти правила оформления заказа могут быть изменены.

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

Фильтры индексированных аргументов/фильтры тем

Ссылка на этот раздел

Requires: SpecVersion >= 1.2.0

Topic filters, also known as indexed argument filters, are a powerful feature in subgraphs that allow users to precisely filter blockchain events based on the values of their indexed arguments.

  • These filters help isolate specific events of interest from the vast stream of events on the blockchain, allowing subgraphs to operate more efficiently by focusing only on relevant data.

  • This is useful for creating personal subgraphs that track specific addresses and their interactions with various smart contracts on the blockchain.

Как работают фильтры тем

Ссылка на этот раздел

When a smart contract emits an event, any arguments that are marked as indexed can be used as filters in a subgraph's manifest. This allows the subgraph to listen selectively for events that match these indexed arguments.

  • The event's first indexed argument corresponds to topic1, the second to topic2, and so on, up to topic3, since the Ethereum Virtual Machine (EVM) allows up to three indexed arguments per event.
// Идентификатор лицензии SPDX: MIT
pragma solidity ^0.8.0;
contract Token {
// Объявление события с индексируемыми параметрами для адресов
event Transfer(address indexed from, address indexed to, uint256 value);
// Функция для имитации передачи токенов
function transfer(address to, uint256 value) public {
// Генерация события Transfer с указанием from, to и value
emit Transfer(msg.sender, to, value);
}
}

В этом примере:

  • Событие Transfer используется для протоколирования транзакций токенов между адресами.
  • The from and to parameters are indexed, allowing event listeners to filter and monitor transfers involving specific addresses.
  • Функция transfer — это простое представление действия передачи токена, которое генерирует событие Transfer при каждом вызове.

Конфигурация в субграфах

Ссылка на этот раздел

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

eventHandlers:
- event: SomeEvent(indexed uint256, indexed address, indexed uint256)
handler: handleSomeEvent
topic1: ['0xValue1', '0xValue2']
topic2: ['0xAddress1', '0xAddress2']
topic3: ['0xValue3']

В этой настройке:

  • topic1 соответствует первому индексированному аргументу события, topic2 — второму, а topic3 — третьему.
  • Каждая тема может иметь одно или несколько значений, и событие обрабатывается только в том случае, если оно соответствует одному из значений в каждой указанной теме.
Логика фильтра
Ссылка на этот раздел
  • В рамках одной темы: логика действует как условие OR. Событие будет обработано, если оно соответствует любому из перечисленных значений в данной теме.
  • Между разными темами: логика функционирует как условие AND. Событие должно удовлетворять всем указанным условиям в разных темах, чтобы вызвать соответствующий обработчик.

Example 1: Tracking Direct Transfers from Address A to Address B

Ссылка на этот раздел
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleDirectedTransfer
topic1: ['0xAddressA'] # Sender Address
topic2: ['0xAddressB'] # Receiver Address

В данной конфигурации:

  • topic1 настроен на фильтрацию событий Transfer, где 0xAddressA является отправителем.
  • topic2 настроен на фильтрацию событий Transfer, где 0xAddressB является получателем.
  • Субграф будет индексировать только транзакции, которые происходят непосредственно от 0xAddressA к 0xAddressB.

Example 2: Tracking Transactions in Either Direction Between Two or More Addresses

Ссылка на этот раздел
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransferToOrFrom
topic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Address
topic2: ['0xAddressB', '0xAddressC'] # Receiver Address

В данной конфигурации:

  • topic1 is configured to filter Transfer events where 0xAddressA, 0xAddressB, 0xAddressC is the sender.
  • topic2 is configured to filter Transfer events where 0xAddressB and 0xAddressC is the receiver.
  • The subgraph will index transactions that occur in either direction between multiple addresses allowing for comprehensive monitoring of interactions involving all addresses.

Requires: SpecVersion >= 1.2.0. Currently, eth_calls can only be declared for event handlers.

Declarative eth_calls are a valuable subgraph feature that allows eth_calls to be executed ahead of time, enabling graph-node to execute them in parallel.

This feature does the following:

  • Significantly improves the performance of fetching data from the Ethereum blockchain by reducing the total time for multiple calls and optimizing the subgraph's overall efficiency.
  • Allows faster data fetching, resulting in quicker query responses and a better user experience.
  • Reduces wait times for applications that need to aggregate data from multiple Ethereum calls, making the data retrieval process more efficient.
  • Declarative eth_calls: Ethereum calls that are defined to be executed in parallel rather than sequentially.
  • Parallel Execution: Instead of waiting for one call to finish before starting the next, multiple calls can be initiated simultaneously.
  • Time Efficiency: The total time taken for all the calls changes from the sum of the individual call times (sequential) to the time taken by the longest call (parallel).

Scenario without Declarative eth_calls

Ссылка на этот раздел

Imagine you have a subgraph that needs to make three Ethereum calls to fetch data about a user's transactions, balance, and token holdings.

Traditionally, these calls might be made sequentially:

  1. Call 1 (Transactions): Takes 3 seconds
  2. Call 2 (Balance): Takes 2 seconds
  3. Call 3 (Token Holdings): Takes 4 seconds

Total time taken = 3 + 2 + 4 = 9 seconds

Scenario with Declarative eth_calls

Ссылка на этот раздел

With this feature, you can declare these calls to be executed in parallel:

  1. Call 1 (Transactions): Takes 3 seconds
  2. Call 2 (Balance): Takes 2 seconds
  3. Call 3 (Token Holdings): Takes 4 seconds

Since these calls are executed in parallel, the total time taken is equal to the time taken by the longest call.

Total time taken = max (3, 2, 4) = 4 seconds

  1. Declarative Definition: In the subgraph manifest, you declare the Ethereum calls in a way that indicates they can be executed in parallel.
  2. Parallel Execution Engine: The Graph Node's execution engine recognizes these declarations and runs the calls simultaneously.
  3. Result Aggregation: Once all calls are complete, the results are aggregated and used by the subgraph for further processing.

Example Configuration in Subgraph Manifest

Ссылка на этот раздел

Declared eth_calls can access the event.address of the underlying event as well as all the event.params.

Subgraph.yaml using event.address:

eventHandlers:
event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)
handler: handleSwap
calls:
global0X128: Pool[event.address].feeGrowthGlobal0X128()
global1X128: Pool[event.address].feeGrowthGlobal1X128()

Details for the example above:

  • global0X128 is the declared eth_call.
  • The text before colon(global0X128) is the label for this eth_call which is used when logging errors.
  • The text (Pool[event.address].feeGrowthGlobal0X128()) is the actual eth_call that will be executed, which is in the form of Contract[address].function(arguments)
  • The address and arguments can be replaced with variables that will be available when the handler is executed.

Subgraph.yaml using event.params

calls:
- ERC20DecimalsToken0: ERC20[event.params.token0].decimals()
ВерсияПримечания к релизу
1.2.0Added support for Indexed Argument Filtering & declared eth_call
1.1.0Supports Timeseries & Aggregations. Added support for type Int8 for id.
1.0.0Supports indexerHints feature to prune subgraphs
0.0.9Supports endBlock feature
0.0.8Added support for polling Block Handlers and Initialisation Handlers.
0.0.7Added support for File Data Sources.
0.0.6Supports fast Proof of Indexing calculation variant.
0.0.5Added support for event handlers having access to transaction receipts.
0.0.4Added support for managing subgraph features.

Файл(ы) ABI должен(ы) соответствовать Вашему контракту (контрактам). Существует несколько способов получения файлов ABI:

  • Если Вы создаете свой собственный проект, у Вас, скорее всего, будет доступ к наиболее актуальным ABIS.
  • Если Вы создаете субграф для публичного проекта, Вы можете загрузить этот проект на свой компьютер и получить ABI, используя truffle compile или используя solc для компиляции.
  • Вы также можете найти ABI на Etherscan, но это не всегда надежно, так как загруженный туда ABI может быть устаревшим. Убедитесь, что у Вас есть нужный ABI, в противном случае запуск Вашего субграфа будет неудачным.

Схема для Вашего субграфа находится в файле schema.graphql. Схемы GraphQL определяются с использованием языка определения интерфейса GraphQL. Если Вы никогда ранее не писали схему GraphQL, рекомендуем ознакомиться с этим руководством по системе типов GraphQL. Справочную документацию по схемам GraphQL можно найти в разделе GraphQL API.

Определение Объектов

Ссылка на этот раздел

Прежде чем определять объекты, важно сделать шаг назад и подумать о том, как структурированы и связаны Ваши данные. Все запросы будут выполняться к модели данных, определенной в схеме субграфа, и объектам, проиндексированным этим субграфом. Для этого рекомендуется определить схему субграфа таким образом, чтобы она соответствовала потребностям Вашего децентрализованного приложения. Может быть полезно представить объекты как "объекты, содержащие данные", а не как события или функции.

С помощью The Graph Вы просто определяете типы объектов в schema.graphql, и узел The Graph будет генерировать поля верхнего уровня для запроса отдельных экземпляров и коллекций этого типа объектов. Каждый тип, который должен быть объектом, должен быть аннотирован директивой @entity. По умолчанию объекты изменяемы, что означает, что мэппинги могут загружать существующие объекты, изменять их и сохранять их новую версию. Измененяемость имеет свою цену, и для типов объектов, для которых известно, что они никогда не будут изменены, например, потому что они просто содержат данные, идентично извлеченные из чейна, рекомендуется помечать их как неизменяемые с помощью @entity(immutable: true). Мэппинги могут вносить изменения в неизменяемые объекты до тех пор, пока эти изменения происходят в том же блоке, в котором был создан объект. Неизменяемые объекты гораздо быстрее записываются и запрашиваются, и поэтому их следует использовать каждый раз, когда это возможно.

Удачный пример

Ссылка на этот раздел

Приведенный ниже объект Gravatar структурирован вокруг объекта Gravatar и является хорошим примером того, как объект может быть определен.

type Gravatar @entity(immutable: true) {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: String
accepted: Boolean
}

Неудачный пример

Ссылка на этот раздел

Приведенные ниже примеры объектов GravatarAccepted и GravatarDeclined основаны на событиях. Не рекомендуется сопоставлять события или вызовы функций с объектами 1:1.

type GravatarAccepted @entity {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: String
}
type GravatarDeclined @entity {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: String
}

Дополнительные и обязательные поля

Ссылка на этот раздел

Поля объекта могут быть определены как обязательные или необязательные. Обязательные поля обозначены символом ! в схеме. Если в мэппинге не задано обязательное поле, то при запросе к нему будет выдана эта ошибка:

Null value resolved for non-null field 'name'

У каждого объекта должно быть поле id, у которого должен быть тип Bytes! или String!. Обычно рекомендуется использовать Bytes!, если только id не содержит удобочитаемый текст, поскольку объекты с ! будут записываться и запрашиваться быстрее, чем объекты с String!``id. Поле id служит первичным ключом и должно быть уникальным среди всех объектов одного типа. В силу исторических причин тип ID! также принимается и является синонимом String!.

Для некоторых типов объектов id создается из идентификаторов двух других объектов; этому способствует concat, например, для формирования id let id = left.id.concat(right.id) из идентификаторов left и right. Аналогично этому, чтобы создать идентификатор из идентификатора существующего объекта и счетчика count, можно использовать let id = left.id.concatI32(count). Объединение гарантированно приведёт к созданию уникальных идентификаторов, если длина left одинакова для всех таких объектов, например, потому что left.id является Address.

Встроенные скалярные типы

Ссылка на этот раздел

Поддерживаемые GraphQL скаляры

Ссылка на этот раздел

Мы поддерживаем следующие скаляры в нашем GraphQL API:

ТипОписание
BytesМассив байтов, представленный в виде шестнадцатеричной строки. Обычно используется для хэшей и адресов Ethereum.
StringСкаляр для значений string. Нулевые символы не поддерживаются и автоматически удаляются.
BooleanСкаляр для значений boolean.
IntThe GraphQL spec defines Int to be a signed 32-bit integer.
Int88-байтовое целое число со знаком, также называемое 64-битным целым числом со знаком, может хранить значения в диапазоне от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807. Предпочтительно использовать это для представления i64 из Ethereum.
BigIntБольшие целые числа. Используются для типов Ethereum uint32, int64, uint64, ..., uint256. Примечание: Все, что находится ниже uint32, например, int32, uint24 или int8, представлено как i32.
BigDecimalBigDecimal Десятичные дроби высокой точности, представленные в виде значащего числа и экспоненты. Диапазон значений экспоненты от -6143 до +6144. Округляется до 34 значащих цифр.
TimestampIt is an i64 value in microseconds. Commonly used for timestamp fields for timeseries and aggregations.

Перечисления

Ссылка на этот раздел

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

enum TokenStatus {
OriginalOwner
SecondOwner
ThirdOwner
}

Как только перечисление определено в схеме, Вы можете использовать строковое представление значения перечисления, чтобы задать поле перечисления для объекта. Например, Вы можете установить для tokenStatus значение SecondOwner, сначала определив свой объект, а затем установив в поле entity.tokenStatus = "SecondOwner". Приведенный ниже пример демонстрирует, как будет выглядеть объект Token с полем enum:

Более подробную информацию о написании перечислений можно найти в Документации по GraphQL.

Связи объектов

Ссылка на этот раздел

Объект может иметь связь с одним или несколькими другими объектами в Вашей схеме. Эти связи могут быть использованы в Ваших запросах. Связи в The Graph являются однонаправленными. Можно смоделировать двунаправленные связи, определив однонаправленную связь на любом "конце" связи.

Связи определяются для объектов точно так же, как и для любого другого поля, за исключением того, что в качестве типа указывается тип другого объекта.

Связи "Один к одному"

Ссылка на этот раздел

Определите тип объекта Transaction с необязательной связью "один к одному" с типом объекта transactionReceipt:

type Transaction @entity(immutable: true) {
id: Bytes!
transactionReceipt: TransactionReceipt
}
type TransactionReceipt @entity(immutable: true) {
id: Bytes!
transaction: Transaction
}

Связи "Один ко многим"

Ссылка на этот раздел

Определите тип объекта TokenBalance с обязательной связью "один ко многим" с типом объекта Token:

type Token @entity(immutable: true) {
id: Bytes!
}
type TokenBalance @entity {
id: Bytes!
amount: Int!
token: Token!
}

Обратные запросы

Ссылка на этот раздел

Обратные зпросы могут быть определены для объекта с помощью поля @derivedFrom. При этом в объекте создается виртуальное поле, которое можно запрашивать, но нельзя задать вручную через API мэппингов. Скорее, оно вытекает из отношений, определенных для другого объекта. Для таких отношений редко имеет смысл сохранять обе стороны связи, а производительность как индексирования, так и запросов будет выше, когда сохраняется только одна сторона, а другая является производной.

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

Мы можем сделать балансы для токена доступными из самого токена, создав поле tokenBalances:

type Token @entity(immutable: true) {
id: Bytes!
tokenBalances: [TokenBalance!]! @derivedFrom(field: "token")
}
type TokenBalance @entity {
id: Bytes!
amount: Int!
token: Token!
}

Связи "Многие ко многим"

Ссылка на этот раздел

Для связей "многие ко многим", таких, например, как пользователи, каждый из которых может принадлежать к любому числу организаций, наиболее простым, но, как правило, не самым производительным способом моделирования связей является создание массива в каждом из двух задействованных объектов. Если связь симметрична, то необходимо сохранить только одну сторону связи, а другая сторона может быть выведена.

Определите обратный запрос от типа объекта User к типу объекта Organization. В приведенном ниже примере это достигается путем поиска атрибута members внутри объекта Organization. В запросах поле organizations в User будет разрешено путем поиска всех объектов Organization, включающих идентификатор пользователя.

type Organization @entity {
id: Bytes!
name: String!
members: [User!]!
}
type User @entity {
id: Bytes!
name: String!
organizations: [Organization!]! @derivedFrom(field: "members")
}

Более эффективный способ сохранить эту взаимосвязь - с помощью таблицы мэппинга, которая содержит по одной записи для каждой пары User / Organization со схемой, подобной

type Organization @entity {
id: Bytes!
name: String!
members: [UserOrganization!]! @derivedFrom(field: "organization")
}
type User @entity {
id: Bytes!
name: String!
organizations: [UserOrganization!] @derivedFrom(field: "user")
}
type UserOrganization @entity {
id: Bytes! # Set to `user.id.concat(organization.id)`
user: User!
organization: Organization!
}

Этот подход требует, чтобы запросы опускались на один дополнительный уровень для получения, например, сведений об организациях для пользователей:

query usersWithOrganizations {
users {
organizations {
# this is a UserOrganization entity
organization {
name
}
}
}
}

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

Добавление комментариев к схеме

Ссылка на этот раздел

As per GraphQL spec, comments can be added above schema entity attributes using the hash symble #. This is illustrated in the example below:

type MyFirstEntity @entity {
# unique identifier and primary key of the entity
id: Bytes!
address: Bytes!
}

Определение полей полнотекстового поиска

Ссылка на этот раздел

Полнотекстовые поисковые запросы фильтруют и ранжируют объекты на основе введенных данных текстового запроса. Полнотекстовые запросы способны возвращать совпадения по схожим словам путем обработки текста запроса в виде строк перед сравнением с индексированными текстовыми данными.

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

Чтобы добавить полнотекстовый запрос, включите тип _Schema_ с полнотекстовой директивой в схему GraphQL.

type _Schema_
@fulltext(
name: "bandSearch"
language: en
algorithm: rank
include: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }]
)
type Band @entity {
id: Bytes!
name: String!
description: String!
bio: String
wallet: Address
labels: [Label!]!
discography: [Album!]!
members: [Musician!]!
}

Поле example bandSearch можно использовать в запросах для фильтрации объектов Band на основе текстовых документов в name, description и bio.> поля. Перейдите к GraphQL API - запросы для описания API полнотекстового поиска и дополнительных примеров использования.

query {
bandSearch(text: "breaks & electro & detroit") {
id
name
description
wallet
}
}

Управление функциями: Начиная с specVersion 0.0.4 и далее, fullTextSearch должно быть объявлено в разделе features в манифесте субграфа.

Поддерживаемые языки

Ссылка на этот раздел

Выбор другого языка окажет решающее, хотя иногда и неуловимое влияние на API полнотекстового поиска. Поля, охватываемые полем полнотекстового запроса, рассматриваются в контексте выбранного языка, поэтому лексемы, полученные в результате анализа и поисковых запросов, варьируются от языка к языку. Например: при использовании поддерживаемого турецкого словаря "token" переводится как "toke", в то время как, конечно, словарь английского языка переводит его в "token".

Поддерживаемые языковые словари:

КодСловарь
простойОбщий
daДатский
nlГолландский
enАнглийский
fiФинский
frФранцузский
deНемецкий
huВенгерский
itИтальянский
noНорвежский
ptПортугальский
roРумынский
ruРусский
esИспанский
svШведский
trТурецкий

Алгоритмы ранжирования

Ссылка на этот раздел

Поддерживаемые алгоритмы для упорядочивания результатов:

АлгоритмОписание
rankИспользуйте качество соответствия (0-1) полнотекстового запроса, чтобы упорядочить результаты.
proximityRankАналогично рангу, но также включает в себя близость совпадений.

Написание мэппингов

Ссылка на этот раздел

Мэппинги берут данные из определенного источника и преобразуют их в объекты, которые определены в Вашей схеме. Мэппинги записываются в подмножестве TypeScript, которое называется AssemblyScript, и которое может быть скомпилировано в WASM (WebAssembly). AssemblyScript более строг, чем обычный TypeScript, но при этом предоставляет знакомый синтаксис.

Для каждого обработчика событий, определенного в subgraph.yaml в разделе mapping.EventHandlers, создайте экспортируемую функцию с тем же именем. Каждый обработчик должен принимать один параметр с именем event с типом, соответствующим имени обрабатываемого события.

В примере субграф src/mapping.ts содержит обработчики для событий NewGravatar и UpdatedGravatar:

import { NewGravatar, UpdatedGravatar } from '../generated/Gravity/Gravity'
import { Gravatar } from '../generated/schema'
export function handleNewGravatar(event: NewGravatar): void {
let gravatar = new Gravatar(event.params.id)
gravatar.owner = event.params.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.save()
}
export function handleUpdatedGravatar(event: UpdatedGravatar): void {
let id = event.params.id
let gravatar = Gravatar.load(id)
if (gravatar == null) {
gravatar = new Gravatar(id)
}
gravatar.owner = event.params.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.save()
}

Первый обработчик принимает событие NewGravatar и создает новый объект Gravatar с помощью new Gravatar(event.params.id.toHex()), заполняя поля объекта, используя соответствующие параметры события. Этот экземпляр объекта представлен переменной gravatar со значением идентификатора event.params.id.toHex().

Второй обработчик пытается загрузить существующий Gravatar из хранилища узлов The Graph. Если его еще нет, он создается по требованию. Затем объект обновляется в соответствии с новыми параметрами события, прежде чем он будет сохранен обратно в хранилище с помощью gravatar.save().

Рекомендуемые идентификаторы для создания новых объектов

Ссылка на этот раздел

Настоятельно рекомендуется использовать Bytes в качестве типа для полей id и использовать String только для атрибутов, которые действительно содержат удобочитаемый текст, например имя токена. Ниже приведены некоторые рекомендуемые значения id, которые следует учитывать при создании новых объектов.

  • transfer.id = event.transaction.hash

  • let id = event.transaction.hash.concatI32(event.logIndex.toI32())

  • Для объектов, которые хранят агрегированные данные, например ежедневные объемы торгов, id обычно содержит номер дня. В данном случае полезно использовать Bytes в качестве id. Определение id будет выглядеть следующим образом:

let dayID = event.block.timestamp.toI32() / 86400
let id = Bytes.fromI32(dayID)
  • Преобразуйте постоянные адреса в Bytes.

const id = Bytes.fromHexString('0xdead...beef')

Существует Библиотека Typescript Graph, которая содержит утилиты для взаимодействия с хранилищем Graph Node и удобства для обработки данных и объектов смарт-контрактов. Её можно импортировать в mapping.ts из @graphprotocol/graph-ts.

Обработка объектов с одинаковыми идентификаторами

Ссылка на этот раздел

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

Если для поля нового объекта с тем же идентификатором намеренно установлено нулевое значение, существующий объект будет обновлен с использованием нулевого значения.

Если для поля в новом объекте с тем же идентификатором не установлено значение, поле также будет иметь значение null.

Генерация кода

Ссылка на этот раздел

Для упрощения и обеспечения безопасности типов при работе со смарт-контрактами, событиями и объектами Graph CLI может генерировать типы AssemblyScript на основе схемы GraphQL субграфа и ABI контрактов, включенных в источники данных.

Это делается с помощью

graph codegen [--output-dir <OUTPUT_DIR>] [<MANIFEST>]

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

# Yarn
yarn codegen
# NPM
npm run codegen

Это сгенерирует класс AssemblyScript для каждого смарт-контракта в файлах ABI, упомянутых в subgraph.yaml, позволяя Вам привязывать эти контракты к определенным адресам в мэппигах и вызывать контрактные методы, доступные только для чтения, для обрабатываемого блока. Кроме того, для каждого события контракта генерируется класс, обеспечивающий удобный доступ к параметрам события, а также к блоку и транзакции, от которых произошло событие. Все эти типы записываются в <OUTPUT_DIR>/<DATA_SOURCE_NAME>/<ABI_NAME>.ts. В примере субграфа это будет код generated/Gravity/Gravity.ts, позволяющий импортировать эти типы с помощью мэппинга.

import {
// The contract class:
Gravity,
// The events classes:
NewGravatar,
UpdatedGravatar,
} from '../generated/Gravity/Gravity'

В дополнение к этому, для каждого типа объекта в схеме GraphQL субграфа генерируется по одному классу. Эти классы обеспечивают безопасную для типов загрузку объектов, доступ к чтению и записи в поля объекта, а также метод save() для записи объектов в хранилище. Все классы объектов записываются в <OUTPUT_DIR>/schema.ts, что позволяет мэппингам импортировать с их помощью

import { Gravatar } from '../generated/schema'

Примечание: Генерация кода должна выполняться повторно после каждого изменения схемы GraphQL или ABIS, включенного в манифест. Это также должно быть выполнено по крайней мере один раз перед сборкой или развертыванием субграфа.

Генерация кода не проверяет Ваш мэппинг код в src/mapping.ts. Если Вы хотите проверить это, прежде чем пытаться развернуть свой субграф в Graph Explorer, Вы можете запустить yarn build и исправить любые синтаксические ошибки, которые может обнаружить компилятор TypeScript.

Шаблоны источников данных

Ссылка на этот раздел

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

Адреса этих субконтрактов могут быть известны или не известны заранее, и многие из этих контрактов могут быть созданы и/или добавлены с течением времени. Поэтому в таких случаях определение одного источника данных или фиксированного количества источников данных невозможно и необходим более динамичный подход: data source templates.

Источник данных для основного контракта

Ссылка на этот раздел

Сначала Вы определяете обычный источник данных для основного контракта. Во фрагменте ниже показан упрощенный пример источника данных для контракта фабрики обмена Uniswap. Обратите внимание на обработчик события New Exchange(address,address). Этот сигнал выдается, когда новый контракт обмена создается в цепочке с помощью заводского контракта.

dataSources:
- kind: ethereum/contract
name: Factory
network: mainnet
source:
address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
abi: Factory
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/factory.ts
entities:
- Directory
abis:
- name: Factory
file: ./abis/factory.json
eventHandlers:
- event: NewExchange(address,address)
handler: handleNewExchange

Шаблоны источников данных для динамически создаваемых контрактов

Ссылка на этот раздел

Затем Вы добавляете data source templates в манифест. Они идентичны обычным источникам данных, за исключением того, что в них отсутствует предопределенный адрес контракта в source. Как правило, Вы определяете один шаблон для каждого типа субконтракта, управляемого родительским контрактом, или на который ссылается родительский контракт.

dataSources:
- kind: ethereum/contract
name: Factory
# ... other source fields for the main contract ...
templates:
- name: Exchange
kind: ethereum/contract
network: mainnet
source:
abi: Exchange
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/exchange.ts
entities:
- Exchange
abis:
- name: Exchange
file: ./abis/exchange.json
eventHandlers:
- event: TokenPurchase(address,uint256,uint256)
handler: handleTokenPurchase
- event: EthPurchase(address,uint256,uint256)
handler: handleEthPurchase
- event: AddLiquidity(address,uint256,uint256)
handler: handleAddLiquidity
- event: RemoveLiquidity(address,uint256,uint256)
handler: handleRemoveLiquidity

Создание шаблона источника данных

Ссылка на этот раздел

На заключительном шаге Вы обновляете мэппинг основного контракта, чтобы создать экземпляр динамического источника данных из одного из шаблонов. В данном примере в отображение основного контракта импортируется шаблон Exchange и вызывается метод Exchange.create(address), чтобы начать индексирование нового контракта обмена.

import { Exchange } from '../generated/templates'
export function handleNewExchange(event: NewExchange): void {
// Start indexing the exchange; `event.params.exchange` is the
// address of the new exchange contract
Exchange.create(event.params.exchange)
}

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

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

Контекст источника данных

Ссылка на этот раздел

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

import { Exchange } from '../generated/templates'
export function handleNewExchange(event: NewExchange): void {
let context = new DataSourceContext()
context.setString('tradingPair', event.params.tradingPair)
Exchange.createWithContext(event.params.exchange, context)
}

Внутри мэппинга шаблона Exchange затем можно получить доступ к контексту:

import { dataSource } from '@graphprotocol/graph-ts'
let context = dataSource.context()
let tradingPair = context.getString('tradingPair')

Существуют установщики и получатели, такие как setString и getString для всех типов значений.

Стартовые блоки

Ссылка на этот раздел

startBlock - это необязательный параметр, который позволяет Вам определить, с какого блока в цепочке источник данных начнет индексацию. Установка начального блока позволяет источнику данных пропускать потенциально миллионы блоков, которые не имеют отношения к делу. Как правило, разработчик субграфа устанавливает startBlock в блок, в котором был создан смарт-контракт источника данных.

dataSources:
- kind: ethereum/contract
name: ExampleSource
network: mainnet
source:
address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
abi: ExampleContract
startBlock: 6627917
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
file: ./src/mappings/factory.ts
entities:
- User
abis:
- name: ExampleContract
file: ./abis/ExampleContract.json
eventHandlers:
- event: NewEvent(address,address)
handler: handleNewEvent

Примечание: Блок создания контракта можно быстро найти в Etherscan:

  1. Найдите контракт, введя его адрес в строке поиска.
  2. Нажмите на хэш транзакции создания в разделе Contract Creator.
  3. Загрузите страницу сведений о транзакции, где Вы найдете начальный блок для этого контракта.

Подсказки индексатору

Ссылка на этот раздел

Параметр indexerHints в манифесте субграфа содержит директивы для индексаторов по обработке и управлению субграфом. Это влияет на оперативные решения по обработке данных, стратегиям индексации и оптимизации. В настоящее время в нем предусмотрена опция prune для управления сохранением или сокращением исторических данных.

Эта функция доступна начиная с specVersion: 1.0.0

indexerHints.prune: определяет срок хранения исторических данных блока для субграфа. Опции включают в себя:

  1. "never": удаление исторических данных не производится; хранит всю историю.
  2. "auto": сохраняет минимально необходимую историю, заданную индексатором, оптимизируя производительность запросов.
  3. Конкретное число: устанавливает индивидуальный лимит на количество сохраняемых исторических блоков.
indexerHints:
prune: auto

The term "history" in this context of subgraphs is about storing data that reflects the old states of mutable entities.

History as of a given block is required for:

  • Time travel queries, which enable querying the past states of these entities at specific blocks throughout the subgraph's history
  • Using the subgraph as a graft base in another subgraph, at that block
  • Rewinding the subgraph back to that block

If historical data as of the block has been pruned, the above capabilities will not be available.

Обычно рекомендуется использовать "auto", поскольку оно максимально увеличивает производительность запросов и достаточно для большинства пользователей, которым не требуется доступ к обширным историческим данным.

Для субграфов, использующих запросы о путешествиях во времени, рекомендуется либо установить определенное количество блоков для хранения исторических данных, либо использовать prune: never, чтобы сохранить все исторические состояния объектов. Ниже приведены примеры того, как настроить оба параметра в настройках вашего субграфа:

Чтобы сохранить определенный объем исторических данных:

indexerHints:
prune: 1000 # Замените 1000 на желаемое количество блоков, которые нужно сохранить

Чтобы сохранить полную историю состояний объекта, выполните следующее:

indexerHints:
prune: never

Вы можете проверить самый ранний блок (с историческим состоянием) для данного субграфа, выполнив запрос к API статуса индексирования:

{
indexingStatuses(subgraphs: ["Qm..."]) {
subgraph
synced
health
chains {
earliestBlock {
number
}
latestBlock {
number
}
chainHeadBlock { number }
}
}
}

Обратите внимание на то, что earliestBlock — это самый ранний блок с историческими данными, который будет более поздним, чем startBlock, указанный в манифесте, если субграф был удален.

Обработчики событий

Ссылка на этот раздел

Обработчики событий в субграфе реагируют на конкретные события, генерируемые смарт-контрактами в блокчейне, и запускают обработчики, определенные в манифесте подграфа. Это позволяет субграфам обрабатывать и хранить данные о событиях в соответствии с определенной логикой.

Определение обработчика событий

Ссылка на этот раздел

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

dataSources:
- kind: ethereum/contract
name: Gravity
network: dev
source:
address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
abi: Gravity
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
- Transaction
abis:
- name: Gravity
file: ./abis/Gravity.json
eventHandlers:
- event: Approval(address,address,uint256)
handler: handleApproval
- event: Transfer(address,address,uint256)
handler: handleTransfer
topic1: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0xc8dA6BF26964aF9D7eEd9e03E53415D37aA96325'] # Optional topic filter which filters only events with the specified topic.

Обработчики вызовов

Ссылка на этот раздел

Хотя события обеспечивают эффективный способ сбора соответствующих изменений в состоянии контракта, многие контракты избегают создания логов для оптимизации затрат на газ. В этих случаях субграф может подписаться на обращения к контракту источника данных. Это достигается путем определения обработчиков вызовов, ссылающихся на сигнатуру функции, и обработчика мэппинга, который будет обрабатывать вызовы этой функции. Чтобы обработать эти вызовы, обработчик мэппинга получит ethereum.Call в качестве аргумента, содержащего типизированные входы и выходы вызова. Вызовы, выполненные на любой глубине цепочки вызовов транзакции, запускают мэппинг, позволяя фиксировать действия с контрактом источника данных через прокси-контракты.

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

Примечание: Обработчики вызовов в настоящее время зависят от Parity tracing API. Некоторые сети, такие как BNB chain и Arbitrium, не поддерживают этот API. Если субграф, индексирующий одну из этих сетей, содержит один или несколько обработчиков вызовов, синхронизация не начнется. Разработчикам субграфов следует вместо этого использовать обработчики событий. Они гораздо более производительны, чем обработчики вызовов, и поддерживаются в каждой сети evm.

Определение обработчика вызова

Ссылка на этот раздел

Чтобы определить обработчика вызовов в Вашем манифесте, просто добавьте массив callHandlers под источником данных, на который Вы хотели бы подписаться.

dataSources:
- kind: ethereum/contract
name: Gravity
network: mainnet
source:
address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
abi: Gravity
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
- Transaction
abis:
- name: Gravity
file: ./abis/Gravity.json
callHandlers:
- function: createGravatar(string,string)
handler: handleCreateGravatar

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

Функция мэппинга

Ссылка на этот раздел

Каждый обработчик вызова принимает один параметр, тип которого соответствует имени вызываемой функции. В приведенном выше примере субграфа мэппинг содержит обработчик для случаев, когда вызывается функция createGravatar и получает параметр CreateGravatarCall в качестве аргумента:

import { CreateGravatarCall } from '../generated/Gravity/Gravity'
import { Transaction } from '../generated/schema'
export function handleCreateGravatar(call: CreateGravatarCall): void {
let id = call.transaction.hash
let transaction = new Transaction(id)
transaction.displayName = call.inputs._displayName
transaction.imageUrl = call.inputs._imageUrl
transaction.save()
}

Функция handleCreateGravatar принимает новый CreateGravatarCall, который является подклассом ethereum.Call, предоставляемый @graphprotocol/graph-ts, который включает в себя введенные входы и выходы о звонке. Тип CreateGravatarCall генерируется для Вас при запуске graph codegen.

Обработчики блоков

Ссылка на этот раздел

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

Поддерживаемые фильтры

Ссылка на этот раздел

Фильтр вызовов

Ссылка на этот раздел
filter:
kind: call

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

Примечание: Фильтр call в настоящее время зависит от Parity tracing API. Некоторые сети, такие как BNB chain и Arbitrium, не поддерживают этот API. Если субграф, индексирующий одну из этих сетей, содержит один или несколько обработчиков блоков с фильтром call, синхронизация не начнется.

Отсутствие фильтра для обработчика блоков гарантирует, что обработчик вызывается для каждого блока. Источник данных может содержать только один обработчик блоков для каждого типа фильтра.

dataSources:
- kind: ethereum/contract
name: Gravity
network: dev
source:
address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
abi: Gravity
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
- Transaction
abis:
- name: Gravity
file: ./abis/Gravity.json
blockHandlers:
- handler: handleBlock
- handler: handleBlockWithCallToContract
filter:
kind: call

Фильтр опроса

Ссылка на этот раздел

Требуется specVersion >= 0.0.8

Примечание. Фильтры опроса доступны только для источников данных kind: ethereum.

blockHandlers:
- handler: handleBlock
filter:
kind: polling
every: 10

Определенный обработчик будет вызываться один раз для каждого блока n, где n — это значение, указанное в поле every. Эта конфигурация позволяет субграфу выполнять определенные операции через регулярные интервалы блоков.

Однократный фильтр

Ссылка на этот раздел

Требуется specVersion >= 0.0.8

Примечание. Однократные фильтры доступны только для источников данных kind: ethereum.

blockHandlers:
- handler: handleOnce
filter:
kind: once

Определенный обработчик с однократным фильтром будет вызываться только один раз перед запуском всех остальных обработчиков. Эта конфигурация позволяет субграфу использовать обработчик в качестве обработчика инициализации, выполняя определенные задачи в начале индексирования.

export function handleOnce(block: ethereum.Block): void {
let data = new InitialData(Bytes.fromUTF8('initial'))
data.data = 'Setup data here'
data.save()
}

Функция мэппинга

Ссылка на этот раздел

Функция мэппинга получит ethereum.Block в качестве своего единственного аргумента. Подобно функциям мэппинга событий, эта функция может получать доступ к существующим в хранилище объектам субграфа, вызывать смарт-контракты и создавать или обновлять объекты.

import { ethereum } from '@graphprotocol/graph-ts'
export function handleBlock(block: ethereum.Block): void {
let id = block.hash
let entity = new Block(id)
entity.save()
}

Анонимные события

Ссылка на этот раздел

Если Вам нужно обрабатывать анонимные события в Solidity, это можно сделать, указав тему события 0, как показано в примере:

eventHandlers:
- event: LogNote(bytes4,address,bytes32,bytes32,uint256,bytes)
topic0: '0x644843f351d3fba4abcd60109eaff9f54bac8fb8ccf0bab941009c21df21cf31'
handler: handleGive

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

Подтверждения транзакций в обработчиках событий

Ссылка на этот раздел

Начиная с specVersion 0.0.5 и apiVersion 0.0.7 обработчики событий могут иметь доступ к подтверждению транзакции, которая их отправила.

Для этого обработчики событий должны быть объявлены в манифесте субграфа с новым ключом receipt: true, который является необязательным и по умолчанию имеет значение false.

eventHandlers:
- event: NewGravatar(uint256,address,string,string)
handler: handleNewGravatar
receipt: true

Внутри функции обработчика доступ к подтверждению можно получить в поле Event.receipt. Если для ключа receipt установлено значениеfalse или оно опущено в манифесте, вместо него будет возвращено значение null.

Экспериментальные функции

Ссылка на этот раздел

Начиная с specVersion 0.0.4, функции субграфа должны быть явно объявлены в разделе features на верхнем уровне файла манифеста, используя их имя camelCase, как указано в таблице ниже:

Например, если в субграфе используются функции Full-Text Search и Non-fatal Errors, поле features в манифесте должно быть:

specVersion: 0.0.4
description: Gravatar for Ethereum
features:
- fullTextSearch
- nonFatalErrors
dataSources: ...

Обратите внимание, что использование функции без ее объявления приведет к ошибке проверки во время развертывания субграфа, но никаких ошибок не возникнет, если функция объявлена, но не используется.

Тайм-серии и агрегации

Ссылка на этот раздел

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

Эта функция представляет два новых типа объектов субграфов. Объекты тайм-серий записывают точки данных с временными метками. Объекты агрегирования выполняют заранее объявленные вычисления над точками данных тайм-серий ежечасно или ежедневно, а затем сохраняют результаты для быстрого доступа через GraphQL.

type Data @entity(timeseries: true) {
id: Int8!
timestamp: Timestamp!
price: BigDecimal!
}
type Stats @aggregation(intervals: ["hour", "day"], source: "Data") {
id: Int8!
timestamp: Timestamp!
sum: BigDecimal! @aggregate(fn: "sum", arg: "price")
}

Определение тайм-серий и агрегаций

Ссылка на этот раздел

Объекты тайм-серий определяются с помощью @entity(timeseries: true) в schema.graphql. Каждый объект тайм-серии должен иметь уникальный идентификатор типа int8, метку времени типа Timestamp и включать данные, которые будут использоваться для вычислений объектами агрегации. Эти объекты тайм-серий могут быть сохранены в обычных обработчиках триггеров и выступать в качестве «необработанных данных» для объектов агрегации.

Объекты агрегации определяются с помощью @aggregation в schema.graphql. Каждый объект агрегирования определяет источник, из которого он будет собирать данные (который должен быть объектом тайм-серии), устанавливает интервалы (например, час, день) и указывает функцию агрегирования, которую он будет использовать (например, сумма, количество, минимум, максимум, первый, последний). Объекты агрегации рассчитываются автоматически на основе указанного источника в конце необходимого интервала.

Доступные интервалы агрегации

Ссылка на этот раздел
  • hour: устанавливает период тайм-серии каждый час, в час.
  • day: устанавливает период тайм-серий ежедневный, который начинается и заканчивается в 00:00.

Доступные функции агрегации

Ссылка на этот раздел
  • sum: сумма всех значений.
  • count: количество значений.
  • min: минимальное значение.
  • max: максимальное значение.
  • first: первое значение в периоде.
  • last: последнее значение за период.

Пример запроса агрегации

Ссылка на этот раздел
{
stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {
id
timestamp
sum
}
}

Примечание:

Чтобы использовать тайм-серии и агрегации, субграф должен иметь версию спецификации ≥1.1.0. Обратите внимание, что эта функция может претерпеть значительные изменения, которые могут повлиять на обратную совместимость.

Подробнее о тайм-сериях и агрегациях.

Нефатальные ошибки

Ссылка на этот раздел

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

Примечание: Сеть The Graph пока не поддерживает нефатальные ошибки, и разработчикам не следует разворачивать субграфы, использующие эту функциональность, в сети через Studio.

Для включения нефатальных ошибок необходимо установить в манифесте субграфа следующий флаг функции:

specVersion: 0.0.4
description: Gravatar for Ethereum
features:
- nonFatalErrors
...

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

foos(first: 100, subgraphError: allow) {
id
}
_meta {
hasIndexingErrors
}

Если субграф обнаруживает ошибку, этот запрос вернет как данные, так и ошибку graphql с сообщением "indexing_error", как в данном примере ответа:

"data": {
"foos": [
{
"id": "0xdead"
}
],
"_meta": {
"hasIndexingErrors": true
}
},
"errors": [
{
"message": "indexing_error"
}
]

Графтинг (перенос) на существующие субграфы

Ссылка на этот раздел

Примечание: не рекомендуется использовать графтинг при первоначальном переходе на сеть The Graph. Подробнее здесь.

Когда субграф развертывается впервые, он начинает индексировать события в блоке genesis соответствующего чейна (или в startBlock, определенном для каждого источника данных). В некоторых обстоятельствах полезно повторно использовать данные из существующего субграфа и начинать индексацию с гораздо более позднего блока. Этот режим индексации называется Grafting. Графтинг, например, полезен во время разработки, чтобы быстро устранить простые ошибки в отображениях или временно возобновить работу существующего субграфа после его сбоя.

Субграф графтится (переносится) к базовому субграфу, когда манифест субграфа в subgraph.yaml содержит блок graft на верхнем уровне:

description: ...
graft:
base: Qm... # Subgraph ID of base subgraph
block: 7345624 # Block number

Когда развертывается субграф, манифест которого содержит блок graft, узел The Graph скопирует данные base субграфа вплоть до указанного block включительно, а затем продолжит индексирование нового субграфа начиная с этого блока. Базовый субграф должен существовать на целевом экземпляре узла The Graph и должен быть проиндексирован по крайней мере до заданного блока. Из-за этого ограничения графтинг следует использовать только в процессе разработки или в экстренных случаях, чтобы ускорить создание эквивалентного графтового субграфа.

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

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

  • Она добавляет или удаляет типы объектов
  • Она удаляет атрибуты из типов объектов
  • Она добавляет в типы объектов атрибуты с возможностью обнуления
  • Она превращает ненулевые атрибуты в нулевые
  • Она добавляет значения в перечисления
  • Она добавляет или удаляет интерфейсы
  • Она изменяется в зависимости от того, для каких типов объектов реализован тот или иной интерфейс

Управление функционалом: grafting должен быть объявлен в разделе features в манифесте субграфа.

IPFS/Arweave File Data Sources

Ссылка на этот раздел

Источники файловых данных — это новая функциональность субграфа для надежного и расширенного доступа к данным вне чейна во время индексации. Источники данных файлов поддерживают получение файлов из IPFS и Arweave.

Это также закладывает основу для детерминированного индексирования данных вне сети, а также потенциального введения произвольных данных из HTTP-источников.

Rather than fetching files "in line" during handler execution, this introduces templates which can be spawned as new data sources for a given file identifier. These new data sources fetch the files, retrying if they are unsuccessful, running a dedicated handler when the file is found.

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

Это заменяет существующий API ipfs.cat

Руководство по обновлению

Ссылка на этот раздел

Обновите graph-ts и graph-cli

Ссылка на этот раздел

Для файловых источников данных требуется graph-ts >=0.29.0 и graph-cli >=0.33.1

Добавьте новый тип объекта, который будет обновляться при обнаружении файлов

Ссылка на этот раздел

Источники файловых данных не могут получать доступ к объектам на чейн-основе или обновлять их, но должны обновлять объекты, специфичные для файлов.

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

Исходный объединенный объект:

type Token @entity {
id: ID!
tokenID: BigInt!
tokenURI: String!
externalURL: String!
ipfsURI: String!
image: String!
name: String!
description: String!
type: String!
updatedAtTimestamp: BigInt
owner: User!
}

Новый разделенный объект:

type Token @entity {
id: ID!
tokenID: BigInt!
tokenURI: String!
ipfsURI: TokenMetadata
updatedAtTimestamp: BigInt
owner: String!
}
type TokenMetadata @entity {
id: ID!
image: String!
externalURL: String!
name: String!
description: String!
}

Если между родительским объектом и результирующим объектом-источником данных существует связь1:1, то наиболее простым вариантом будет связать родительский объект с результирующим файловым объектом, используя в качестве поиска IPFS CID. Свяжитесь с нами в Discord, если у Вас возникли трудности с моделированием новых объектов на основе файлов!

Вы можете использовать вложенные фильтры для фильтрации родительских объектов на основе этих вложенных объектов.

Добавьте новый шаблонный источник данных с помощью kind: file/ipfs или kind: file/arweave.

Ссылка на этот раздел

Это источник данных, который будет создан при обнаружении интересующего файла.

templates:
- name: TokenMetadata
kind: file/ipfs
mapping:
apiVersion: 0.0.7
language: wasm/assemblyscript
file: ./src/mapping.ts
handler: handleMetadata
entities:
- TokenMetadata
abis:
- name: Token
file: ./abis/Token.json

В настоящее время требуется abis, хотя невозможно вызывать контракты из файловых источников данных

В файле-источнике данных должны быть конкретно указаны все типы объектов, с которыми он будет взаимодействовать в рамках entities. Дополнительные сведения см. в разделе ограничения.

Создание нового обработчика для обработки файлов

Ссылка на этот раздел

Этот обработчик должен принимать один параметр Bytes, который будет содержимым файла, когда он будет найден, который затем можно будет обработать. Часто это файл JSON, который можно обработать с помощью помощников graph-ts (документация).

Доступ к CID файла в виде читаемой строки можно получить через dataSource следующим образом:

const cid = dataSource.stringParam()

Пример обработчика:

import { json, Bytes, dataSource } from '@graphprotocol/graph-ts'
import { TokenMetadata } from '../generated/schema'
export function handleMetadata(content: Bytes): void {
let tokenMetadata = new TokenMetadata(dataSource.stringParam())
const value = json.fromBytes(content).toObject()
if (value) {
const image = value.get('image')
const name = value.get('name')
const description = value.get('description')
const externalURL = value.get('external_url')
if (name && image && description && externalURL) {
tokenMetadata.name = name.toString()
tokenMetadata.image = image.toString()
tokenMetadata.externalURL = externalURL.toString()
tokenMetadata.description = description.toString()
}
tokenMetadata.save()
}
}

Создание файловых источников данных при необходимости

Ссылка на этот раздел

Теперь вы можете создавать файловые источники данных во время выполнения обработчиков на чейн-основе:

  • Импортируйте шаблон из автоматически созданных templates
  • вызовите TemplateName.create(cid: string) из мэппинга, где cid является действительным идентификатором контента для IPFS или Arweave

Для IPFS Graph Node поддерживает идентификаторы контента v0 и v1, а также идентификаторы контента с каталогами (например, bafyreighykzv2we26wfrbzkcdw37sbrby4upq7ae3aqobbq7i4er3tnxci/metadata.json).

Что касается Arweave, то начиная с версии 0.33.0 Graph Node может извлекать файлы, хранящиеся в Arweave, на основе их ID транзакции из шлюза Arweave (файл примера). Arweave поддерживает транзакции, загруженные через Irys (ранее Bundlr), а Graph Node также может получать файлы на основе манифестов Irys.

Пример:

import { TokenMetadata as TokenMetadataTemplate } from '../generated/templates'
const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
//Этот пример кода предназначен для сборщика субграфа Crypto. Приведенный выше хеш ipfs представляет собой каталог с метаданными токена для всех NFT криптоковена.
export function handleTransfer(event: TransferEvent): void {
let token = Token.load(event.params.tokenId.toString())
if (!token) {
token = new Token(event.params.tokenId.toString())
token.tokenID = event.params.tokenId
token.tokenURI = '/' + event.params.tokenId.toString() + '.json'
const tokenIpfsHash = ipfshash + token.tokenURI
//Это создает путь к метаданным для одного сборщика NFT Crypto. Он объединяет каталог с "/" + filename + ".json"
token.ipfsURI = tokenIpfsHash
TokenMetadataTemplate.create(tokenIpfsHash)
}
token.updatedAtTimestamp = event.block.timestamp
token.owner = event.params.to.toHexString()
token.save()
}

Это создаст новый источник данных файла, который будет опрашивать настроенную конечную точку IPFS или Arweave Graph Node, повторяя попытку, если она не найдена. Когда файл будет найден, будет выполнен обработчик источника данных файла.

В этом примере CID используется для поиска между родительским объектом Token и результирующим объектом TokenMetadata.

Раньше это была точка, в которой разработчик субграфа вызывал ipfs.cat(CID) для извлечения файла

Поздравляем, Вы используете файловые источники данных!

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

Ссылка на этот раздел

Теперь Вы можете build (построить) и deploy (развернуть) свой субграф на любом узле The Graph >=v0.30.0-rc.0.

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

  • Объекты, созданные с помощью файловых источников данных, неизменяемы и не могут быть обновлены
  • Обработчики файловых источников данных не могут получить доступ к объектам из других файловых источников данных
  • Объекты, связанные с источниками данных файлов, не могут быть доступны обработчикам на чейн-основе

Хотя это ограничение не должно вызывать проблем в большинстве случаев, для некоторых оно может вызвать сложности. Если у Вас возникли проблемы с моделированием Ваших файловых данных в субграфе, свяжитесь с нами через Discord!

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

Лучшие практики

Ссылка на этот раздел

Если Вы связываете метаданные NFT с соответствующими токенами, используйте хэш IPFS метаданных для ссылки на объект Metadata из объекта Token. Сохраните объект Metadata, используя хэш IPFS в качестве идентификатора.

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

Если у Вас есть объекты, которые обновляются несколько раз, создайте уникальные объекты на основе файлов, используя хэш IPFS и идентификатор объекта, и ссылайтесь на них, используя производное поле в объекте на чейн-основе.

Мы работаем над улучшением приведенной выше рекомендации, поэтому запросы возвращают только "самую последнюю" версию

Известные проблемы

Ссылка на этот раздел

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

Обработчики для файловых источников данных не могут находиться в файлах, которые импортируют привязки контракта eth_call, с ошибкой "unknown import: ethereum::ethereum.call has not been defined" (проблема). Обходным решением является создание обработчиков файловых источников данных в специальном файле.

Миграция субграфа Crypto Coven

Источники данных GIP-файла

Редактировать страницу

Предыдущий
Поддерживаемые сети
Следующий
AssemblyScript API
Редактировать страницу