Developing > Como criar um Subgraph

Como criar um Subgraph

Reading time: 47 min

Um subgraph extrai dados de uma blockchain, os processa e os armazena para poderem ser consultados facilmente via GraphQL.

Como definir um Subgraph

A definição de subgraph consiste de alguns arquivos:

Para utilizar o seu subgraph na rede descentralizada do The Graph, será necessário criar uma chave API. É recomendado adicionar um sinal ao seu subgraph com, no mínimo, 3000 GRT.

Antes de se aprofundar nos conteúdos do arquivo manifest, instale o Graph CLI, que será necessário para construir e adicionar um subgraph.

Como instalar o Graph CLI

Link para esta seção

O Graph CLI é escrito em JavaScript, e só pode ser usado após instalar o yarn ou o npm; vamos supor que tens o yarn daqui em diante.

Quando tiver o yarn, instale o Graph CLI com o seguinte

Instalação com o yarn:

yarn global add @graphprotocol/graph-cli

Instalação com o npm:

npm install -g @graphprotocol/graph-cli

Instalado, o comando graph init pode preparar um novo projeto de subgraph, seja de um contrato existente ou de um exemplo de subgraph. Este comando serve para criar um subgraph no Subgraph Studio ao passar o graph init --product subgraph-studio. Se já tem um contrato inteligente lançado na mainnet do Ethereum ou uma de suas testnets, inicializar um novo subgraph daquele contrato pode ser um bom começo.

De um Contrato Existente

Link para esta seção

O seguinte comando cria um subgraph que indexa todos os eventos de um contrato existente. Ele tenta buscar a ABI de contrato do Etherscan e resolve solicitar um local de arquivo. Se quaisquer dos argumentos opcionais estiverem a faltar, ele levará-te a um formulário interativo.

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

O <SUBGRAPH_SLUG> é a ID do seu subgraph no Subgraph Studio, visível na página dos detalhes do seu subgraph.

De um Exemplo de Subgraph

Link para esta seção

O segundo modo que o graph init apoia é criar um projeto a partir de um exemplo de subgraph. O seguinte comando faz isso:

graph init --studio <SUBGRAPH_SLUG>

O subgraph de exemplo é baseado no contrato Gravity por Dani Grant, que administra avatares de usuários e emite eventos NewGravatar ou UpdateGravatar sempre que avatares são criados ou atualizados. O subgraph lida com estes eventos ao escrever entidades Gravatar ao armazenamento do Graph Node e garantir que estes são atualizados de acordo com os eventos. As seguintes secções lidarão com os arquivos que compõem o manifest do subgraph para este exemplo.

Como Adicionar Novos dataSources para um Subgraph Existente

Link para esta seção

Desde a v0.31.0, o graph-cli apoia a adição de novos dataSources para um subgraph existente, através do comando graph add.

graph add <address> [<subgraph-manifest default: "./subgraph.yaml">]
Opções:
--abi <path> Caminho à ABI do contrato (padrão: baixar do Etherscan)
--contract-name Nome do contrato (padrão: Contract)
--merge-entities Se fundir ou não entidades com o mesmo nome (padrão: false)
--network-file <path> Caminho ao arquivo de configuração das redes (padrão: "./networks.json")

O comando add pegará a ABI do Etherscan (a não ser que um caminho para a ABI seja especificado com a opção --abi), e criará um novo dataSource da mesma maneira que o comando graph init cria um dataSource --from-contract, a atualizar o schema e os mapeamentos de acordo.

A opção --merge entities identifica como o programador gostaria de lidar com nomes de conflito em entity e event:

  • Se for true: o novo dataSource deve usar eventHandlers & entities existentes.
  • Se for false: um novo handler de entidades & eventos deve ser criado com ${dataSourceName}{EventName}.

O endereço (address) será escrito ao networks.json para a rede relevante.

Nota: Quando usar a cli interativa, após executar o graph init com êxito, receberá uma solicitação para adicionar um novo dataSource.

O Manifest do Subgraph

Link para esta seção

O manifest do subgraph subgraph.yaml define os contratos inteligentes indexados pelo seu subgraph; a quais eventos destes contratos prestar atenção; e como mapear dados de eventos a entidades que o Graph Node armazena e permite queries. Veja a especificação completa para manifests de subgraph aqui.

Para o subgraph de exemplo, o 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

As entradas importantes para atualizar para o manifest são:

  • specVersion: uma versão de semver que identifica a estrutura de mainfest apoiada e a sua funcionalidade para o subgraph. A versão mais recente é 1.2.0. Veja a secção de lançamentos do specVersion para mais detalhes sobre recursos & lançamentos.

  • description: uma descrição legível a humanos do que é o subgraph. Esta descrição é exibida pelo Graph Explorer quando o subgraph é lançado ao Subgraph Studio.

  • repository: a URL do repositório onde está o manifest do subgraph. Isto também é exibido no Graph Explorer.

  • features: uma lista de todos os nomes de feature usados.

  • indexerHints.prune: Define a retenção de dados históricos de blocos para um subgraph. Veja prune na secção indexerHints.

  • dataSources.source: o endereço do contrato inteligente que abastece o subgraph, e a ABI do contrato inteligente a ser usada. O endereço é opcional; omiti-lo permite indexar eventos correspondentes de todos os contratos.

  • dataSources.source.startBlock: o número opcional do bloco de onde a fonte de dados começa a indexar. Em muitos casos, sugerimos usar o bloco em que o contrato foi criado.

  • dataSources.source.endBlock: O número opcional do bloco onde a fonte de dados pára de indexar, o que inclui aquele bloco. Versão de spec mínima requerida: 0.0.9.

  • dataSources.context: pares de key-value que podem ser usados dentro de mapeamentos de subgraph. Apoia vários tipos de dados como Bool, String, Int, Int8, BigDecimal, Bytes, List, e BigInt. Cada variável deve especificar o seu type e data. Estas variáveis de contexto são então acessíveis nos arquivos de mapeamento, a fim de oferecer opções mais configuráveis para o desenvolvimento de subgraphs.

  • dataSources.mapping.entities: as entidades que a fonte de dados escreve ao armazenamento. O schema para cada entidade é definido no arquivo schema.graphql.

  • dataSources.mapping.abis: um ou mais arquivos ABI nomeados para o contrato-fonte, além de quaisquer outros contratos inteligentes com os quais interage de dentro dos mapeamentos.

  • dataSources.mapping.eventHandlers: lista os eventos de contratos inteligentes aos quais este subgraph reage, e os handlers no mapping — ./src/mapping.ts no exemplo — que transformam estes eventos em entidades no armazenamento.

  • dataSources.mapping.callHandlers: lista as funções de contratos inteligentes aos quais este subgraph reage, e os handlers no mapping que transformam as entradas e saídas para chamadas de função em entidades no armazenamento.

  • dataSources.mapping.blockHandlers: lista os blocos aos quais este subgraph reage, e handlers no mapeamento quando um bloco é atrelado à chain. Sem um filtro, o handler de blocos será executado em todo bloco. Um filtro de chamada opcional pode ser fornecido ao adicionar um campo filter com kind: call no handler. Isto só executará o handler se o bloco conter no mínimo uma chamada ao contrato da fonte de dados.

