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.
A definição de subgraph consiste de alguns arquivos:
-
subgraph.yaml
: um arquivo YAML que contém o manifest do subgraph -
schema.graphql
: um schema GraphQL que define quais dados são armazenados para o seu subgraph, e como consultá-los em query via GraphQL -
AssemblyScript Mappings
: código em que traduz dos dados de eventos às entidades definidas no seu schema (por ex.,mapping.ts
neste tutorial)
Para utilizar o seu subgraph na rede descentralizada do The Graph, será necessário . É recomendado ao seu subgraph com, no mínimo, .
Antes de se aprofundar nos conteúdos do arquivo manifest, instale o , que será necessário para construir e adicionar um subgraph.
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.
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.
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 é 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.
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 novodataSource
deve usareventHandlers
&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 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 .
Para o subgraph de exemplo, o 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
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 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. -
indexerHints.prune
: Define a retenção de dados históricos de blocos para um subgraph. Veja na secção . -
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 comoBool
,String
,Int
,Int8
,BigDecimal
,Bytes
,List
, eBigInt
. Cada variável deve especificar o seutype
edata
. 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 campofilter
comkind: 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
.
Os gatilhos para uma fonte de dados dentro de um bloco são ordenados com o seguinte processo:
- Gatilhos de evento e chamada são, primeiro, ordenados por índice de transação no bloco.
- 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.
- 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 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.
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-License-Identifier: MITpragma solidity ^0.8.0;contract Token {// Declaração de evento com parâmetros indexados para endereçosevent Transfer(address indexed from, address indexed to, uint256 value);// Função para simular a transferência de tokensfunction transfer(address to, uint256 value) public {// Emitting the Transfer event with from, to, and valueemit Transfer(msg.sender, to, value);}}
Neste exemplo:
- O evento
Transfer
é usado para gravar transações de tokens entre endereços. - The
from
andto
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.
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: handleSomeEventtopic1: ['0xValue1', '0xValue2']topic2: ['0xAddress1', '0xAddress2']topic3: ['0xValue3']
Neste cenário:
topic1
corresponde ao primeiro argumento indexado do evento,topic2
ao segundo etopic3
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.
- 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.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleDirectedTransfertopic1: ['0xAddressA'] # Sender Addresstopic2: ['0xAddressB'] # Receiver Address
Nesta configuração:
topic1
é configurado para filtrar eventosTransfer
onde0xAddressA
é o remetente.topic2
é configurado para filtrar eventosTransfer
onde0xAddressB
é o remetente.- O subgraph só indexará transações que ocorrerem diretamente do
0xAddressA
ao0xAddressB
.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleTransferToOrFromtopic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Addresstopic2: ['0xAddressB', '0xAddressC'] # Receiver Address
Nesta configuração:
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()
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 ou compilar com solc.
- Também pode achar a ABI no , 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 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 .
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: BytesdisplayName: StringimageUrl: Stringaccepted: 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: BytesdisplayName: StringimageUrl: String}type GravatarDeclined @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: String}
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).
Nós apoiamos os seguintes escalares na nossa API do GraphQL:
Tipo | Descrição |
---|---|
Bytes | Arranjo de bytes, representado como string hexadecimal. Usado frequentemente por hashes e endereços no Ethereum. |
String | Escalar para valores string . Caracteres nulos são removidos automaticamente. |
Boolean | Escalar para valores boolean . |
Int | A especificação do GraphQL define o Int como um inteiro assinado de 32 bits. |
Int8 | Um 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. |
BigInt | Nú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 . |
BigDecimal | BigDecimal 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 {OriginalOwnerSecondOwnerThirdOwner}
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 .
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.
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}
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 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!}
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 UserOrganizationorganization {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.
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 entidadeid: Bytes!address: Bytes!}
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: 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!]!}
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 para uma descrição da API de busca fulltext e mais exemplos de uso.
query {bandSearch(text: "breaks & electro & detroit") {idnamedescriptionwallet}}
: A partir do specVersion
0.0.4
em diante, o fullTextSearch
deve ser declarado sob a seção features
no manifest do subgraph.
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ódigo | Dicionário |
---|---|
simple | Geral |
da | Dinamarquês |
nl | Holandês |
en | Inglês |
fi | Finlandês |
fr | Francês |
de | Alemão |
hu | Húngaro |
it | Italiano |
no | Norueguês |
pt | Português |
ro | Romeno |
ru | Russo |
es | Espanhol |
sv | Sueco |
tr | Turco |
Algoritmos apoiados para a organização de resultados:
Algoritmo | Descrição |
---|---|
rank | Organiza os resultados pela qualidade da correspondência (0-1) da busca fulltext. |
proximityRank | Parecido com o rank, mas também inclui a proximidade das correspondências. |
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 chamado , que pode ser compilado ao WASM (). 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.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()}
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()
.
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, usarBytes
como aid
é beneficial. Determinar aid
pareceria com
let dayID = event.block.timestamp.toI32() / 86400let id = Bytes.fromI32(dayID)
- Converta endereços constantes em
Bytes
.
const id = Bytes.fromHexString('0xdead...beef')
Há uma , 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
.
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.
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:
# Yarnyarn codegen# NPMnpm 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.
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.
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 . 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/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
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/contractname: Factory# ... outros campos de fonte para o contrato principal ...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
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 trocaExchange.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 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.
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/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
Nota: O bloco da criação do contrato pode ser buscado rapidamente no Etherscan:
- Procure pelo contrato ao inserir o seu endereço na barra de busca.
- Clique no hash da transação da criação na seção
Contract Creator
. - 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:
"never"
: Nenhum pruning de dados históricos; retém o histórico completo."auto"
: Retém o histórico mínimo necessário determinado pelo Indexador e otimiza o desempenho das queries.- 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:
- , 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 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 , 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 :
{indexingStatuses(subgraphs: ["Qm..."]) {subgraphsyncedhealthchains {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 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.
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/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'] # Filtro de tópico opcional que só filtra eventos com o tópico especificado.
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.
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/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
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.
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.hashlet transaction = new Transaction(id)transaction.displayName = call.inputs._displayNametransaction.imageUrl = call.inputs._imageUrltransaction.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
.
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.
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/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
Requer specVersion
>= 0.0.8
Nota: Filtros de polling só estão disponíveis nas dataSources kind: ethereum
.
blockHandlers:- handler: handleBlockfilter:kind: pollingevery: 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: handleOncefilter: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()}
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.hashlet entity = new Block(id)entity.save()}
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.
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: handleNewGravatarreceipt: 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.
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:
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.4description: Gravatar for Ethereumfeatures:- fullTextSearch- nonFatalErrorsdataSources: ...
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 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.
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")}
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.
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.
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.
{stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {idtimestampsum}}
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.
sobre Séries de Tempo e Agregações.
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.4description: Gravatar for Ethereumfeatures:- 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"}]
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 baseblock: 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
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 , usados para dinamicamente criar fontes de dados baseadas em chains.
Isto substitui a API ipfs.cat
existente
O recurso de fontes de dados de arquivos exige o graph-ts >=0.29.0 e o graph-cli >=0.33.1
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: BigIntowner: User!}
Entidade nova, separada:
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!}
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!
Esta é a fonte de dados que será gerada quando um arquivo de interesse for identificado.
templates:- name: TokenMetadatakind: file/ipfsmapping:apiVersion: 0.0.7language: wasm/assemblyscriptfile: ./src/mapping.tshandler: handleMetadataentities:- TokenMetadataabis:- name: Tokenfile: ./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 para mais detalhes.
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
().
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()}}
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 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 de um gateway do Arweave (). O Arweave apoia transações enviadas via Irys (antigo Bundlr), e o Graph Node também pode resgatar arquivos com base em .
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.tokenIdtoken.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 = tokenIpfsHashTokenMetadataTemplate.create(tokenIpfsHash)}token.updatedAtTimestamp = event.block.timestamptoken.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!
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.
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 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"
Atualmente, fontes de dados de arquivos requerem ABIs, apesar destas não serem usadas (). 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" (). A solução é criar handlers de fontes de dados de arquivos num arquivo dedicado.