Creación de un subgrafo
Reading time: 48 min
Un subgrafo extrae datos de una blockchain, los procesa y los almacena para que puedan consultarse fácilmente mediante GraphQL.
La definición del subgrafo consta de unos cuantos archivos:
-
subgraph.yaml
: un archivo YAML que contiene el manifiesto del subgrafo -
schema.graphql
: un esquema GraphQL que define qué datos se almacenan para su subgrafo, y cómo consultarlos a través de GraphQL -
AssemblyScript Mappings
: codigo que traduce de los datos del evento a las entidades definidas en su esquema (por ejemplomapping.ts
en este tutorial)
In order to use your subgraph on The Graph's decentralized network, you will need to . It is recommended that you to your subgraph with at least .
Before you go into detail about the contents of the manifest file, you need to install the which you will need to build and deploy a subgraph.
The Graph CLI está escrito en JavaScript, y tendrás que instalar yarn
o npm
para utilizarlo; se asume que tienes yarn en lo que sigue.
Una vez que tengas yarn
, instala The Graph CLI ejecutando
Instalar con yarn:
yarn global add @graphprotocol/graph-cli
Instalar con npm:
npm install -g @graphprotocol/graph-cli
Once installed, the graph init
command can be used to set up a new subgraph project, either from an existing contract or from an example subgraph. This command can be used to create a subgraph in Subgraph Studio by passing in graph init --product subgraph-studio
. If you already have a smart contract deployed to your preferred network, bootstrapping a new subgraph from that contract can be a good way to get started.
El siguiente comando crea un subgrafo que indexa todos los eventos de un contrato existente. Intenta obtener la ABI del contrato desde Etherscan y vuelve a solicitar una ruta de archivo local. Si falta alguno de los argumentos opcionales, te lleva a través de un formulario interactivo.
graph init \--product subgraph-studio--from-contract <CONTRACT_ADDRESS> \[--network <ETHEREUM_NETWORK>] \[--abi <FILE>] \<SUBGRAPH_SLUG> [<DIRECTORY>]
El <SUBGRAPH_SLUG>
es el ID de tu subgrafo en Subgraph Studio, y se puede encontrar en la página de detalles de tu subgrafo.
El segundo modo que admite graph init
es la creación de un nuevo proyecto a partir de un subgrafo de ejemplo. El siguiente comando lo hace:
graph init --studio <SUBGRAPH_SLUG>
The is based on the Gravity contract by Dani Grant that manages user avatars and emits NewGravatar
or UpdateGravatar
events whenever avatars are created or updated. The subgraph handles these events by writing Gravatar
entities to the Graph Node store and ensuring these are updated according to the events. The following sections will go over the files that make up the subgraph manifest for this example.
Desde v0.31.0
, graph-cli
permite añadir nuevos dataSources a un subgrafo existente mediante el comando graph add
.
graph add <address> [<subgraph-manifest default: "./subgraph.yaml">]Options:--abi <path> Path to the contract ABI (default: download from Etherscan)--contract-name Name of the contract (default: Contract)--merge-entities Whether to merge entities with the same name (default: false)--network-file <path> Networks config file path (default: "./networks.json")
El comando add
obtendrá el ABI de Etherscan (a menos que se especifique una ruta ABI con la opción --abi
), y creará un nuevo dataSource
de la misma manera que el comando graph init
crea un dataSource
--from-contract
, actualizando el esquema y los mappings de manera acorde.
La opción --merge-entities
identifica cómo el desarrollador desea manejar los conflictos de nombres de entity
y event
:
- Si es
true
: el nuevodataSource
debe utilizar loseventHandlers
&entities
existentes. - Si es
false
: se creará una nueva entidad & event handler con${dataSourceName}{EventName}
.
La address
del contrato se escribirá en el archivo networks.json
para la red correspondiente.
Nota: Cuando se utiliza el cli interactivo, después de ejecutar correctamente graph init
, se te pedirá que añadas un nuevo dataSource
.
El manifiesto del subgrafo subgraph.yaml
define los contratos inteligentes que indexa tu subgrafo, a qué eventos de estos contratos prestar atención, y cómo mapear los datos de los eventos a las entidades que Graph Node almacena y permite consultar. La especificación completa de los manifiestos de subgrafos puede encontrarse en .
Para este subgrafo de ejemplo, subgraph.yaml
es:
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
Las entradas importantes a actualizar para el manifiesto son:
-
specVersion
: a semver version that identifies the supported manifest structure and functionality for the subgraph. The latest version is1.2.0
. See section to see more details on features & releases. -
description
: a human-readable description of what the subgraph is. This description is displayed in Graph Explorer when the subgraph is deployed to Subgraph Studio. -
repository
: the URL of the repository where the subgraph manifest can be found. This is also displayed in Graph Explorer. -
indexerHints.prune
: Defines the retention of historical block data for a subgraph. See in section. -
dataSources.source
: la dirección del contrato inteligente del que procede el subgrafo, y la ABI del contrato inteligente a utilizar. La dirección es opcional; omitirla permite indexar eventos coincidentes de todos los contratos. -
dataSources.source.startBlock
: el número opcional del bloque desde el que la fuente de datos comienza a indexar. En la mayoría de los casos, sugerimos utilizar el bloque en el que se creó el contrato. -
dataSources.source.endBlock
: The optional number of the block that the data source stops indexing at, including that block. Minimum spec version required:0.0.9
. -
dataSources.context
: key-value pairs that can be used within subgraph mappings. Supports various data types likeBool
,String
,Int
,Int8
,BigDecimal
,Bytes
,List
, andBigInt
. Each variable needs to specify itstype
anddata
. These context variables are then accessible in the mapping files, offering more configurable options for subgraph development. -
dataSources.mapping.entities
: las entidades que la fuente de datos escribe en el almacén. El esquema de cada entidad se define en el archivo schema.graphql. -
dataSources.mapping.abis
: uno o más archivos ABI con nombre para el contrato fuente, así como cualquier otro contrato inteligente con el que interactúes desde los mappings. -
dataSources.mapping.eventHandlers
: enumera los eventos de contratos inteligentes a los que reacciona este subgrafo y los handlers en el mapping -./src/mapping.ts en el ejemplo- que transforman estos eventos en entidades en el almacén. -
dataSources.mapping.callHandlers
: enumera las funciones de contrato inteligente a las que reacciona este subgrafo y los handlers en el mapping que transforman las entradas y salidas a las llamadas de función en entidades en el almacén. -
dataSources.mapping.blockHandlers
: enumera los bloques a los que reacciona este subgrafo y los handlers en el mapping que se ejecutan cuando se agrega un bloque a la cadena. Sin un filtro, el handler de bloque se ejecutará cada bloque. Se puede proporcionar un filtro de llamada opcional añadiendo un campo confilter
field withkind: call
al handler. Este sólo ejecutará el handler si el bloque contiene al menos una llamada al contrat de la fuente de datos.
Un único subgrafo puede indexar datos de múltiples contratos inteligentes. Añade una entrada por cada contrato del que haya que indexar datos a la array dataSources
.
Las triggers de una fuente de datos dentro de un bloque se ordenan mediante el siguiente proceso:
- Las triggers de eventos y calls se ordenan primero por el índice de la transacción dentro del bloque.
- Los triggers de eventos y calls dentro de la misma transacción se ordenan siguiendo una convención: primero los triggers de eventos y luego los de calls, respetando cada tipo el orden en que se definen en el manifiesto.
- Las triggers de bloques se ejecutan después de las triggers de eventos y calls, en el orden en que están definidos en el manifiesto.
Estas normas de orden están sujetas a cambios.
Note: When new are created, the handlers defined for dynamic data sources will only start processing after all existing data source handlers are processed, and will repeat in the same sequence whenever triggered.
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 {// Event declaration with indexed parameters for addressesevent Transfer(address indexed from, address indexed to, uint256 value);// Function to simulate transferring tokensfunction transfer(address to, uint256 value) public {// Emitting the Transfer event with from, to, and valueemit Transfer(msg.sender, to, value);}}
In this example:
- The
Transfer
event is used to log transactions of tokens between addresses. - The
from
andto
parameters are indexed, allowing event listeners to filter and monitor transfers involving specific addresses. - The
transfer
function is a simple representation of a token transfer action, emitting the Transfer event whenever it is called.
Topic filters are defined directly within the event handler configuration in the subgraph manifest. Here is how they are configured:
eventHandlers:- event: SomeEvent(indexed uint256, indexed address, indexed uint256)handler: handleSomeEventtopic1: ['0xValue1', '0xValue2']topic2: ['0xAddress1', '0xAddress2']topic3: ['0xValue3']
In this setup:
topic1
corresponds to the first indexed argument of the event,topic2
to the second, andtopic3
to the third.- Each topic can have one or more values, and an event is only processed if it matches one of the values in each specified topic.
- Within a Single Topic: The logic functions as an OR condition. The event will be processed if it matches any one of the listed values in a given topic.
- Between Different Topics: The logic functions as an AND condition. An event must satisfy all specified conditions across different topics to trigger the associated handler.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleDirectedTransfertopic1: ['0xAddressA'] # Sender Addresstopic2: ['0xAddressB'] # Receiver Address
In this configuration:
topic1
is configured to filterTransfer
events where0xAddressA
is the sender.topic2
is configured to filterTransfer
events where0xAddressB
is the receiver.- The subgraph will only index transactions that occur directly from
0xAddressA
to0xAddressB
.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleTransferToOrFromtopic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Addresstopic2: ['0xAddressB', '0xAddressC'] # Receiver Address
In this configuration:
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()
Los archivos ABI deben coincidir con tu(s) contrato(s). Hay varias formas de obtener archivos ABI:
- Si estás construyendo tu propio proyecto, es probable que tengas acceso a tus ABIs más actuales.
- Si estás construyendo un subgrafo para un proyecto público, puedes descargar ese proyecto en tu computadora y obtener la ABI utilizando o usando solc para compilar.
- También puedes encontrar la ABI en , pero no siempre es fiable, ya que la ABI que se sube allí puede estar desactualizada. Asegúrate de que tienes la ABI correcta, de lo contrario la ejecución de tu subgrafo fallará.
El esquema para tu subgrafo está en el archivo schema.graphql
. Los esquemas GraphQL se definen utilizando el lenguaje de definición de interfaces GraphQL. Si nunca has escrito un esquema GraphQL, te recomendamos que le eches un vistazo a este manual sobre el sistema de tipos GraphQL. La documentación de referencia para los esquemas GraphQL se puede encontrar en la sección .
Antes de definir las entidades, es importante dar un paso atrás y pensar en cómo están estructurados y vinculados los datos. Todas las consultas se harán contra el modelo de datos definido en el esquema del subgrafo y las entidades indexadas por el subgrafo. Debido a esto, es bueno definir el esquema del subgrafo de una manera que coincida con las necesidades de tu dapp. Puede ser útil imaginar las entidades como "objetos que contienen datos", más que como eventos o funciones.
Con The Graph, simplemente defines los tipos de entidad en schema.graphql
y Graph Node generará campos de nivel superior para consultar instancias individuales y colecciones de ese tipo de entidad. Cada tipo que deba ser una entidad debe ser anotado con una directiva @entity
. Por defecto, las entidades son mutables, lo que significa que los mappings pueden cargar entidades existentes, modificarlas y almacenar una nueva versión de esa entidad. La mutabilidad tiene un precio, y para los tipos de entidades que se sabe que nunca se modificarán, por ejemplo, porque simplemente contienen datos extraídos textualmente de la cadena, se recomienda marcarlas como inmutables con @entity(immutable: true)
. Los mappings pueden realizar cambios en las entidades inmutables siempre que esos cambios se produzcan en el mismo bloque en el que se creó la entidad. Las entidades inmutables son mucho más rápidas de escribir y de consultar, por lo que deberían utilizarse siempre que sea posible.
La entidad Gravatar
que aparece a continuación está estructurada en torno a un objeto Gravatar y es un buen ejemplo de cómo podría definirse una entidad.
type Gravatar @entity(immutable: true) {id: Bytes!owner: BytesdisplayName: StringimageUrl: Stringaccepted: Boolean}
El ejemplo las entidades GravatarAccepted
y GravatarDeclined
que aparecen a continuación se basan en eventos. No se recomienda asignar eventos o calls a funciones a entidades 1:1.
type GravatarAccepted @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: String}type GravatarDeclined @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: String}
Los campos de la entidad pueden definirse como obligatorios u opcionales. Los campos obligatorios se indican con el !
en el esquema. Si un campo obligatorio no está establecido en el mapping, recibirá este error al consultar el campo:
Null value resolved for non-null field 'name'
Cada entidad debe tener un campo id
, que debe ser de tipo Bytes!
o String!
. Por lo general, se recomienda utilizar Bytes!
, a menos que el id
contenga texto legible por humanos, ya que las entidades con ids de Bytes!
serán más rápidas de escribir y consultar que las que tienen un id
de String!
El campo id
sirve como clave primaria y debe ser único entre todas las entidades del mismo tipo. Por razones históricas, el tipo ID!
también se acepta y es un sinónimo de String!
.
Para algunos tipos de entidad, el id
se construye a partir de los id de otras dos entidades; esto es posible utilizando concat
, por ejemplo, let id = left.id.concat(right.id)
para formar el id a partir de los id de left
y right
. Del mismo modo, para construir un id a partir del id de una entidad existente y un contador, se puede utilizar count
y let id = left.id.concatI32(count)
. Se garantiza que la concatenación produzca id's únicos siempre que la longitud de left
sea la misma para todas esas entidades, por ejemplo, porque left.id
es una Address
.
Admitimos los siguientes escalares en nuestra API GraphQL:
Tipo | Descripción |
---|---|
Bytes | Byte array, representado como un string hexadecimal. Comúnmente utilizado para los hashes y direcciones de Ethereum. |
String | Escalar para valores string . Los caracteres nulos no son compatibles y se eliminan automáticamente. |
Boolean | Escalar para valores boolean . |
Int | The GraphQL spec defines Int to be a signed 32-bit integer. |
Int8 | An 8-byte signed integer, also known as a 64-bit signed integer, can store values in the range from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Prefer using this to represent i64 from ethereum. |
BigInt | Números enteros grandes. Se utiliza para los tipos uint32 , int64 , uint64 , ..., uint256 de Ethereum. Nota: Todo por debajo de uint32 , como int32 , uint24 o int8 se representa como i32 . |
BigDecimal | BigDecimal Decimales de alta precisión representados como un signo y un exponente. El rango de exponentes va de -6143 a +6144. Redondeado a 34 dígitos significativos. |
Timestamp | It is an i64 value in microseconds. Commonly used for timestamp fields for timeseries and aggregations. |
También puedes crear enums dentro de un esquema. Los enums tienen la siguiente sintaxis:
enum TokenStatus {OriginalOwnerSecondOwnerThirdOwner}
Una vez definido el enum en el esquema, puedes utilizar la representación del string del valor del enum para establecer un campo enum en una entidad. Por ejemplo, puedes establecer el tokenStatus
a SecondOwner
definiendo primero tu entidad y posteriormente estableciendo el campo con entity.tokenStatus = "SecondOwner
. El siguiente ejemplo muestra el aspecto de la entidad Token con un campo enum:
Puedes encontrar más detalles sobre la escritura de enums en la .
Una entidad puede tener una relación con otra u otras entidades de su esquema. Estas relaciones pueden ser recorridas en sus consultas. Las relaciones en The Graph son unidireccionales. Es posible simular relaciones bidireccionales definiendo una relación unidireccional en cada "extremo" de la relación.
Las relaciones se definen en las entidades como cualquier otro campo, salvo que el tipo especificado es el de otra entidad.
Define un tipo de entidad Transaction
con una relación opcional de uno a uno con un tipo de entidad TransactionReceipt
:
type Transaction @entity(immutable: true) {id: Bytes!transactionReceipt: TransactionReceipt}type TransactionReceipt @entity(immutable: true) {id: Bytes!transaction: Transaction}
Define un tipo de entidad TokenBalance
con una relación requerida de uno a varios con un tipo de entidad Token:
type Token @entity(immutable: true) {id: Bytes!}type TokenBalance @entity {id: Bytes!amount: Int!token: Token!}
Se pueden definir búsquedas inversas en una entidad a través del campo @derivedFrom
. Esto crea un campo virtual en la entidad que puede ser consultado pero que no puede ser establecido manualmente a través de la API de mapping. Más bien, se deriva de la relación definida en la otra entidad. Para este tipo de relaciones, rara vez tiene sentido almacenar ambos lados de la relación, y tanto la indexación como el rendimiento de la consulta serán mejores cuando sólo se almacene un lado y el otro se derive.
En el caso de las relaciones one-to-many, la relación debe almacenarse siempre en el lado "one", y el lado "many" debe derivarse siempre. Almacenar la relación de esta manera, en lugar de almacenar una array de entidades en el lado "many", resultará en un rendimiento dramáticamente mejor tanto para la indexación como para la consulta del subgrafo. En general, debe evitarse, en la medida de lo posible, el almacenamiento de arrays de entidades.
Podemos hacer que los balances de un token sean accesibles desde el token derivando un campo tokenBalances
:
type Token @entity(immutable: true) {id: Bytes!tokenBalances: [TokenBalance!]! @derivedFrom(field: "token")}type TokenBalance @entity {id: Bytes!amount: Int!token: Token!}
Para las relaciones de many-to-many, como los usuarios pueden pertenecer a cualquier número de organizaciones, la forma más directa, pero generalmente no la más eficaz, de modelar la relación es en un array en cada una de las dos entidades implicadas. Si la relación es simétrica, sólo es necesario almacenar un lado de la relación y el otro puede derivarse.
Define una búsqueda inversa desde un tipo de entidad User
a un tipo de entidad Organization
. En el ejemplo siguiente, esto se consigue buscando el atributo members
desde la entidad Organization
. En las consultas, el campo organizations
en User
se resolverá buscando todas las entidades de Organization
que incluyan el ID del usuario.
type Organization @entity {id: Bytes!name: String!members: [User!]!}type User @entity {id: Bytes!name: String!organizations: [Organization!]! @derivedFrom(field: "members")}
Una forma más eficaz de almacenar esta relación es a través de una tabla de mapping que tiene una entrada para cada par User
/ Organization
con un esquema 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!}
Este enfoque requiere que las consultas desciendan a un nivel adicional para recuperar, por ejemplo, las organizaciones para los usuarios:
query usersWithOrganizations {users {organizations {# this is a UserOrganization entityorganization {name}}}}
Esta forma más elaborada de almacenar las relaciones many-to-many se traducirá en menos datos almacenados para el subgrafo y, por tanto, en un subgrafo que suele ser mucho más rápido de indexar y consultar.
As per GraphQL spec, comments can be added above schema entity attributes using the hash symble #
. This is illustrated in the example below:
type MyFirstEntity @entity {# unique identifier and primary key of the entityid: Bytes!address: Bytes!}
Las consultas de búsqueda de texto completo filtran y clasifican las entidades basándose en una entrada de búsqueda de texto. Las consultas de texto completo pueden devolver coincidencias de palabras similares procesando el texto de la consulta en stems antes de compararlo con los datos del texto indexado.
La definición de una consulta de texto completo incluye el nombre de la consulta, el diccionario lingüístico utilizado para procesar los campos de texto, el algoritmo de clasificación utilizado para ordenar los resultados y los campos incluidos en la búsqueda. Cada consulta de texto completo puede abarcar varios campos, pero todos los campos incluidos deben ser de un solo tipo de entidad.
Para agregar una consulta de texto completo, incluye un tipo _Schema_
con una directiva de texto completo en el esquema 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!]!}
El ejemplo campo bandSearch
se puede utilizar en las consultas para filtrar las entidades Band
con base en los documentos de texto en los campos name
, description
, y bio
. Ve a para ver una descripción de la API de búsqueda de texto completo y más ejemplos de uso.
query {bandSearch(text: "breaks & electro & detroit") {idnamedescriptionwallet}}
: Desde specVersion
0.0.4
y en adelante, fullTextSearch
se debe declarar bajo la sección features
en el manifiesto del subgrafo.
La elección de un idioma diferente tendrá un efecto definitivo, aunque a veces sutil, en la API de búsqueda de texto completo. Los campos cubiertos por un campo de consulta de texto completo se examinan en el contexto de la lengua elegida, por lo que los lexemas producidos por las consultas de análisis y búsqueda varían de un idioma a otro. Por ejemplo: al utilizar el diccionario turco compatible, "token" se convierte en "toke", mientras que el diccionario inglés lo convierte en "token".
Diccionarios de idiomas admitidos:
Código | Diccionario |
---|---|
simple | General |
da | Danés |
nl | Holandés |
en | Inglés |
fi | Finlandés |
fr | Francés |
de | Alemán |
hu | Húngaro |
it | Italiano |
no | Noruego |
pt | Portugués |
ro | Rumano |
ru | Ruso |
es | Español |
sv | Sueco |
tr | Turco |
Algoritmos admitidos para ordenar los resultados:
Algoritmos | Descripción |
---|---|
rango | Usa la calidad de coincidencia (0-1) de la consulta de texto completo para ordenar los resultados. |
rango de proximidad | Similar al rango, pero también incluye la proximidad de los matches. |
Los mapeos toman datos de una fuente particular y los transforman en entidades que están definidas dentro de su esquema. Los mapeos se escriben en un subconjunto de llamado que puede compilarse a WASM (). AssemblyScript es más estricto que TypeScript normal, pero proporciona una sintaxis familiar.
Para cada handler de eventos que se define en subgraph.yaml
bajo mapping.eventHandlers
, crea una función exportada del mismo nombre. Cada handler debe aceptar un único parámetro llamado event
con un tipo correspondiente al nombre del evento que se está manejando.
En el subgrafo de ejemplo, src/mapping.ts
contiene handlers para los eventos NewGravatar
y 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()}
El primer handler toma un evento NewGravatar
y crea una nueva entidad Gravatar
con new Gravatar(event.params.id.toHex())
, poblando los campos de la entidad usando los parámetros correspondientes del evento. Esta instancia de entidad está representada por la variable gravatar
, con un valor de id de event.params.id.toHex()
.
El segundo handler intenta cargar el Gravatar
existente desde el almacén de The Graph Node. Si aún no existe, se crea bajo demanda. A continuación, la entidad se actualiza para que coincida con los nuevos parámetros del evento, antes de volver a guardarla en el almacén mediante gravatar.save()
.
It is highly recommended to use Bytes
as the type for id
fields, and only use String
for attributes that truly contain human-readable text, like the name of a token. Below are some recommended id
values to consider when creating new entities.
-
transfer.id = event.transaction.hash
-
let id = event.transaction.hash.concatI32(event.logIndex.toI32())
-
For entities that store aggregated data, for e.g, daily trade volumes, the
id
usually contains the day number. Here, using aBytes
as theid
is beneficial. Determining theid
would look like
let dayID = event.block.timestamp.toI32() / 86400let id = Bytes.fromI32(dayID)
- Convert constant addresses to
Bytes
.
const id = Bytes.fromHexString('0xdead...beef')
There is a which contains utilities for interacting with the Graph Node store and conveniences for handling smart contract data and entities. It can be imported into mapping.ts
from @graphprotocol/graph-ts
.
When creating and saving a new entity, if an entity with the same ID already exists, the properties of the new entity are always preferred during the merge process. This means that the existing entity will be updated with the values from the new entity.
If a null value is intentionally set for a field in the new entity with the same ID, the existing entity will be updated with the null value.
If no value is set for a field in the new entity with the same ID, the field will result in null as well.
Para que trabajar con contratos inteligentes, eventos y entidades sea fácil y seguro desde el punto de vista de los tipos, Graph CLI puede generar tipos AssemblyScript a partir del esquema GraphQL del subgrafo y de las ABIs de los contratos incluidas en las fuentes de datos.
Esto se hace con
graph codegen [--output-dir <OUTPUT_DIR>] [<MANIFEST>]
pero en la mayoría de los casos, los subgrafos ya están preconfigurados a través de package.json
para permitirte simplemente ejecutar uno de los siguientes para lograr lo mismo:
# Yarnyarn codegen# NPMnpm run codegen
Esto generará una clase AssemblyScript para cada contrato inteligente en los archivos ABI mencionados en subgraph.yaml
, permitiéndote vincular estos contratos a direcciones específicas en los mappings y llamar a métodos de contrato de sólo lectura contra el bloque que se está procesando. También generará una clase para cada evento del contrato para facilitar el acceso a los parámetros del evento, así como el bloque y la transacción que originó el evento. Todos estos tipos se escriben en <OUTPUT_DIR>/<DATA_SOURCE_NAME>/<ABI_NAME>.ts
. En el subgrafo de ejemplo, esto sería generated/Gravity/Gravity.ts
, permitiendo mappings con los que importar estos tipos.
import {// The contract class:Gravity,// The events classes:NewGravatar,UpdatedGravatar,} from '../generated/Gravity/Gravity'
Además, se genera una clase para cada tipo de entidad en el esquema GraphQL del subgrafo. Estas clases proporcionan una carga de entidades segura, acceso de lectura y escritura a los campos de la entidad, así como un método save()
para escribir entidades en el almacén. Todas las clases de entidades se escriben en <OUTPUT_DIR>/schema.ts
, permitiendo mappings con los que importarlos
import { Gravatar } from '../generated/schema'
Nota: La generación de código debe realizarse de nuevo después de cada cambio en el esquema GraphQL o en las ABIs incluidas en el manifiesto. También debe realizarse al menos una vez antes de construir o deployar el subgrafo.
Code generation does not check your mapping code in src/mapping.ts
. If you want to check that before trying to deploy your subgraph to Graph Explorer, you can run yarn build
and fix any syntax errors that the TypeScript compiler might find.
Un patrón común en los contratos inteligentes de Ethereum es el uso de contratos de registro o fábrica, donde un contrato crea, gestiona o hace referencia a un número arbitrario de otros contratos que tienen cada uno su propio estado y eventos.
Las direcciones de estos subcontratos pueden o no ser conocidas de antemano y muchos de estos contratos pueden ser creados y/o añadidos con el tiempo. Es por eso que, en tales casos, es imposible definir una sola fuente de datos o un número fijo de fuentes de datos y se necesita un enfoque más dinámico: plantillas de fuente de datos.
En primer lugar, define una fuente de datos regular para el contrato principal. El siguiente fragmento muestra un ejemplo simplificado de fuente de datos para el contrato generador del exchange . Nota el handler NewExchange(address,address)
del evento. Esto se emite cuando el contrato de fábrica crea un nuevo contrato de exchange en la cadena.
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
A continuación, añade plantillas de origen de datos al manifiesto. Son idénticas a las fuentes de datos normales, salvo que carecen de una dirección de contrato predefinida en source
. Normalmente, defines un modelo para cada tipo de subcontrato gestionado o referenciado por el contrato principal.
dataSources:- kind: ethereum/contractname: Factory# ... other source fields for the main contract ...templates:- name: Exchangekind: ethereum/contractnetwork: mainnetsource:abi: Exchangemapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/exchange.tsentities:- Exchangeabis:- name: Exchangefile: ./abis/exchange.jsoneventHandlers:- event: TokenPurchase(address,uint256,uint256)handler: handleTokenPurchase- event: EthPurchase(address,uint256,uint256)handler: handleEthPurchase- event: AddLiquidity(address,uint256,uint256)handler: handleAddLiquidity- event: RemoveLiquidity(address,uint256,uint256)handler: handleRemoveLiquidity
En el último paso, actualiza el mapping del contrato principal para crear una instancia de fuente de datos dinámica a partir de una de las plantillas. En este ejemplo, cambiarías el mapping del contrato principal para importar la plantilla Exchange
y llamaría al método Exchange.create(address)
en él para empezar a indexar el nuevo contrato de exchange.
import { Exchange } from '../generated/templates'export function handleNewExchange(event: NewExchange): void {// Start indexing the exchange; `event.params.exchange` is the// address of the new exchange contractExchange.create(event.params.exchange)}
Nota: Un nuevo origen de datos sólo procesará las llamadas y los eventos del bloque en el que fue creado y todos los bloques siguientes, pero no procesará los datos históricos, es decir, los datos que están contenidos en bloques anteriores.
Si los bloques anteriores contienen datos relevantes para la nueva fuente de datos, lo mejor es indexar esos datos leyendo el estado actual del contrato y creando entidades que representen ese estado en el momento de crear la nueva fuente de datos.
Los contextos de fuentes de datos permiten pasar una configuración extra al instanciar una plantilla. En nuestro ejemplo, digamos que los exchanges se asocian a un par de trading concreto, que se incluye en el evento NewExchange
. Esa información se puede pasar a la fuente de datos instanciada, así:
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 un mapping de la plantilla Exchange
, se puede acceder al contexto:
import { dataSource } from '@graphprotocol/graph-ts'let context = dataSource.context()let tradingPair = context.getString('tradingPair')
Hay setters y getters como setString
and getString
para todos los tipos de valores.
El startBlock
es un ajuste opcional que permite definir a partir de qué bloque de la cadena comenzará a indexar la fuente de datos. Establecer el bloque inicial permite a la fuente de datos omitir potencialmente millones de bloques que son irrelevantes. Normalmente, un desarrollador de subgrafos establecerá startBlock
al bloque en el que se creó el contrato inteligente de la fuente de datos.
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: El bloque de creación del contrato se puede buscar rápidamente en Etherscan:
- Busca el contrato introduciendo su dirección en la barra de búsqueda.
- Haz clic en el hash de la transacción de creación en la sección
Contract Creator
. - Carga la página de detalles de la transacción, donde encontrarás el bloque inicial de ese contrato.
The indexerHints
setting in a subgraph's manifest provides directives for indexers on processing and managing a subgraph. It influences operational decisions across data handling, indexing strategies, and optimizations. Presently, it features the prune
option for managing historical data retention or pruning.
This feature is available from specVersion: 1.0.0
indexerHints.prune
: Defines the retention of historical block data for a subgraph. Options include:
"never"
: No pruning of historical data; retains the entire history."auto"
: Retains the minimum necessary history as set by the indexer, optimizing query performance.- A specific number: Sets a custom limit on the number of historical blocks to retain.
indexerHints:prune: auto
The term "history" in this context of subgraphs is about storing data that reflects the old states of mutable entities.
History as of a given block is required for:
- , which enable querying the past states of these entities at specific blocks throughout the subgraph's history
- Using the subgraph as a in another subgraph, at that block
- Rewinding the subgraph back to that block
If historical data as of the block has been pruned, the above capabilities will not be available.
Using "auto"
is generally recommended as it maximizes query performance and is sufficient for most users who do not require access to extensive historical data.
For subgraphs leveraging , it's advisable to either set a specific number of blocks for historical data retention or use prune: never
to keep all historical entity states. Below are examples of how to configure both options in your subgraph's settings:
To retain a specific amount of historical data:
indexerHints:prune: 1000 # Replace 1000 with the desired number of blocks to retain
To preserve the complete history of entity states:
indexerHints:prune: never
You can check the earliest block (with historical state) for a given subgraph by querying the :
{indexingStatuses(subgraphs: ["Qm..."]) {subgraphsyncedhealthchains {earliestBlock {number}latestBlock {number}chainHeadBlock { number }}}}
Note that the earliestBlock
is the earliest block with historical data, which will be more recent than the startBlock
specified in the manifest, if the subgraph has been pruned.
Event handlers in a subgraph react to specific events emitted by smart contracts on the blockchain and trigger handlers defined in the subgraph's manifest. This enables subgraphs to process and store event data according to defined logic.
An event handler is declared within a data source in the subgraph's YAML configuration. It specifies which events to listen for and the corresponding function to execute when those events are detected.
dataSources:- kind: ethereum/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsoneventHandlers:- event: Approval(address,address,uint256)handler: handleApproval- event: Transfer(address,address,uint256)handler: handleTransfertopic1: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0xc8dA6BF26964aF9D7eEd9e03E53415D37aA96325'] # Optional topic filter which filters only events with the specified topic.
Aunque los eventos proporcionan una forma eficaz de recoger los cambios relevantes en el estado de un contrato, muchos contratos evitan generar registros para optimizar los costos de gas. En estos casos, un subgrafo puede suscribirse a las calls realizadas al contrato de la fuente de datos. Esto se consigue definiendo los call handlers que hacen referencia a la firma de la función y al handler de mapping que procesará las calls a esta función. Para procesar estas calls, el handler de mapping recibirá un ethereum.Call
como argumento con las entradas y salidas tipificadas de la call. Las calls realizadas en cualquier profundidad de la cadena de calls de una transacción activarán el mapping, permitiendo capturar la actividad con el contrato de origen de datos a través de los contratos proxy.
Los call handlers solo se activarán en uno de estos dos casos: cuando la función especificada sea llamada por una cuenta distinta del propio contrato o cuando esté marcada como externa en Solidity y sea llamada como parte de otra función en el mismo contrato.
Nota: Los call handlers dependen actualmente de la API de rastreo Parity. Ciertas redes, como BNB chain y Arbitrum, no soportan esta API. Si un subgrafo que indexa una de estas redes contiene uno o más call handlers, no comenzará la sincronización. En su lugar, los developers de subgrafos deberían utilizar handlers de eventos. Estos son mucho más eficaces que los handlers de llamadas, y están soportados en todas las redes evm.
Para definir un call handler en su manifiesto simplemente añade una array callHandlers
bajo la fuente de datos a la que deseas suscribirte.
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
La function
es la firma de la función normalizada por la que se filtran las llamadas. La propiedad handler
es el nombre de la función de tu mapping que quieres ejecutar cuando se llame a la función de destino en el contrato de origen de datos.
Cada call handler toma un solo parámetro que tiene un tipo correspondiente al nombre de la función llamada. En el subgrafo de ejemplo anterior, el mapping contiene un handler para cuando se llama a la función createGravatar
y recibe un parámetro 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()}
La función handleCreateGravatar
toma una nueva CreateGravatarCall
que es una subclase de ethereum.Call
, proporcionada por @graphprotocol/graph-ts
, que incluye las entradas y salidas tipificadas del call. El tipo CreateGravatarCall
se genera por ti cuando ejecutas graph codegen
.
Además de suscribirse a eventos del contracto o calls de funciones, un subgrafo puede querer actualizar sus datos a medida que se añaden nuevos bloques a la cadena. Para ello, un subgrafo puede ejecutar una función después de cada bloque o después de los bloques que coincidan con un filtro predefinido.
filter:kind: call
El handler definido será llamado una vez por cada bloque que contenga una llamada al contrato (fuente de datos) bajo el cual está definido el handler.
Nota: El filtro call
depende actualmente de la API de rastreo Parity. Ciertas redes, como la cadena BNB y Arbitrum, no soportan esta API. Si un subgrafo que indexa una de estas redes contiene uno o más handlers de bloque con un filtro de call
, no comenzará la sincronización.
La ausencia de un filtro para un handler de bloque asegurará que el handler sea llamado en cada bloque. Una fuente de datos solo puede contener un handler de bloque 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
Requires specVersion
>= 0.0.8
Note: Polling filters are only available on dataSources of kind: ethereum
.
blockHandlers:- handler: handleBlockfilter:kind: pollingevery: 10
The defined handler will be called once for every n
blocks, where n
is the value provided in the every
field. This configuration allows the subgraph to perform specific operations at regular block intervals.
Requires specVersion
>= 0.0.8
Note: Once filters are only available on dataSources of kind: ethereum
.
blockHandlers:- handler: handleOncefilter:kind: once
The defined handler with the once filter will be called only once before all other handlers run. This configuration allows the subgraph to use the handler as an initialization handler, performing specific tasks at the start of indexing.
export function handleOnce(block: ethereum.Block): void {let data = new InitialData(Bytes.fromUTF8('initial'))data.data = 'Setup data here'data.save()}
La función de mapeo recibirá un ethereum.Block
como único argumento. Al igual que las funciones de mapping para eventos, esta función puede acceder a entidades de subgrafos existentes en el almacén, llamar a contratos inteligentes y crear o actualizar entidades.
import { ethereum } from '@graphprotocol/graph-ts'export function handleBlock(block: ethereum.Block): void {let id = block.hashlet entity = new Block(id)entity.save()}
Si necesitas procesar eventos anónimos en Solidity, puedes hacerlo proporcionando el tema 0 del evento, como en el ejemplo:
eventHandlers:- event: LogNote(bytes4,address,bytes32,bytes32,uint256,bytes)topic0: '0x644843f351d3fba4abcd60109eaff9f54bac8fb8ccf0bab941009c21df21cf31'handler: handleGive
Un evento solo se activará cuando la firma y el tema 0 coincidan. Por defecto, topic0
es igual al hash de la firma del evento.
A partir de specVersion
0.0.5
y apiVersion
0.0.7
, los handlers de eventos pueden tener acceso al recibo de la transacción que los emitió.
Para ello, los handlers de eventos deben declararse en el manifiesto del subgrafo con la nueva clave receipt: true
, que es opcional y por defecto es false.
eventHandlers:- event: NewGravatar(uint256,address,string,string)handler: handleNewGravatarreceipt: true
Dentro de la función handler, se puede acceder al recibo en el campo Event.receipt
. Cuando la clave receipt
se establece en false
o se omite en el manifiesto, se devolverá un valor null
en su lugar.
A partir de specVersion
0.0.4
, los features de los subgrafos deben declararse explícitamente en la sección de features
del nivel superior del archivo de manifiesto, utilizando su nombre en camelCase
, como se indica en la tabla siguiente:
Por ejemplo, si un subgrafo utiliza las características Full-Text Search y Non-fatal Errors, el campo features
del manifiesto debería ser:
specVersion: 0.0.4description: Gravatar for Ethereumfeatures:- fullTextSearch- nonFatalErrorsdataSources: ...
Ten en cuenta que el uso de una característica sin declararla incurrirá en un error de validación durante el deploy del subgrafo, pero no se producirá ningún error si se declara una característica pero no se utiliza.
Timeseries and aggregations enable your subgraph to track statistics like daily average price, hourly total transfers, etc.
This feature introduces two new types of subgraph entity. Timeseries entities record data points with timestamps. Aggregation entities perform pre-declared calculations on the Timeseries data points on an hourly or daily basis, then store the results for easy access 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")}
Timeseries entities are defined with @entity(timeseries: true)
in schema.graphql. Every timeseries entity must have a unique ID of the int8 type, a timestamp of the Timestamp type, and include data that will be used for calculation by aggregation entities. These Timeseries entities can be saved in regular trigger handlers, and act as the “raw data” for the Aggregation entities.
Aggregation entities are defined with @aggregation
in schema.graphql. Every aggregation entity defines the source from which it will gather data (which must be a Timeseries entity), sets the intervals (e.g., hour, day), and specifies the aggregation function it will use (e.g., sum, count, min, max, first, last). Aggregation entities are automatically calculated on the basis of the specified source at the end of the required interval.
hour
: sets the timeseries period every hour, on the hour.day
: sets the timeseries period every day, starting and ending at 00:00.
sum
: Total of all values.count
: Number of values.min
: Minimum value.max
: Maximum value.first
: First value in the period.last
: Last value in the period.
{stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {idtimestampsum}}
Note:
To use Timeseries and Aggregations, a subgraph must have a spec version ≥1.1.0. Note that this feature might undergo significant changes that could affect backward compatibility.
about Timeseries and Aggregations.
Los errores de indexación en subgrafos ya sincronizados provocarán, por defecto, que el subgrafo falle y deje de sincronizarse. Los subgrafos pueden ser configurados de manera alternativa para continuar la sincronización en presencia de errores, ignorando los cambios realizados por el handler que provocó el error. Esto da a los autores de los subgrafos tiempo para corregir sus subgrafos mientras las consultas continúan siendo servidas contra el último bloque, aunque los resultados serán posiblemente inconsistentes debido al bug que provocó el error. Nótese que algunos errores siguen siendo siempre fatales, para que el error no sea fatal debe saberse que es deterministico.
Nota: The Graph Network todavía no admite errores no fatales, y los developers no deben deployar subgrafos que utilicen esa funcionalidad en la red a través de Studio.
Para activar los errores no fatales es necesario establecer el siguiente indicador en el manifiesto del subgrafo:
specVersion: 0.0.4description: Gravatar for Ethereumfeatures:- nonFatalErrors...
La consulta también debe optar por consultar los datos con posibles incoherencias a través del argumento subgraphError
. También se recomienda consultar _meta
para comprobar si el subgrafo ha saltado por encima de los errores, como en el ejemplo:
foos(first: 100, subgraphError: allow) {id}_meta {hasIndexingErrors}
Si el subgrafo encuentra un error esa consulta devolverá tanto los datos como un error de graphql con el mensaje "indexing_error"
, como en este ejemplo de respuesta:
"data": {"foos": [{"id": "0xdead"}],"_meta": {"hasIndexingErrors": true}},"errors": [{"message": "indexing_error"}]
Note: it is not recommended to use grafting when initially upgrading to The Graph Network. Learn more .
Cuando un subgrafo es deployado por primera vez, comienza a indexar eventos en el bloque génesis de la cadena correspondiente (o en el startBlock
definido con cada fuente de datos) En algunas circunstancias, es beneficioso reutilizar los datos de un subgrafo existente y comenzar a indexar en un bloque mucho más tardío. Este modo de indexación se denomina Grafting. El Grafting es, por ejemplo, útil durante el desarrollo para superar rápidamente errores simples en los mappings, o para hacer funcionar temporalmente un subgrafo existente después de que haya fallado.
Un subgrafo se graftea en un subgrafo base cuando el manifiesto de subgrafo subgraph.yaml
contiene un bloque graft
en el nivel superior:
description: ...graft:base: Qm... # Subgraph ID of base subgraphblock: 7345624 # Block number
Cuando se realiza el deploy de un subgrafo cuyo manifiesto contiene un bloque graft
, Graph Node copiará los datos del subgrafo base
hasta el block
dado, inclusive, y luego continuará indexando el nuevo subgrafo a partir de ese bloque. El subgrafo base debe existir en la instancia de Graph Node de destino y debe haber indexado hasta al menos el bloque dado. Debido a esta restricción, el grafting sólo debería utilizarse durante el desarrollo o durante una emergencia para acelerar la producción de un subgrafo equivalente no grafted (injertado).
Debido a que el grafting copia en lugar de indexar los datos base, es mucho más rápido llevar el subgrafo al bloque deseado que indexar desde cero, aunque la copia inicial de los datos aún puede llevar varias horas para subgrafos muy grandes. Mientras se inicializa el subgrafo grafted, Graph Node registrará información sobre los tipos de entidad que ya han sido copiados.
El subgrafo grafteado puede utilizar un esquema GraphQL que no es idéntico al del subgrafo base, sino simplemente compatible con él. Tiene que ser un esquema de subgrafo válido por sí mismo, pero puede diferir del esquema del subgrafo base de las siguientes maneras:
- Agrega o elimina tipos de entidades
- Elimina los atributos de los tipos de entidad
- Agrega atributos anulables a los tipos de entidad
- Convierte los atributos no anulables en atributos anulables
- Añade valores a los enums
- Agrega o elimina interfaces
- Cambia para qué tipos de entidades se implementa una interfaz
File data sources are a new subgraph functionality for accessing off-chain data during indexing in a robust, extendable way. File data sources support fetching files from IPFS and from Arweave.
Esto también establece las bases para la indexación determinista de datos off-chain, así como la posible introducción de datos arbitrarios procedentes de 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.
This is similar to the , which are used to dynamically create new chain-based data sources.
Esto sustituye a la API ipfs.cat
existente
Las fuentes de datos de archivos requieren graph-ts >=0.29.0 y graph-cli >=0.33.1
Las fuentes de datos de archivos no pueden acceder a entidades basadas en cadenas ni actualizarlas, pero deben actualizar entidades específicas de archivos.
Esto puede significar dividir campos de entidades existentes en entidades separadas, vinculadas entre sí.
Entidad 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!}
Nueva, entidad dividida:
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!}
Si la relación es 1:1 entre la entidad padre y la entidad fuente de datos de archivo resultante, el patrón más sencillo es vincular la entidad padre a una entidad de archivo resultante utilizando el CID IPFS como búsqueda. Pónte en contacto con nosotros en Discord si tienes dificultades para modelar tus nuevas entidades basadas en archivos!
Esta es la fuente de datos que se generará cuando se identifique un archivo de interés.
templates:- name: TokenMetadatakind: file/ipfsmapping:apiVersion: 0.0.7language: wasm/assemblyscriptfile: ./src/mapping.tshandler: handleMetadataentities:- TokenMetadataabis:- name: Tokenfile: ./abis/Token.json
Actualmente se requieren abis
, aunque no es posible llamar a los contratos desde las fuentes de datos de los archivos
The file data source must specifically mention all the entity types which it will interact with under entities
. See for more details.
This handler should accept one Bytes
parameter, which will be the contents of the file, when it is found, which can then be processed. This will often be a JSON file, which can be processed with graph-ts
helpers ().
Se puede acceder al CID del archivo como un string legible a través del dataSource
de la siguiente manera:
const cid = dataSource.stringParam()
Ejemplo 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()}}
Ahora puedes crear fuentes de datos de archivos durante la ejecución de handlers basados en cadenas:
- Importar la plantilla desde los
templates
autogenerados - call
TemplateName.create(cid: string)
from within a mapping, where the cid is a valid content identifier for IPFS or Arweave
For IPFS, Graph Node supports , and content identifers with directories (e.g. bafyreighykzv2we26wfrbzkcdw37sbrby4upq7ae3aqobbq7i4er3tnxci/metadata.json
).
For Arweave, as of version 0.33.0 Graph Node can fetch files stored on Arweave based on their from an Arweave gateway (). Arweave supports transactions uploaded via Irys (previously Bundlr), and Graph Node can also fetch files based on .
Ejemplo:
import { TokenMetadata as TokenMetadataTemplate } from '../generated/templates'const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'//This example code is for a Crypto coven subgraph. The above ipfs hash is a directory with token metadata for all crypto coven NFTs.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//This creates a path to the metadata for a single Crypto coven NFT. It concats the directory with "/" + filename + ".json"token.ipfsURI = tokenIpfsHashTokenMetadataTemplate.create(tokenIpfsHash)}token.updatedAtTimestamp = event.block.timestamptoken.owner = event.params.to.toHexString()token.save()}
This will create a new file data source, which will poll Graph Node's configured IPFS or Arweave endpoint, retrying if it is not found. When the file is found, the file data source handler will be executed.
En este ejemplo se utiliza el CID como búsqueda entre la entidad Token
principal y la entidad TokenMetadata
resultante.
Anteriormente, este es el punto en el que un developer de subgrafos habría llamado a ipfs.cat(CID)
para obtener el archivo
¡Felicitaciones, estás utilizando fuentes de datos de archivos!
Ya puedes build
y deploy
tu subgrafo en cualquier Graph Node >=v0.30.0-rc.0.
Los handlers y entidades de fuentes de datos de archivos están aislados de otras entidades del subgrafo, asegurando que son deterministas cuando se ejecutan, y asegurando que no se contaminan las fuentes de datos basadas en cadenas. En concreto:
- Las entidades creadas por File Data Sources son inmutables y no pueden actualizarse
- Los handlers de File Data Source no pueden acceder a entidades de otras fuentes de datos de archivos
- Los handlers basados en cadenas no pueden acceder a las entidades asociadas a File Data Sources
Aunque esta restricción no debería ser problemática para la mayoría de los casos de uso, puede introducir complejidad para algunos. Si tienes problemas para modelar tus datos basados en archivos en un subgrafo, ponte en contacto con nosotros a través de Discord!
Además, no es posible crear fuentes de datos a partir de una File Data Source, ya sea una fuente de datos on-chain u otra File Data Source. Es posible que esta restricción se elimine en el futuro.
Si estás vinculando metadatos NFT a los tokens correspondientes, utiliza el hash IPFS de los metadatos para hacer referencia a una entidad Metadata desde la entidad Token. Guarda la entidad de metadatos utilizando el hash IPFS como ID.
You can use when creating File Data Sources to pass extra information which will be available to the File Data Source handler.
Si tienes entidades que se actualizan varias veces, crea entidades únicas basadas en archivos utilizando el hash IPFS & el ID de la entidad, y haz referencia a ellas utilizando un campo derivado en la entidad basada en cadena.
Estamos trabajando para mejorar la recomendación anterior, de modo que las consultas sólo devuelvan la versión "más reciente"
File Data Sources requieren actualmente ABIs, aunque no se utilicen ABIs (). La solución es añadir cualquier ABI.
Handlers for File Data Sources cannot be in files which import eth_call
contract bindings, failing with "unknown import: ethereum::ethereum.call
has not been defined" (). Workaround is to create file data source handlers in a dedicated file.