Um único subgraph pode indexar dados de vários contratos inteligentes. Adicione uma entrada para cada contrato cujos dados devem ser indexados ao arranjo dataSources.

Ordem de Handlers de Gatilhos

Link para esta seção

Os gatilhos para uma fonte de dados dentro de um bloco são ordenados com o seguinte processo:

  1. Gatilhos de evento e chamada são, primeiro, ordenados por índice de transação no bloco.
  2. Gatilhos de evento e chamada dentro da mesma transação são ordenados a usar uma convenção: primeiro, gatilhos de evento, e depois, de chamada, cada tipo a respeitar a ordem em que são definidos no manifest.
  3. Gatilhos de blocos são executados após gatilhos de evento e chamada, na ordem em que são definidos no manifest.

Estas regras de organização estão sujeitas à mudança.

Nota: Quando novas fontes de dados dinâmicas forem criadas, os handlers definidos para fontes de dados dinâmicas só começarão o processamento após todos os handlers existentes forem processados, e repetirão a mesma sequência quando ativados.

Filtros de Argumentos Indexados / Filtros de Tópicos

Link para esta seção

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.

Como Filtros de Tópicos Funcionam

Link para esta seção

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-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Token {
// Declaração de evento com parâmetros indexados para endereços
event Transfer(address indexed from, address indexed to, uint256 value);
// Função para simular a transferência de tokens
function transfer(address to, uint256 value) public {
// Emitting the Transfer event with from, to, and value
emit Transfer(msg.sender, to, value);
}
}

Neste exemplo:

  • O evento Transfer é usado para gravar transações de tokens entre endereços.
  • The from and to parameters are indexed, allowing event listeners to filter and monitor transfers involving specific addresses.
  • A função transfer é uma representação simples de uma ação de transferência de token, e emite o evento Transfer sempre que é chamada.

Configuração em Subgraphs

Link para esta seção

Filtros de tópicos são definidos diretamente na configuração de handlers de eventos no manifest do subgraph. Veja como eles são configurados:

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

Neste cenário:

  • topic1 corresponde ao primeiro argumento indexado do evento, topic2 ao segundo e topic3 ao terceiro.
  • Cada tópico pode ter um ou mais valores, e um evento só é processado se corresponder a um dos valores em cada tópico especificado.
Lógica de Filtro
Link para esta seção
  • Dentro de um Tópico Único: A lógica funciona como uma condição OR. O evento será processado se corresponder a qualquer dos valores listados num tópico.
  • Entre Tópicos Diferentes: A lógica funciona como uma condição AND. Um evento deve atender a todas as condições especificadas em vários tópicos para acionar o handler associado.

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

Link para esta seção
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleDirectedTransfer
topic1: ['0xAddressA'] # Sender Address
topic2: ['0xAddressB'] # Receiver Address

Nesta configuração:

  • topic1 é configurado para filtrar eventos Transfer onde 0xAddressA é o remetente.
  • topic2 é configurado para filtrar eventos Transfer onde 0xAddressB é o remetente.
  • O subgraph só indexará transações que ocorrerem diretamente do 0xAddressA ao 0xAddressB.

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

Link para esta seção
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransferToOrFrom
topic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Address
topic2: ['0xAddressB', '0xAddressC'] # Receiver Address

Nesta configuração:

  • 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.

eth_call declarada

Link para esta seção

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

Link para esta seção

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

Link para esta seção

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.

Exemplo de Configuração no Manifest do Subgraph

Link para esta seção

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()

Versões do SpecVersion

Link para esta seção
VersãoNotas de atualização
1.2.0Adicionado apoio a Filtragem de Argumentos Indexados & eth_call declarado
1.1.0Apoio a Séries de Tempo & Agregações. Apoio adicionado ao tipo Int8 para id.
1.0.0Apoia o recurso indexerHints para fazer pruning de subgraphs
0.0.9Apoio ao recurso endBlock
0.0.8Adicionado apoio ao polling de Handlers de Bloco e Handlers de Inicialização.
0.0.7Adicionado apoio a Fontes de Arquivos de Dados.
0.0.6Apoio à variante de calculação de Proof of Indexing.
0.0.5Adicionado apoio a handlers de eventos com acesso a recibos de transação.
0.0.4Adicionado apoio à gestão de recursos de subgraph.

Como Obter as ABIs

Link para esta seção

Os arquivos da ABI devem combinar com o(s) seu(s) contrato(s). Há algumas maneiras de obter estes arquivos:

  • Caso construa o seu próprio projeto, provavelmente terá acesso às suas ABIs mais recentes.
  • Caso construa uma subgraph para um projeto público, pode baixar aquele projeto no seu computador e construir a ABI ao usar o comando truffle compile ou compilar com solc.
  • Também pode achar a ABI no Etherscan, mas isto nem sempre é confiável, pois a ABI exibida lá pode estar ultrapassada. Tenha certeza que tem a ABI certa, senão, pode haver um erro ao executar o seu subgraph.

O Schema GraphQL

Link para esta seção

O schema para o seu subgraph está no arquivo schema.graphql. Schemas de GraphQL são definidos usando a linguagem de definição GraphQL. Se você nunca tiver escrito um schema nesta linguagem, confira este preparatório no sistema de tipos do GraphQL. A documentação de referência para schemas em GraphQL está na seção API GraphQL.

Como Definir Entidades

Link para esta seção

Antes de definir entidades, é important parar para pensar sobre como os seus dados são estruturados e ligados. Todas as consultas serão feitas perante o modelo de dados definido no schema do subgraph e as entidades indexadas pelo subgraph. Portanto, é bom definir o schema do subgraph de uma forma que atenda as necessidades do seu dApp. Pode ser conveniente imaginar entidades como "objetos a conter dados", ao invés de eventos ou funções.

Com o The Graph, pode simplesmente definir tipos de entidade no schema.graphql; assim, o Graph Node gerará campos de nível alto para fazer queries de instâncias únicas e coleções daquele tipo de entidade. Cada tipo que deve ser uma entidade tem de ser anotado com uma diretiva @entity. As entidades são mutáveis, o que significa que os mapeamentos podem carregar entidades existentes, modificá-las e armazenar novas versões delas. A mutabilidade vem com um preço; por exemplo, para tipos de entidade que claramente não podem ser alterados por conter dados extraídos exatamente da chain, é recomendado marcá-los como imutáveis com @entity(immutable: true). Entidades imutáveis podem ser alteradas com mapeamentos, desde que as alterações aconteçam no mesmo bloco em que a entidade foi criada. Entidades imutáveis são muito mais rápidas de escrever e consultar, e então devem ser usadas sempre que possível.

