Создание субграфа
Reading time: 41 min
Субграф извлекает данные из блокчейна, обрабатывает их и сохраняет таким образом, чтобы их можно было легко запросить с помощью GraphQL.
Определение субграфа состоит из нескольких файлов:
-
subgraph.yaml
: файл YAML, содержащий манифест субграфа -
schema.graphql
: схема GraphQL, которая определяет, какие данные хранятся для Вашего субграфа и как их запрашивать через GraphQL -
AssemblyScript Mappings
: , который преобразует данные события в объекты, определенные в Вашей схеме (например,mapping.ts
в этом руководстве)
Чтобы использовать свой субграф в децентрализованной сети The Graph, Вам необходимо . Рекомендуется в свой субграф как минимум с .
Прежде чем Вы перейдете к подробному описанию содержимого файла манифеста, Вам необходимо установить, который понадобится для создания и развертывания субграфа.
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.4description: Gravatar for Ethereumrepository: https://github.com/graphprotocol/graph-toolingschema:file: ./schema.graphqlindexerHints:prune: autodataSources:- kind: ethereum/contractname: Gravitynetwork: mainnetsource:address: '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'abi: GravitystartBlock: 6175244endBlock: 7175245context:foo:type: Booldata: truebar:type: Stringdata: 'bar'mapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatarabis:- name: Gravityfile: ./abis/Gravity.jsoneventHandlers:- event: NewGravatar(uint256,address,string,string)handler: handleNewGravatar- event: UpdatedGravatar(uint256,address,string,string)handler: handleUpdatedGravatarcallHandlers:- function: createGravatar(string,string)handler: handleCreateGravatarblockHandlers:- handler: handleBlock- handler: handleBlockWithCallfilter:kind: callfile: ./src/mapping.ts
Важными элементами манифеста, которые необходимо обновить, являются:
-
specVersion
: a semver version that identifies the supported manifest structure and functionality for the subgraph. The latest version is1.2.0
. See section to see more details on features & releases. -
description
: понятное описание того, что представляет собой субграф. Это описание отображается в Graph Explorer при развертывании субграфа в Subgraph Studio. -
repository
: URL-адрес репозитория, в котором можно найти манифест субграфа. Это также отображается в The Graph Explorer. -
indexerHints.prune
: определяет срок хранения исторических данных блока для субграфа. См. в разделе . -
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
запись для каждого контракта, данные которого нужно проиндексировать.
Триггеры для источника данных внутри блока упорядочиваются с помощью следующего процесса:
- Триггеры событий и вызовов сначала упорядочиваются по индексу транзакции внутри блока.
- Триггеры событий и вызовов в рамках одной транзакции упорядочиваются по следующему принципу: сначала триггеры событий, затем триггеры вызовов, причем для каждого типа соблюдается тот порядок, в котором они определены в манифесте.
- Триггеры блоков запускаются после триггеров событий и вызовов в том порядке, в котором они определены в манифесте.
Эти правила оформления заказа могут быть изменены.
Примечание: При создании нового обработчики, определенные для динамических источников данных, начнут обработку только после обработки всех существующих обработчиков источников данных и будут повторяться в той же последовательности при каждом запуске.
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 totopic2
, and so on, up totopic3
, since the Ethereum Virtual Machine (EVM) allows up to three indexed arguments per event.
// Идентификатор лицензии SPDX: MITpragma 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 и valueemit Transfer(msg.sender, to, value);}}
В этом примере:
- Событие
Transfer
используется для протоколирования транзакций токенов между адресами. - The
from
andto
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: handleSomeEventtopic1: ['0xValue1', '0xValue2']topic2: ['0xAddress1', '0xAddress2']topic3: ['0xValue3']
В этой настройке:
topic1
соответствует первому индексированному аргументу события,topic2
— второму, аtopic3
— третьему.- Каждая тема может иметь одно или несколько значений, и событие обрабатывается только в том случае, если оно соответствует одному из значений в каждой указанной теме.
- В рамках одной темы: логика действует как условие OR. Событие будет обработано, если оно соответствует любому из перечисленных значений в данной теме.
- Между разными темами: логика функционирует как условие AND. Событие должно удовлетворять всем указанным условиям в разных темах, чтобы вызвать соответствующий обработчик.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleDirectedTransfertopic1: ['0xAddressA'] # Sender Addresstopic2: ['0xAddressB'] # Receiver Address
В данной конфигурации:
topic1
настроен на фильтрацию событийTransfer
, где0xAddressA
является отправителем.topic2
настроен на фильтрацию событийTransfer
, где0xAddressB
является получателем.- Субграф будет индексировать только транзакции, которые происходят непосредственно от
0xAddressA
к0xAddressB
.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleTransferToOrFromtopic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Addresstopic2: ['0xAddressB', '0xAddressC'] # Receiver Address
В данной конфигурации:
topic1
is configured to filterTransfer
events where0xAddressA
,0xAddressB
,0xAddressC
is the sender.topic2
is configured to filterTransfer
events where0xAddressB
and0xAddressC
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.
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).
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:
- Call 1 (Transactions): Takes 3 seconds
- Call 2 (Balance): Takes 2 seconds
- Call 3 (Token Holdings): Takes 4 seconds
Total time taken = 3 + 2 + 4 = 9 seconds
With this feature, you can declare these calls to be executed in parallel:
- Call 1 (Transactions): Takes 3 seconds
- Call 2 (Balance): Takes 2 seconds
- 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
- Declarative Definition: In the subgraph manifest, you declare the Ethereum calls in a way that indicates they can be executed in parallel.
- Parallel Execution Engine: The Graph Node's execution engine recognizes these declarations and runs the calls simultaneously.
- Result Aggregation: Once all calls are complete, the results are aggregated and used by the subgraph for further processing.
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: handleSwapcalls:global0X128: Pool[event.address].feeGrowthGlobal0X128()global1X128: Pool[event.address].feeGrowthGlobal1X128()
Details for the example above:
global0X128
is the declaredeth_call
.- The text before colon(
global0X128
) is the label for thiseth_call
which is used when logging errors. - The text (
Pool[event.address].feeGrowthGlobal0X128()
) is the actualeth_call
that will be executed, which is in the form ofContract[address].function(arguments)
- The
address
andarguments
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()
Файл(ы) ABI должен(ы) соответствовать Вашему контракту (контрактам). Существует несколько способов получения файлов ABI:
- Если Вы создаете свой собственный проект, у Вас, скорее всего, будет доступ к наиболее актуальным ABIS.
- Если Вы создаете субграф для публичного проекта, Вы можете загрузить этот проект на свой компьютер и получить ABI, используя или используя solc для компиляции.
- Вы также можете найти ABI на , но это не всегда надежно, так как загруженный туда ABI может быть устаревшим. Убедитесь, что у Вас есть нужный ABI, в противном случае запуск Вашего субграфа будет неудачным.
Схема для Вашего субграфа находится в файле schema.graphql
. Схемы GraphQL определяются с использованием языка определения интерфейса GraphQL. Если Вы никогда ранее не писали схему GraphQL, рекомендуем ознакомиться с этим руководством по системе типов GraphQL. Справочную документацию по схемам GraphQL можно найти в разделе .
Прежде чем определять объекты, важно сделать шаг назад и подумать о том, как структурированы и связаны Ваши данные. Все запросы будут выполняться к модели данных, определенной в схеме субграфа, и объектам, проиндексированным этим субграфом. Для этого рекомендуется определить схему субграфа таким образом, чтобы она соответствовала потребностям Вашего децентрализованного приложения. Может быть полезно представить объекты как "объекты, содержащие данные", а не как события или функции.
С помощью The Graph Вы просто определяете типы объектов в schema.graphql
, и узел The Graph будет генерировать поля верхнего уровня для запроса отдельных экземпляров и коллекций этого типа объектов. Каждый тип, который должен быть объектом, должен быть аннотирован директивой @entity
. По умолчанию объекты изменяемы, что означает, что мэппинги могут загружать существующие объекты, изменять их и сохранять их новую версию. Измененяемость имеет свою цену, и для типов объектов, для которых известно, что они никогда не будут изменены, например, потому что они просто содержат данные, идентично извлеченные из чейна, рекомендуется помечать их как неизменяемые с помощью @entity(immutable: true)
. Мэппинги могут вносить изменения в неизменяемые объекты до тех пор, пока эти изменения происходят в том же блоке, в котором был создан объект. Неизменяемые объекты гораздо быстрее записываются и запрашиваются, и поэтому их следует использовать каждый раз, когда это возможно.
Приведенный ниже объект Gravatar
структурирован вокруг объекта Gravatar и является хорошим примером того, как объект может быть определен.
type Gravatar @entity(immutable: true) {id: Bytes!owner: BytesdisplayName: StringimageUrl: Stringaccepted: Boolean}
Приведенные ниже примеры объектов GravatarAccepted
и GravatarDeclined
основаны на событиях. Не рекомендуется сопоставлять события или вызовы функций с объектами 1:1.
type GravatarAccepted @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: String}type GravatarDeclined @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: 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 API:
Тип | Описание |
---|---|
Bytes | Массив байтов, представленный в виде шестнадцатеричной строки. Обычно используется для хэшей и адресов Ethereum. |
String | Скаляр для значений string . Нулевые символы не поддерживаются и автоматически удаляются. |
Boolean | Скаляр для значений boolean . |
Int | The GraphQL spec defines Int to be a signed 32-bit integer. |
Int8 | 8-байтовое целое число со знаком, также называемое 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 . |
BigDecimal | BigDecimal Десятичные дроби высокой точности, представленные в виде значащего числа и экспоненты. Диапазон значений экспоненты от -6143 до +6144. Округляется до 34 значащих цифр. |
Timestamp | It is an i64 value in microseconds. Commonly used for timestamp fields for timeseries and aggregations. |
Вы также можете создавать перечисления внутри схемы. Перечисления имеют следующий синтаксис:
enum TokenStatus {OriginalOwnerSecondOwnerThirdOwner}
Как только перечисление определено в схеме, Вы можете использовать строковое представление значения перечисления, чтобы задать поле перечисления для объекта. Например, Вы можете установить для tokenStatus
значение SecondOwner
, сначала определив свой объект, а затем установив в поле entity.tokenStatus = "SecondOwner"
. Приведенный ниже пример демонстрирует, как будет выглядеть объект Token с полем enum:
Более подробную информацию о написании перечислений можно найти в .
Объект может иметь связь с одним или несколькими другими объектами в Вашей схеме. Эти связи могут быть использованы в Ваших запросах. Связи в 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 entityorganization {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 entityid: Bytes!address: Bytes!}
Полнотекстовые поисковые запросы фильтруют и ранжируют объекты на основе введенных данных текстового запроса. Полнотекстовые запросы способны возвращать совпадения по схожим словам путем обработки текста запроса в виде строк перед сравнением с индексированными текстовыми данными.
Определение полнотекстового запроса включает в себя название запроса, словарь языка, используемый для обработки текстовых полей, алгоритм ранжирования, используемый для упорядочивания результатов, и поля, включенные в поиск. Каждый полнотекстовый запрос может охватывать несколько полей, но все включенные поля должны относиться к одному типу объекта.
Чтобы добавить полнотекстовый запрос, включите тип _Schema_
с полнотекстовой директивой в схему GraphQL.
type _Schema_@fulltext(name: "bandSearch"language: enalgorithm: rankinclude: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }])type Band @entity {id: Bytes!name: String!description: String!bio: Stringwallet: Addresslabels: [Label!]!discography: [Album!]!members: [Musician!]!}
Поле example bandSearch
можно использовать в запросах для фильтрации объектов Band
на основе текстовых документов в name
, description
и bio
.> поля. Перейдите к для описания API полнотекстового поиска и дополнительных примеров использования.
query {bandSearch(text: "breaks & electro & detroit") {idnamedescriptionwallet}}
: Начиная с 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 | Аналогично рангу, но также включает в себя близость совпадений. |
Мэппинги берут данные из определенного источника и преобразуют их в объекты, которые определены в Вашей схеме. Мэппинги записываются в подмножестве , которое называется , и которое может быть скомпилировано в WASM (). 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.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.save()}export function handleUpdatedGravatar(event: UpdatedGravatar): void {let id = event.params.idlet gravatar = Gravatar.load(id)if (gravatar == null) {gravatar = new Gravatar(id)}gravatar.owner = event.params.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.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() / 86400let id = Bytes.fromI32(dayID)
- Преобразуйте постоянные адреса в
Bytes
.
const id = Bytes.fromHexString('0xdead...beef')
Существует , которая содержит утилиты для взаимодействия с хранилищем Graph Node и удобства для обработки данных и объектов смарт-контрактов. Её можно импортировать в mapping.ts
из @graphprotocol/graph-ts
.
При создании и сохранении нового объекта, если объект с таким же идентификатором уже существует, в процессе слияния приоритетны свойства нового объекта. Это означает, что существующий объект будет обновлен значениями из нового объекта.
Если для поля нового объекта с тем же идентификатором намеренно установлено нулевое значение, существующий объект будет обновлен с использованием нулевого значения.
Если для поля в новом объекте с тем же идентификатором не установлено значение, поле также будет иметь значение null.
Для упрощения и обеспечения безопасности типов при работе со смарт-контрактами, событиями и объектами Graph CLI может генерировать типы AssemblyScript на основе схемы GraphQL субграфа и ABI контрактов, включенных в источники данных.
Это делается с помощью
graph codegen [--output-dir <OUTPUT_DIR>] [<MANIFEST>]
но в большинстве случаев субграфы уже предварительно сконфигурированы с помощью package.json
, что позволяет Вам просто запустить одно из следующих действий для достижения того же результата:
# Yarnyarn codegen# NPMnpm 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.
Сначала Вы определяете обычный источник данных для основного контракта. Во фрагменте ниже показан упрощенный пример источника данных для контракта фабрики обмена . Обратите внимание на обработчик события New Exchange(address,address)
. Этот сигнал выдается, когда новый контракт обмена создается в цепочке с помощью заводского контракта.
dataSources:- kind: ethereum/contractname: Factorynetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: Factorymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Directoryabis:- name: Factoryfile: ./abis/factory.jsoneventHandlers:- event: NewExchange(address,address)handler: handleNewExchange
Затем Вы добавляете data source templates в манифест. Они идентичны обычным источникам данных, за исключением того, что в них отсутствует предопределенный адрес контракта в source
. Как правило, Вы определяете один шаблон для каждого типа субконтракта, управляемого родительским контрактом, или на который ссылается родительский контракт.
dataSources:- kind: ethereum/contractname: Factory# ... other source fields for the main contract ...templates:- name: Exchangekind: ethereum/contractnetwork: mainnetsource:abi: Exchangemapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/exchange.tsentities:- Exchangeabis:- name: Exchangefile: ./abis/exchange.jsoneventHandlers:- 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 contractExchange.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/contractname: ExampleSourcenetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: ExampleContractstartBlock: 6627917mapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Userabis:- name: ExampleContractfile: ./abis/ExampleContract.jsoneventHandlers:- event: NewEvent(address,address)handler: handleNewEvent
Примечание: Блок создания контракта можно быстро найти в Etherscan:
- Найдите контракт, введя его адрес в строке поиска.
- Нажмите на хэш транзакции создания в разделе
Contract Creator
. - Загрузите страницу сведений о транзакции, где Вы найдете начальный блок для этого контракта.
Параметр indexerHints
в манифесте субграфа содержит директивы для индексаторов по обработке и управлению субграфом. Это влияет на оперативные решения по обработке данных, стратегиям индексации и оптимизации. В настоящее время в нем предусмотрена опция prune
для управления сохранением или сокращением исторических данных.
Эта функция доступна начиная с specVersion: 1.0.0
indexerHints.prune
: определяет срок хранения исторических данных блока для субграфа. Опции включают в себя:
"never"
: удаление исторических данных не производится; хранит всю историю."auto"
: сохраняет минимально необходимую историю, заданную индексатором, оптимизируя производительность запросов.- Конкретное число: устанавливает индивидуальный лимит на количество сохраняемых исторических блоков.
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:
- , which enable querying the past states of these entities at specific blocks throughout the subgraph's history
- Using the subgraph as a 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
Вы можете проверить самый ранний блок (с историческим состоянием) для данного субграфа, выполнив запрос к :
{indexingStatuses(subgraphs: ["Qm..."]) {subgraphsyncedhealthchains {earliestBlock {number}latestBlock {number}chainHeadBlock { number }}}}
Обратите внимание на то, что earliestBlock
— это самый ранний блок с историческими данными, который будет более поздним, чем startBlock
, указанный в манифесте, если субграф был удален.
Обработчики событий в субграфе реагируют на конкретные события, генерируемые смарт-контрактами в блокчейне, и запускают обработчики, определенные в манифесте подграфа. Это позволяет субграфам обрабатывать и хранить данные о событиях в соответствии с определенной логикой.
Обработчик событий объявлен внутри источника данных в конфигурации YAML субграфа. Он определяет, какие события следует прослушивать, и соответствующую функцию, которую необходимо выполнить при обнаружении этих событий.
dataSources:- kind: ethereum/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsoneventHandlers:- event: Approval(address,address,uint256)handler: handleApproval- event: Transfer(address,address,uint256)handler: handleTransfertopic1: ['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/contractname: Gravitynetwork: mainnetsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsoncallHandlers:- 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.hashlet transaction = new Transaction(id)transaction.displayName = call.inputs._displayNametransaction.imageUrl = call.inputs._imageUrltransaction.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/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsonblockHandlers:- handler: handleBlock- handler: handleBlockWithCallToContractfilter:kind: call
Требуется specVersion
>= 0.0.8
Примечание. Фильтры опроса доступны только для источников данных kind: ethereum
.
blockHandlers:- handler: handleBlockfilter:kind: pollingevery: 10
Определенный обработчик будет вызываться один раз для каждого блока n
, где n
— это значение, указанное в поле every
. Эта конфигурация позволяет субграфу выполнять определенные операции через регулярные интервалы блоков.
Требуется specVersion
>= 0.0.8
Примечание. Однократные фильтры доступны только для источников данных kind: ethereum
.
blockHandlers:- handler: handleOncefilter: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.hashlet 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: handleNewGravatarreceipt: true
Внутри функции обработчика доступ к подтверждению можно получить в поле Event.receipt
. Если для ключа receipt
установлено значениеfalse
или оно опущено в манифесте, вместо него будет возвращено значение null
.
Начиная с specVersion
0.0.4
, функции субграфа должны быть явно объявлены в разделе features
на верхнем уровне файла манифеста, используя их имя camelCase
, как указано в таблице ниже:
Например, если в субграфе используются функции Full-Text Search и Non-fatal Errors, поле features
в манифесте должно быть:
specVersion: 0.0.4description: Gravatar for Ethereumfeatures:- fullTextSearch- nonFatalErrorsdataSources: ...
Обратите внимание, что использование функции без ее объявления приведет к ошибке проверки во время развертывания субграфа, но никаких ошибок не возникнет, если функция объявлена, но не используется.
Тайм-серии и агрегации позволяют Вашему субграфу отслеживать такие статистические данные, как средняя цена за день, общий объем переводов за час и т. д.
Эта функция представляет два новых типа объектов субграфов. Объекты тайм-серий записывают точки данных с временными метками. Объекты агрегирования выполняют заранее объявленные вычисления над точками данных тайм-серий ежечасно или ежедневно, а затем сохраняют результаты для быстрого доступа через 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 }) {idtimestampsum}}
Примечание:
Чтобы использовать тайм-серии и агрегации, субграф должен иметь версию спецификации ≥1.1.0. Обратите внимание, что эта функция может претерпеть значительные изменения, которые могут повлиять на обратную совместимость.
Ошибки индексирования в уже синхронизированных субграфах по умолчанию приведут к сбою субграфа и прекращению синхронизации. В качестве альтернативы субграфы можно настроить на продолжение синхронизации при наличии ошибок, игнорируя изменения, внесенные обработчиком, который спровоцировал ошибку. Это дает авторам субграфов время на исправление своих субграфов, в то время как запросы к последнему блоку продолжают обрабатываться, хотя результаты могут быть противоречивыми из-за бага, вызвавшего ошибку. Обратите внимание на то, что некоторые ошибки всё равно всегда будут фатальны. Чтобы быть нефатальной, ошибка должна быть детерминированной.
Примечание: Сеть The Graph пока не поддерживает нефатальные ошибки, и разработчикам не следует разворачивать субграфы, использующие эту функциональность, в сети через Studio.
Для включения нефатальных ошибок необходимо установить в манифесте субграфа следующий флаг функции:
specVersion: 0.0.4description: Gravatar for Ethereumfeatures:- 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 subgraphblock: 7345624 # Block number
Когда развертывается субграф, манифест которого содержит блок graft
, узел The Graph скопирует данные base
субграфа вплоть до указанного block
включительно, а затем продолжит индексирование нового субграфа начиная с этого блока. Базовый субграф должен существовать на целевом экземпляре узла The Graph и должен быть проиндексирован по крайней мере до заданного блока. Из-за этого ограничения графтинг следует использовать только в процессе разработки или в экстренных случаях, чтобы ускорить создание эквивалентного графтового субграфа.
Поскольку графтинг копирует, а не индексирует базовые данные, гораздо быстрее перенести субграф в нужный блок, чем индексировать с нуля, хотя для очень больших субграфов копирование исходных данных может занять несколько часов. Пока графтовый субграф инициализируется, узел The Graph будет регистрировать информацию о типах объектов, которые уже были скопированы.
Графтовый субграф может использовать схему GraphQL, которая не идентична схеме базового субграфа, а просто совместима с ней. Она сама по себе должна быть допустимой схемой субграфа, но может отличаться от схемы базового субграфа следующими способами:
- Она добавляет или удаляет типы объектов
- Она удаляет атрибуты из типов объектов
- Она добавляет в типы объектов атрибуты с возможностью обнуления
- Она превращает ненулевые атрибуты в нулевые
- Она добавляет значения в перечисления
- Она добавляет или удаляет интерфейсы
- Она изменяется в зависимости от того, для каких типов объектов реализован тот или иной интерфейс
Источники файловых данных — это новая функциональность субграфа для надежного и расширенного доступа к данным вне чейна во время индексации. Источники данных файлов поддерживают получение файлов из 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 >=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: BigIntowner: User!}
Новый разделенный объект:
type Token @entity {id: ID!tokenID: BigInt!tokenURI: String!ipfsURI: TokenMetadataupdatedAtTimestamp: BigIntowner: String!}type TokenMetadata @entity {id: ID!image: String!externalURL: String!name: String!description: String!}
Если между родительским объектом и результирующим объектом-источником данных существует связь1:1, то наиболее простым вариантом будет связать родительский объект с результирующим файловым объектом, используя в качестве поиска IPFS CID. Свяжитесь с нами в Discord, если у Вас возникли трудности с моделированием новых объектов на основе файлов!
Это источник данных, который будет создан при обнаружении интересующего файла.
templates:- name: TokenMetadatakind: file/ipfsmapping:apiVersion: 0.0.7language: wasm/assemblyscriptfile: ./src/mapping.tshandler: handleMetadataentities:- TokenMetadataabis:- name: Tokenfile: ./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 поддерживает , а также идентификаторы контента с каталогами (например, bafyreighykzv2we26wfrbzkcdw37sbrby4upq7ae3aqobbq7i4er3tnxci/metadata.json
).
Что касается Arweave, то начиная с версии 0.33.0 Graph Node может извлекать файлы, хранящиеся в Arweave, на основе их из шлюза Arweave (). Arweave поддерживает транзакции, загруженные через Irys (ранее Bundlr), а Graph Node также может получать файлы на основе .
Пример:
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.tokenIdtoken.tokenURI = '/' + event.params.tokenId.toString() + '.json'const tokenIpfsHash = ipfshash + token.tokenURI//Это создает путь к метаданным для одного сборщика NFT Crypto. Он объединяет каталог с "/" + filename + ".json"token.ipfsURI = tokenIpfsHashTokenMetadataTemplate.create(tokenIpfsHash)}token.updatedAtTimestamp = event.block.timestamptoken.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 в качестве идентификатора.
Вы можете использовать при создании файловых источников данных для передачи дополнительной информации, которая будет доступна обработчику файлового источника данных.
Если у Вас есть объекты, которые обновляются несколько раз, создайте уникальные объекты на основе файлов, используя хэш IPFS и идентификатор объекта, и ссылайтесь на них, используя производное поле в объекте на чейн-основе.
Мы работаем над улучшением приведенной выше рекомендации, поэтому запросы возвращают только "самую последнюю" версию
Файловые источники данных в настоящее время требуют ABI, даже если ABI не используются (). Обходным решением является добавление любого ABI.
Обработчики для файловых источников данных не могут находиться в файлах, которые импортируют привязки контракта eth_call
, с ошибкой "unknown import: ethereum::ethereum.call
has not been defined" (). Обходным решением является создание обработчиков файловых источников данных в специальном файле.