A entidade Gravatar embaixo é estruturada em torno de um objeto Gravatar, e é um bom exemplo de como pode ser definida uma entidade.

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

As entidades GravatarAccepted e GravatarDeclined abaixo têm base em torno de eventos. Não é recomendado mapear eventos ou chamadas de função a entidades identicamente.

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

Campos Opcionais e Obrigatórios

Link para esta seção

Campos de entidade podem ser definidos como obrigatórios ou opcionais. Os obrigatórios são indicados no schema pelo código !. Se um campo obrigatório não for determinado no mapeamento, receberá este erro ao consultar o campo:

Null value resolved for non-null field 'name'

Cada entidade deve ter um campo id, que deve ser do tipo Bytes! ou String!. Geralmente é melhor usar Bytes! — a não ser que o id tenha texto legível para humanos, já que entidades com as ids Bytes! são mais fáceis de escrever e consultar como aquelas com um id String!. O campo id serve como a chave primária, e deve ser singular entre todas as entidades do mesmo tipo. Por razões históricas, o tipo ID! também é aceito, como um sinônimo de String!.

Para alguns tipos de entidade, o id é construído das id's de duas outras entidades; isto é possível com o concat, por ex., let id = left.id.concat(right.id) para formar a id a partir das id's de left e right. Da mesma forma, para construir uma id a partir da id de uma entidade existente e um contador count, pode ser usado o let id = left.id.concatI32(count). Isto garante a concatenação a produzir id's únicas enquanto o comprimento do left for o mesmo para todas as tais entidades; por exemplo, porque o left.id é um Address (endereço).

Tipos Embutidos de Escalar

Link para esta seção

Escalares Apoiados pelo GraphQL

Link para esta seção

Nós apoiamos os seguintes escalares na nossa API do GraphQL:

TipoDescrição
BytesArranjo de bytes, representado como string hexadecimal. Usado frequentemente por hashes e endereços no Ethereum.
StringEscalar para valores string. Caracteres nulos são removidos automaticamente.
BooleanEscalar para valores boolean.
IntA especificação do GraphQL define o Int como um inteiro assinado de 32 bits.
Int8Um número inteiro assinado em 8 bits, também conhecido como um número inteiro assinado em 64 bits, pode armazenar valores de -9,223,372,036,854,775,808 a 9,223,372,036,854,775,807. Prefira usar isto para representar o i64 do ethereum.
BigIntNúmeros inteiros grandes. Usados para os tipos uint32, int64, uint64, ..., uint256 do Ethereum. Nota: Tudo abaixo de uint32, como int32, uint24 ou int8 é representado como i32.
BigDecimalBigDecimal Decimais de alta precisão representados como um significando e um exponente. O alcance de exponentes é de -6143 até +6144. Arredondado para 34 dígitos significantes.
TimestampÉ um valor i64 em microssegundos. Usado frequentemente para campos timestamp para séries de tempo e agregações.

Também pode criar enums dentro de um schema. Enums têm a seguinte sintaxe:

enum TokenStatus {
OriginalOwner
SecondOwner
ThirdOwner
}

Quando o enum for definido no schema, pode usar a representação do string do valor enum para determinar um campo enum numa entidade. Por exemplo, pode implantar o tokenStatus no SecondOwner ao definir primeiro a sua entidade e depois determinar o campo com entity.tokenStatus = "SecondOwner". O exemplo abaixo demonstra como ficaria a entidade do Token com um campo enum:

Veja mais detalhes sobre a escrita de enums na documentação do GraphQL.

Relacionamentos de Entidades

Link para esta seção

Uma entidade pode ter relacionamentos com uma ou mais entidades no seu schema; estes podem ser tratados nas suas consultas. Os relacionamentos no The Graph são unidirecionais, e é possível simular relacionamentos bidirecionais ao definir um relacionamento unidirecional em cada "lado" do relacionamento projetado.

Relacionamentos são definidos em entidades como qualquer outro campo, sendo que o tipo especificado é o de outra entidade.

Relacionamentos Um-com-Um

Link para esta seção

Defina um tipo de entidade Transaction com um relacionamento um-com-um opcional, com um tipo de entidade TransactionReceipt:

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

Relacionamentos Um-com-Vários

Link para esta seção

Defina um tipo de entidade TokenBalance com um relacionamento um-com-vários, exigido com um tipo de entidade Token:

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

Buscas Reversas

Link para esta seção

Buscas reversas podem ser definidas em uma entidade pelo campo @derivedFrom. Isto cria um campo virtual na entidade, que pode ser consultado, mas não pode ser configurado manualmente pela API de mapeamentos. Em vez disto, ele é derivado do relacionamento definido na outra entidade. Para tais relacionamentos, raramente faz sentido armazenar ambos os lados do relacionamento, e tanto o indexing quanto o desempenho dos queries melhorarão quando apenas um lado for armazenado, e o outro derivado.

Para relacionamentos um-com-vários, o relacionamento sempre deve ser armazenado no lado 'um', e o lado 'vários' deve sempre ser derivado. Armazenar o relacionamento desta maneira, em vez de armazenar um arranjo de entidades no lado 'vários', melhorará dramaticamente o desempenho para o indexing e os queries no subgraph. Em geral, evite armazenar arranjos de entidades enquanto for prático.

Podemos fazer os saldos para um token acessíveis a partir do mesmo token ao derivar um campo tokenBalances:

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

Relacionamentos Vários-com-Vários

Link para esta seção

Para relacionamentos vários-com-vários, como um conjunto de utilizadores em que cada um pertence a qualquer número de organizações, o relacionamento é mais simplesmente — mas não mais eficientemente — modelado como um arranjo em cada uma das duas entidades envolvidas. Se o relacionamento for simétrico, apenas um lado do relacionamento precisa ser armazenado, e o outro lado pode ser derivado.

Defina uma busca reversa a partir de um tipo de entidade User para um tipo de entidade Organization. No exemplo abaixo, isto é feito ao buscar pelo atributo members a partir de dentro da entidade Organization. Em queries, o campo organizations no User será resolvido ao encontrar todas as entidades Organization que incluem a ID do utilizador.

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

Uma maneira mais eficiente para armazenar este relacionamento é com uma mesa de mapeamento que tem uma entrada para cada par de User / Organization, com um schema como

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!
}

Esta abordagem requer que os queries desçam a um nível adicional para retirar, por exemplo, as organizações para utilizadores:

query usersWithOrganizations {
users {
organizations {
# isto é uma entidade UserOrganization
organization {
name
}
}
}
}

Esta maneira mais elaborada de armazenar relacionamentos vários-com-vários armazenará menos dados para o subgraph, portanto, o subgraph ficará muito mais rápido de indexar e consultar.

Como adicionar comentários ao schema

Link para esta seção

Pela especificação do GraphQL, é possível adicionar comentários acima de atributos de entidade do schema com o símbolo de hash #. Isto é ilustrado no exemplo abaixo:

type MyFirstEntity @entity {
# identificador único e chave primária da entidade
id: Bytes!
address: Bytes!
}

Como Definir Campos de Busca Fulltext

Link para esta seção

Buscas fulltext filtram e ordenam entidades baseadas num texto inserido. Queries fulltext podem retornar resultados para palavras semelhantes ao processar o texto inserido antes de compará-los aos dados do texto indexado.

Uma definição de query fulltext inclui: o nome do query, o dicionário do idioma usado para processar os campos de texto, o algoritmo de ordem usado para ordenar os resultados, e os campos incluídos na busca. Todo query fulltext pode ter vários campos, mas todos os campos incluídos devem ser de um único tipo de entidade.

Para adicionar um query fulltext, inclua um tipo _Schema_ com uma diretiva fulltext no schema em 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!]!
}

O exemplo bandSearch serve, em queries, para filtrar entidades Band baseadas nos documentos de texto nos campos name, description e bio. Confira a página API GraphQL - Consultas para uma descrição da API de busca fulltext e mais exemplos de uso.

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

Gestão de Características: A partir do specVersion 0.0.4 em diante, o fullTextSearch deve ser declarado sob a seção features no manifest do subgraph.

Idiomas apoiados

Link para esta seção

Escolher um idioma diferente terá um efeito definitivo, porém às vezes sutil, na API da busca fulltext. Campos cobertos por um campo de query fulltext serão examinados no contexto do idioma escolhido, para que os lexemas produzidos pela análise e pelos queries de busca variem de idioma para idioma. Por exemplo: ao usar o dicionário turco, "token" é abreviado para "toke" enquanto, claro, o dicionário em inglês o categorizará como "token".

Dicionários apoiados:

CódigoDicionário
simpleGeral
daDinamarquês
nlHolandês
enInglês
fiFinlandês
frFrancês
deAlemão
huHúngaro
itItaliano
noNorueguês
ptPortuguês
roRomeno
ruRusso
esEspanhol
svSueco
trTurco

Algoritmos de Ordem

Link para esta seção

Algoritmos apoiados para a organização de resultados:

AlgoritmoDescrição
rankOrganiza os resultados pela qualidade da correspondência (0-1) da busca fulltext.
proximityRankParecido com o rank, mas também inclui a proximidade das correspondências.

Como Escrever Mapeamentos

Link para esta seção

Os mapeamentos tomam dados de uma fonte particular e os transformam em entidades que são definidas dentro do seu schema. São escritos em um subconjunto do TypeScript chamado AssemblyScript, que pode ser compilado ao WASM (WebAssembly). O AssemblyScript é mais rígido que o TypeScript normal, mas rende uma sintaxe familiar.

Para cada handler de evento definido no subgraph.yaml sob o mapping.eventHandlers, crie uma função exportada de mesmo nome. Cada handler deve aceitar um único parâmetro chamado event com um tipo a corresponder ao nome do evento sendo lidado.

No subgraph de exemplo, o src/mapping.ts contém handlers para os eventos NewGravatar e 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()
}

O primeiro handler toma um evento NewGravatar e cria uma nova entidade Gravatar com o new Gravatar(event.params.id.toHex()), e assim popula os campos da entidade com os parâmetros de evento correspondentes. Esta instância da entidade é representada pelo variável gravatar, com um valor de id de event.params.id.toHex().

O segundo handler tenta carregar o Gravatar do armazenamento do Graph Node. Se ele ainda não existe, ele é criado por demanda. A entidade é então atualizada para corresponder aos novos parâmetros de evento, antes de ser devolvida ao armazenamento com gravatar.save().

IDs Recomendadas para Criar Novas Entidades

Link para esta seção

Recomendamos muito utilizar Bytes como o tipo para campos id, e só usar o String para atributos que realmente contenham texto legível para humanos, como o nome de um token. Abaixo estão alguns valores recomendados de id para considerar ao criar novas entidades.

  • transfer.id = event.transaction.hash

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

  • Para entidades que armazenam dados agregados como, por exemplo, volumes diários de trading, a id costuma conter o número do dia. Aqui, usar Bytes como a id é beneficial. Determinar a id pareceria com

let dayID = event.block.timestamp.toI32() / 86400
let id = Bytes.fromI32(dayID)
  • Converta endereços constantes em Bytes.

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

Há uma Biblioteca do Graph Typescript, com utilidades para interagir com o armazenamento do Graph Node e conveniências para lidar com entidades e dados de contratos inteligentes. Ela pode ser importada ao mapping.ts do @graphprotocol/graph-ts.

Gestão de entidades com IDs idênticas

Link para esta seção

Ao criar e salvar uma nova entidade, se já houver uma com a mesma ID, vale sempre usar as propriedades da nova entidade durante o processo de fusão. Isto significa que a entidade existente será atualizada com os valores da entidade nova.

Se um valor nulo for propositadamente determinado para um campo na nova entidade com a mesma ID, a entidade existente será atualizada com o valor nulo.

Se nenhum valor for inserido para um campo na nova entidade com a mesma ID, o campo também resultará em nulo.

Geração de Código

Link para esta seção

Para tornar mais fácil e seguro a tipos o trabalho com contratos inteligentes, eventos e entidades, o Graph CLI pode gerar tipos de AssemblyScript a partir do schema GraphQL do subgraph e das ABIs de contratos incluídas nas fontes de dados.

Isto é feito com

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

mas geralmente, os subgraphs já são pré-configurados através do package.json para alcançar o mesmo com a execução de um dos seguintes:

# Yarn
yarn codegen
# NPM
npm run codegen

Isto gerará uma classe de AssemblyScript para todo contrato inteligente nos arquivos ABI mencionados no subgraph.yaml, permitindo ligar estes contratos a endereços específicos nos mapeamentos e chamar métodos de contratos de apenas-leitura contra o bloco a ser processado. Também gerará uma classe para todo evento de contrato para fornecer acesso fácil a parâmetros de eventos, assim como ao bloco e a transação dos quais o evento originou. Todos estes tipos são escritos para <OUTPUT_DIR>/<DATA_SOURCE_NAME>/<ABI_NAME>.ts. No subgraph de exemplo, isto seria o generated/Gravity/Gravity.ts, permitindo que estes tipos sejam importados com mapeamentos.

import {
// Classe do contrato:
Gravity,
// Classes de eventos:
NewGravatar,
UpdatedGravatar,
} from '../generated/Gravity/Gravity'

Além disto, uma classe é gerada para cada tipo de entidade no schema GraphQL do subgraph. Estas classes rendem carregamento, acesso de leitura e escritura para campos de entidades com segurança de tipos, além de um método save() para escrever entidades ao armazenamento. Todas as classes de entidades são escritas no <OUTPUT_DIR>/schema.ts, permitindo que os mapeamentos as importem com

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

Nota: A geração de códigos deve ser executada novamente após todas as mudanças ao schema GraphQL ou às ABIs incluídas no manifest. Ela também deve ser executada pelo menos uma vez antes de construir ou lançar o subgraph.

A geração de código não confere o seu código de mapeamento no src/mapping.ts. Se quiser conferir isto antes de tentar lançar o seu subgraph ao Graph Explorer, pode executar o yarn build e consertar quaisquer erros de sintaxe que o compilador TypeScript possa encontrar.

Modelos de Fontes de Dados

Link para esta seção

Um padrão comum em contratos inteligentes compatíveis com EVMs é o uso de contratos de registro ou fábrica. Nisto, um contrato cria, gesta ou refere a um número arbitrário de outros contratos, cada um com o seu próprio estado e eventos.

Os endereços destes subcontratos podem ou não ser conhecidos imediatamente, e muitos destes contratos podem ser criados e/ou adicionados ao longo do tempo. É por isto que, em muitos casos, é impossível definir uma única fonte de dados ou um número fixo de fontes de dados, e é necessária uma abordagem mais dinâmica: modelos de fontes de dados.

Fonte de Dados para o Contrato Principal

Link para esta seção

Primeiro, defina uma fonte de dados regular para o contrato principal. Abaixo está um exemplo simplificado de fonte de dados para o contrato de fábrica de trocas do Uniswap. Preste atenção ao handler de evento NewExchange(address,address): é emitido quando um novo contrato de troca é criado on-chain pelo contrato de fábrica.

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

Modelos de Fontes de Dados para Contratos Criados Dinamicamente

Link para esta seção

Depois, adicione modelos de fontes de dados ao manifest. Estes são idênticos a fontes de dados regulares, mas não têm um endereço de contrato predefinido sob source. Tipicamente, definiria um modelo para cada tipo de subcontrato gestado ou referenciado pelo contrato parente.

dataSources:
- kind: ethereum/contract
name: Factory
# ... outros campos de fonte para o contrato principal ...
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

Como Instanciar um Modelo de Fontes de Dados

Link para esta seção

No passo final, atualize o seu mapeamento de contratos para criar uma instância dinâmica de fontes de dados de um dos modelos. Neste exemplo, mudarias o mapeamento do contrato principal para importar o modelo Exchange e chamar o método Exchange.create(address) nele, para começar a indexar o novo contrato de troca.

import { Exchange } from '../generated/templates'
export function handleNewExchange(event: NewExchange): void {
// Comece a indexar a troca; `event.params.exchange` é o
// endereço do novo contrato de troca
Exchange.create(event.params.exchange)
}

Nota: Uma nova fonte de dados só processará as chamadas e eventos para o bloco onde ele foi criado e todos os blocos a seguir. Porém, não serão processados dados históricos, por ex, contidos em blocos anteriores.

Se blocos anteriores conterem dados relevantes à nova fonte, é melhor indexá-los ao ler o estado atual do contrato e criar entidades que representem aquele estado na hora que a nova fonte de dados for criada.

Contextos de Fontes de Dados

Link para esta seção

Contextos de fontes de dados permitem passar configurações extras ao instanciar um template. Em nosso exemplo, vamos dizer que trocas são associadas com um par de trading particular, que é incluído no evento NewExchange. Essa informação pode ser passada na fonte de dados instanciada, como:

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)
}

Dentro de um mapeamento do modelo Exchange, dá para acessar o contexto:

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

Há setters e getters como setString e getString para todos os tipos de valores.

Blocos Iniciais

Link para esta seção

O startBlock é uma configuração opcional que permite-lhe definir de qual bloco na chain a fonte de dados começará a indexar. Determinar o bloco inicial permite que a fonte de dados potencialmente pule milhões de blocos irrelevantes. Tipicamente, um programador de subgraph configurará o startBlock ao bloco em que o contrato inteligente da fonte de dados foi criado.

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

Nota: O bloco da criação do contrato pode ser buscado rapidamente no Etherscan:

  1. Procure pelo contrato ao inserir o seu endereço na barra de busca.
  2. Clique no hash da transação da criação na seção Contract Creator.
  3. Carregue a página dos detalhes da transação, onde encontrará o bloco inicial para aquele contrato.

A configuração indexerHints, no manifest de um subgraph, providencia diretivas para Indexadores processarem e gestarem um subgraph. Ela influencia decisões operacionais entre gestão de dados, estratégias de indexação e otimizações. Atualmente ela tem a opção prune para lidar com a retenção ou o pruning de dados históricos.

Este recurso está disponível desde a specVersion: 1.0.0

indexerHints.prune: Define a retenção de dados históricos de bloco para um subgraph. As opções incluem:

  1. "never": Nenhum pruning de dados históricos; retém o histórico completo.
  2. "auto": Retém o histórico mínimo necessário determinado pelo Indexador e otimiza o desempenho das queries.
  3. Um número específico: Determina um limite personalizado no número de blocos históricos a guardar.
indexerHints:
prune: auto

O termo "histórico", neste contexto de subgraphs, refere-se ao armazenamento de dados que refletem os estados antigos de entidades mutáveis.

O histórico, desde um bloco especificado, é necessário para:

  • Queries de viagem no tempo, que permitem queries dos estados anteriores destas entidades em blocos específicos, através do histórico do subgraph
  • O uso do subgraph como uma base de enxerto em outro subgraph naquele bloco
  • Rebobinar o subgraph de volta àquele bloco

Se os dados históricos desde aquele bloco tiverem passado por pruning, as capacidades acima não estarão disponíveis.

Vale usar o "auto", por maximizar o desempenho de queries e ser suficiente para a maioria dos utilizadores que não requerem acesso a dados extensos no histórico.

Para subgraphs que usam queries de viagem no tempo, recomendamos configurar um número especifico de blocos para reter dados históricos ou usar o prune: never para manter todos os estados históricos da entidade. Seguem abaixo exemplos de como configurar ambas as opções nas configurações do seu subgraph:

Para reter uma quantidade específica de dados históricos:

indexerHints:
prune: 1000 # Substitua 1000 pelo número desejado de blocos a reter

Para preservar o histórico completo dos estados da entidade:

indexerHints:
prune: never

É possível verificar o bloco mais antigo (com estado histórico) para um subgraph ao fazer um query da API de Estado de Indexação:

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

Note que o earliestBlock é o bloco mais antigo com dados históricos, que será mais recente que o startBlock (bloco inicial) especificado no manifest, se o subgraph tiver passado por pruning.

Handlers de Eventos

Link para esta seção

Handlers de eventos em um subgraph reagem a eventos específicos emitidos por contratos inteligentes na blockchain e acionam handlers definidos no manifest do subgraph. Isto permite que subgraphs processem e armazenem dados conforme a lógica definida.

Como Definir um Handler de Evento

Link para esta seção

Um handler de evento é declarado dentro de uma fonte de dados na configuração YAML do subgraph. Ele especifica quais eventos devem ser escutados e a função correspondente a ser executada quando estes eventos forem detetados.

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'] # Filtro de tópico opcional que só filtra eventos com o tópico especificado.

Handlers de chamada

Link para esta seção

Enquanto os eventos provém uma forma eficiente de coletar mudanças relevantes ao estado de um contrato, muitos contratos evitam gerar logs para otimizar os custos de gas. Nestes casos, um subgraph pode se inscrever em chamadas feitas ao contrato da fonte de dados. Isto é alcançado ao definir handlers de calls que referenciam a assinatura da função, e o handler de mapeamento que processará chamadas para esta função. Para processar estas chamadas, o handler de mapeamento receberá um ethereum.Call como um argumento com as entradas digitadas e as saídas da chamada. Chamadas feitas a qualquer profundidade na cadeia de chamadas de uma transação irão engatilhar o mapeamento; assim, atividades com o contrato de fontes de dados serão capturados através de contratos de proxy.

Handlers de chamadas só serão ativados em um de dois casos: quando a função especificada é chamada por uma conta que não for do próprio contrato, ou quando ela é marcada como externa no Solidity e chamada como parte de outra função no mesmo contrato.

Nota: Os handlers de chamada atualmente dependem da API de rastreamento do Parity. Certas redes, como Arbitrum e BNB Chain, não apoiam esta API. Se um subgraph que indexa uma destas redes conter um ou mais handlers de chamadas, ele não começará a sincronização. Os programadores de subgraph devem, em vez disto, usar handlers de eventos. Estes têm desempenho bem melhor que handlers de chamadas, e são apoiados em toda rede EVM.

Como Definir um Handler de Chamada

Link para esta seção

Para definir um handler de chamada no seu manifest, apenas adicione um arranjo callHandlers sob a fonte de dados para a qual quer se inscrever.

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

O functionm é a assinatura de função normalizada para filtrar chamadas. A propriedade handler é o nome da função no seu mapeamento que gostaria de executar quando a função-alvo é chamada no contrato da fonte de dados.

Função de Mapeamento

Link para esta seção

Cada handler de chamadas toma um único parâmetro, que tem um tipo correspondente ao nome da função chamada. No exemplo de subgraph acima, o mapeamento contém um handler para quando a função createGravatar é chamada e recebe um CreateGravatarCall como argumento:

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()
}

A função handleCreateGravatar toma um novo CreateGravatarCall que é uma subclasse do ethereum.Call, fornecido pelo @graphprotocol/graph-ts, que inclui as entradas e saídas digitadas da chamada. O tipo CreateGravatarCall é gerado para ti quando executa o graph codegen.

Handlers de Blocos

Link para esta seção

Além de se inscrever a eventos de contratos ou chamadas para funções, um subgraph também pode querer atualizar os seus dados enquanto novos blocos são afixados à chain. Para isto, um subgraph pode executar uma função após cada bloco, ou após blocos que correspondem a um filtro predefinido.

Filtros Apoiados

Link para esta seção
filter:
kind: call

O handler definido será chamado uma vez para cada bloco, que contém uma chamada ao contrato (fonte de dados) sob o qual o handler está definido.

Nota: O filtro call atualmente depende da API de rastreamento do Parity. Certas redes, como Arbitrum e BNB Chain, não apoiam esta API. Se um subgraph que indexa uma destas redes conter um ou mais handlers de blocos com um filtro call, ele não começará a sincronização.

A ausência de um filtro para um handler de blocos garantirá que o handler seja chamado a todos os blocos. Uma fonte de dados só pode conter um handler de bloco para cada tipo de filtro.

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

Filtro Polling

Link para esta seção

Requer specVersion >= 0.0.8

Nota: Filtros de polling só estão disponíveis nas dataSources kind: ethereum.

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

O handler definido será chamado uma vez a cada n blocos, onde n é o valor providenciado no campo every. Esta configuração permite que o subgraph faça operações específicas em intervalos de blocos regulares.

Requer specVersion >= 0.0.8

Nota: Filtros de once só estão disponíveis nas dataSources kind: ethereum.

blockHandlers:
- handler: handleOnce
filter:
kind: once

O handler definido com o filtro once só será chamado uma única vez antes da execução de todos os outros handlers (por isto, o nome "once" / "uma vez"). Esta configuração permite que o subgraph use o handler como um handler de inicialização, para realizar tarefas específicas no começo da indexação.

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

Função de Mapeamento

Link para esta seção

A função de mapeamento receberá um ethereum.block como o seu único argumento. Assim como funções de mapeamento para eventos, esta função pode acessar entidades existentes no armazenamento do subgraph, chamar contratos inteligentes e criar ou atualizar entidades.

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

Eventos Anónimos

Link para esta seção

Caso precise processar eventos anónimos no Solidity, isto é possível ao fornecer o topic 0 do evento, como no seguinte exemplo:

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

Um evento só será ativado quando a assinatura e o topic 0 corresponderem. topic0 é igual ao hash da assinatura do evento.

Recibos de Transação em Handlers de Eventos

Link para esta seção

A partir do specVersion 0.0.5 e apiVersion 0.0.7, handlers de eventos podem ter acesso ao recibo para a transação que os emitiu.

Para fazer isto, os handlers de eventos devem ser declarados no manifest do subgraph com a nova chave receipt: true, sendo esta opcional e configurada normalmente para false.

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

Dentro da função do handler, o recibo pode ser acessado no campo Event.receipt. Quando a chave receipt é configurada em false, ou omitida no manifest, um valor null será retornado em vez disto.

Recursos experimentais

Link para esta seção

A partir do specVersion 0.0.4, os recursos de subgraph devem ser explicitamente declarados na seção features no maior nível do arquivo de manifest com o seu nome em camelCase, como listado abaixo:

RecursoNome
Erros não-fataisnonFatalErrors
Busca fulltextfullTextSearch
Enxertosgrafting

Por exemplo, se um subgraph usa os recursos de Busca Fulltext e Erros não-fatais, o campo features no manifest deve ser:

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

Note que usar uma ferramenta sem declará-la causará um erro de validação durante o lançamento de um subgraph, mas não ocorrerá nenhum erro se um recurso for declarado sem ser usado.

Séries de Tempo e Agregações

Link para esta seção

Séries de tempo e agregações permitem que o seu subgraph registre estatísticas como médias diárias de preço, total de transferências por hora, etc.

Este recurso introduz dois novos tipos de entidade de subgraph. Entidades de série de tempo registram pontos de dados com marcações de tempo. Entidades de agregação realizam cálculos pré-declarados nos pontos de dados de Séries de Tempo numa base por hora ou diária, e depois armazenam os resultados para acesso fácil via GraphQL.

Exemplo de Schema

Link para esta seção
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")
}

Definição de Série de Tempo e Agregações

Link para esta seção

Entidades de série de tempo são definidas com @entity(timeseries: true) no schema.graphql. Cada entidade deste tipo deve ter uma ID única do tipo int8, uma marcação do tipo Timestamp, e inclui dados que serão usados para cálculos por entidades de agregação. Estas entidades de Série de Tempo podem ser salvas em handlers de ação regular, e agem como os "dados brutos" das entidades de Agregação.

Entidades de agregação são definidas com @aggregation no schema.graphql. Toda entidade deste tipo define a fonte de qual resgatará dados (que deve ser uma entidade de Série de Tempo), determina os intervalos (por ex., hora, dia) e especifica a função de agregação que usará (por ex., soma, contagem, min, max, primeiro, último). Entidades de agregação são calculadas automaticamente na base da fonte especificada ao final do intervalo requerido.

Intervalos de Agregação Disponíveis

Link para esta seção
  • hour: configura o período de série de tempo para cada hora, em ponto.
  • day: configura o período de série de tempo para cada dia, a começar e terminar à meia-noite.

Funções de Agregação Disponíveis

Link para esta seção
  • sum: Total de todos os valores.
  • count: Número de valores.
  • min: Valor mínimo.
  • max: Valor máximo.
  • first: Primeiro valor no período.
  • last: Último valor no período.

Exemplo de Query de Agregações

Link para esta seção
{
stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {
id
timestamp
sum
}
}

Nota:

Para utilizar Séries de Tempo e Agregações, um subgraph deve ter uma versão de especificação maior que 1.1.0. Note que este recurso pode passar por mudanças significativas que podem afetar a retrocompatibilidade.

Leia mais sobre Séries de Tempo e Agregações.

Erros não-fatais

Link para esta seção

Erros de indexação em subgraphs já sincronizados, por si próprios, farão que o subgraph falhe e pare de sincronizar. Os subgraphs podem, de outra forma, ser configurados a continuar a sincronizar na presença de erros, ao ignorar as mudanças feitas pelo handler que provocaram o erro. Isto dá tempo aos autores de subgraphs para corrigir seus subgraphs enquanto queries continuam a ser servidos perante o bloco mais recente, porém os resultados podem ser inconsistentes devido ao bug que causou o erro. Note que alguns erros ainda são sempre fatais. Para ser não-fatais, os erros devem ser confirmados como determinísticos.

Nota: A rede do The Graph ainda não apoia erros não fatais, e os programadores não devem lançar subgraphs à rede pelo Studio por esta funcionalidade.

Permitir erros não fatais exige a configuração da seguinte feature flag no manifest do subgraph:

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

A consulta também deve concordar em consultar dados que tenham possíveis inconsistências através do argumento subgraphError. Também vale consultar o _meta para verificar se o subgraph pulou erros, como no seguinte exemplo:

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

Caso o subgraph encontre um erro, esse query retornará tanto os dados quanto o erro no graphql com a mensagem "indexing_error". Veja neste exemplo de resposta:

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

Como Enxertar em Subgraphs Existentes

Link para esta seção

Nota: não é recomendado usar enxertos na primeira atualização para a Graph Network. Saiba mais aqui.

Quando um subgraph é lançado pela primeira vez, ele começa a indexar eventos no bloco gênese da chain correspondente (ou no startBlock definido com cada fonte de dados). Às vezes, há vantagem em reutilizar os dados de um subgraph existente e começar a indexar em um bloco muito mais distante. Este modo de indexar é chamado de Enxerto. O enxerto, por exemplo, serve para passar rapidamente por erros simples nos mapeamentos durante a programação, ou consertar temporariamente um subgraph existente após ele ter falhado.

Um subgraph é enxertado em um subgraph base quando um manifest de subgraph no subgraph.yaml contém um bloco graft no maior nível:

description: ...
graft:
base: Qm... # ID do subgraph base
block: 7345624 # Número do bloco

Quando é lançado um subgraph cujo manifest contém um bloco graft, o Graph Node copiará os dados do subgraph base até, e inclusive, o block dado, e então continuará a indexar o novo subgraph a partir daquele bloco. O subgraph base deve existir na instância-alvo do Graph Node e ter indexado até, no mínimo, o bloco dado. Devido a esta restrição, o enxerto só deve ser usado durante a programação, ou em uma emergência para acelerar a produção de um subgraph não-enxertado equivalente.

Como o enxerto copia em vez de indexar dados base, dirigir o subgraph para o bloco desejado desta maneira é mais rápido que indexar do começo, mesmo que a cópia inicial dos dados ainda possa levar várias horas para subgraphs muito grandes. Enquanto o subgraph enxertado é inicializado, o Graph Node gravará informações sobre os tipos de entidade que já foram copiados.

O subgraph enxertado pode usar um schema GraphQL que não é idêntico ao schema do subgraph base, mas é apenas compatível com ele. Ele deve ser um schema válido no seu próprio mérito, mas pode desviar do schema do subgraph base nas seguintes maneiras:

  • Ele adiciona ou remove tipos de entidade
  • Ele retira atributos de tipos de identidade
  • Ele adiciona atributos anuláveis a tipos de entidade
  • Ele transforma atributos não anuláveis em atributos anuláveis
  • Ele adiciona valores a enums
  • Ele adiciona ou remove interfaces
  • Ele muda para quais tipos de entidades uma interface é implementada

Gerenciamento de Recursos: O grafting deve ser declarado sob features no manifest do subgraph.

IPFS/Arweave File Data Sources

Link para esta seção

Fontes de dados de arquivos são uma nova funcionalidade de subgraph para acessar dados off-chain de forma robusta e extensível. As fontes de dados de arquivos apoiam o retiro de arquivos do IPFS e do Arweave.

Isto também abre as portas para indexar dados off-chain de forma determinística, além de potencialmente introduzir dados arbitrários com fonte em 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.

Isto é parecido com os modelos de fontes de dados existentes, usados para dinamicamente criar fontes de dados baseadas em chains.

Isto substitui a API ipfs.cat existente

Guia de atualização

Link para esta seção

Atualizar graph-ts e graph-cli

Link para esta seção

O recurso de fontes de dados de arquivos exige o graph-ts >=0.29.0 e o graph-cli >=0.33.1

Adicionar um novo tipo de entidade que será atualizado quando os arquivos forem encontrados

Link para esta seção

Fontes de dados de arquivos não podem acessar ou atualizar entidades baseadas em chain, mas devem atualizar entidades específicas a arquivos.

Isto pode implicar separar campos de entidades existentes em entidades separadas, ligadas juntas.

Entidade combinada original:

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!
}

Entidade nova, separada:

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!
}

Se o relacionamento for perfeitamente proporcional entre a entidade parente e a entidade de fontes de dados de arquivos resultante, é mais simples ligar a entidade parente a uma entidade de arquivos resultante, com a CID IPFS como o assunto de busca. Se tiver dificuldades em modelar suas novas entidades baseadas em arquivos, pergunte no Discord!

É necessário usar filtros ninhados para filtrar entidades parentes na base destas entidades ninhadas.

Adicione um novo modelo de fonte de dados com kind: file/ipfs ou kind: file/arweave

Link para esta seção

Esta é a fonte de dados que será gerada quando um arquivo de interesse for identificado.

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

Atualmente é obrigatório usar abis, mas não é possível chamar contratos de dentro de fontes de dados de arquivos

A fonte de dados de arquivos deve mencionar especificamente todos os tipos de entidades com os quais ela interagirá sob entities. Veja as limitações para mais detalhes.

Criar um novo handler para processar arquivos

Link para esta seção

Este handler deve aceitar um parâmetro Bytes, que consistirá dos conteúdos do arquivo; quando encontrado, este poderá ser acessado. Isto costuma ser um arquivo JSON, que pode ser processado com helpers graph-ts (documentação).

A CID do arquivo como um string legível pode ser acessada através do dataSource a seguir:

const cid = dataSource.stringParam()

Exemplo de handler:

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()
}
}

Gerar fontes de dados de arquivos quando for obrigatório

Link para esta seção

Agora pode criar fontes de dados de arquivos durante a execução de handlers baseados em chain:

  • Importe o modelo do templates autogerado
  • chame o TemplateName.create(cid: string) de dentro de um mapeamento, onde o cid é um identificador de conteúdo válido para IPFS ou Arweave

Para o IPFS, o Graph Node apoia identificadores de conteúdo v0 e v1 e identificadores com diretórios (por ex. bafyreighykzv2we26wfrbzkcdw37sbrby4upq7ae3aqobbq7i4er3tnxci/metadata.json).

Para o Arweave, desde a versão 0.33.0, o Graph Node pode resgatar arquivos armazenados no Arweave com base na sua ID de transação de um gateway do Arweave (exemplo de arquivo). O Arweave apoia transações enviadas via Irys (antigo Bundlr), e o Graph Node também pode resgatar arquivos com base em manifests do Irys.

Exemplo:

import { TokenMetadata as TokenMetadataTemplate } from '../generated/templates'
const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
//Este exemplo de código é para um subgraph do Crypto Coven. O hash ipfs acima é um diretório com metadados de tokens para todos os NFTs do Crypto Coven.
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
//Isto cria um caminho aos metadados para um único NFT do Crypto Coven. Ele concatena o diretório com "/" + nome do arquivo + ".json"
token.ipfsURI = tokenIpfsHash
TokenMetadataTemplate.create(tokenIpfsHash)
}
token.updatedAtTimestamp = event.block.timestamp
token.owner = event.params.to.toHexString()
token.save()
}

Isto criará uma fonte de dados de arquivos, que avaliará o endpoint de IPFS ou Arweave configurado do Graph Node, e tentará novamente caso não achá-lo. Com o arquivo localizado, o handler da fonte de dados de arquivos será executado.

Este exemplo usa a CID como a consulta entre a entidade parente Token e a entidade TokenMetadata resultante.

Anteriormente, este era o ponto em que qual um programador de subgraph teria chamado o ipfs.cat(CID) para resgatar o arquivo

Parabéns, você está a usar fontes de dados de arquivos!

Como lançar os seus Subgraphs

Link para esta seção

Agora, pode construir (build) e lançar (deploy) seu subgraph a qualquer Graph Node >=v0.30.0-rc.0.

Handlers e entidades de fontes de dados de arquivos são isolados de outras entidades de subgraph, o que garante que sejam determinísticos quando executados e que não haja contaminação de fontes de dados baseadas em chain. Especificamente:

  • Entidades criadas por Fontes de Dados de Arquivos são imutáveis, e não podem ser atualizadas
  • Handlers de Fontes de Dados de Arquivos não podem acessar entidades de outras fontes de dados de arquivos
  • Entidades associadas com Fontes de Dados de Arquivos não podem ser acessadas por handlers baseados em chain

Enquanto esta limitação pode não ser problemática para a maioria dos casos de uso, ela pode deixar alguns mais complexos. Se houver qualquer problema neste processo, por favor dê um alô via Discord!

Além disto, não é possível criar fontes de dados de uma fonte de dado de arquivos, seja uma on-chain ou outra fonte de dados de arquivos. Esta restrição poderá ser retirada no futuro.

Boas práticas

Link para esta seção

Caso ligue metadados de NFTs a tokens correspondentes, use o hash IPFS destes para referenciar uma entidade de Metadados da entidade do Token. Salve a entidade de Metadados a usar o hash IPFS como ID.

É possível usar o contexto DataSource ao criar Fontes de Dados de Arquivos para passar informações extras, que estarão disponíveis ao handler de Fontes de Dados de Arquivos.

Caso tenha entidades a ser atualizadas várias vezes, crie entidades únicas baseadas em arquivos utilizando o hash IPFS & o ID da entidade, e as referencie com um campo derivado na entidade baseada na chain.

Estamos a melhorar a recomendação acima, para que os queries retornem apenas a versão "mais recente"

Problemas conhecidos

Link para esta seção

Atualmente, fontes de dados de arquivos requerem ABIs, apesar destas não serem usadas (problema no GitHub). A solução é adicionar qualquer ABI.

Handlers para Fontes de Dados de Arquvios não podem estar em arquivos que importam ligações de contrato eth_call, o que causa falhas com "unknown import: ethereum::ethereum.call has not been defined" (problema no GitHub). A solução é criar handlers de fontes de dados de arquivos num arquivo dedicado.

Migração de subgraph do Crypto Coven

Fontes de Dados de Arquivos GIP

Editar página

Anterior
Redes Apoiadas
Próximo
API AssemblyScript
Editar página