Desarrollando > Creación de un subgrafo

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.

Definir un Subgrafo

La definición del subgrafo consta de unos cuantos archivos:

In order to use your subgraph on The Graph's decentralized network, you will need to create an API key. It is recommended that you add signal to your subgraph with at least 3,000 GRT.

Before you go into detail about the contents of the manifest file, you need to install the Graph CLI which you will need to build and deploy a subgraph.

Instalar The Graph CLI

Enlace a esta sección

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.

Desde un Contrato Existente

Enlace a esta sección

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.

Desde un Subgrafo de Ejemplo

Enlace a esta sección

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 example subgraph 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.

Añadir nuevas fuentes de datos a un subgrafo existente

Enlace a esta sección

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 nuevo dataSource debe utilizar los eventHandlers& 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 de Subgrafo

Enlace a esta sección

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

Para este subgrafo de ejemplo, subgraph.yaml es:

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

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 is 1.2.0. See specVersion releases 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.

  • features: una lista de todos los nombres de las feature usadas.

  • indexerHints.prune: Defines the retention of historical block data for a subgraph. See prune in indexerHints 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 like Bool, String, Int, Int8, BigDecimal, Bytes, List, and BigInt. Each variable needs to specify its type and data. 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 con filter field with kind: 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.

Order of Triggering Handlers

Enlace a esta sección

Las triggers de una fuente de datos dentro de un bloque se ordenan mediante el siguiente proceso:

  1. Las triggers de eventos y calls se ordenan primero por el índice de la transacción dentro del bloque.
  2. 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.
  3. 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 dynamic data source 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.

Indexed Argument Filters / Topic Filters

Enlace a esta sección

Requires: SpecVersion >= 1.2.0

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

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

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

How Topic Filters Work

Enlace a esta sección

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

  • The event's first indexed argument corresponds to topic1, the second to topic2, and so on, up to topic3, since the Ethereum Virtual Machine (EVM) allows up to three indexed arguments per event.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Token {
// Event declaration with indexed parameters for addresses
event Transfer(address indexed from, address indexed to, uint256 value);
// Function to simulate transferring tokens
function transfer(address to, uint256 value) public {
// Emitting the Transfer event with from, to, and value
emit Transfer(msg.sender, to, value);
}
}

In this example:

  • The Transfer event is used to log transactions of tokens between addresses.
  • The from and to 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.

Configuration in Subgraphs

Enlace a esta sección

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: handleSomeEvent
topic1: ['0xValue1', '0xValue2']
topic2: ['0xAddress1', '0xAddress2']
topic3: ['0xValue3']

In this setup:

  • topic1 corresponds to the first indexed argument of the event, topic2 to the second, and topic3 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.

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

Enlace a esta sección
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleDirectedTransfer
topic1: ['0xAddressA'] # Sender Address
topic2: ['0xAddressB'] # Receiver Address

In this configuration:

  • topic1 is configured to filter Transfer events where 0xAddressA is the sender.
  • topic2 is configured to filter Transfer events where 0xAddressB is the receiver.
  • The subgraph will only index transactions that occur directly from 0xAddressA to 0xAddressB.

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

Enlace a esta sección
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransferToOrFrom
topic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Address
topic2: ['0xAddressB', '0xAddressC'] # Receiver Address

In this configuration:

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

Declared eth_call

Enlace a esta sección

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

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

This feature does the following:

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

Scenario without Declarative eth_calls

Enlace a esta sección

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

Traditionally, these calls might be made sequentially:

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

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

Scenario with Declarative eth_calls

Enlace a esta sección

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

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

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

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

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

Example Configuration in Subgraph Manifest

Enlace a esta sección

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

Subgraph.yaml using event.address:

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

Details for the example above:

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

Subgraph.yaml using event.params

calls:
- ERC20DecimalsToken0: ERC20[event.params.token0].decimals()

SpecVersion Releases

Enlace a esta sección
VersionNotas del lanzamiento
1.2.0Added support for Indexed Argument Filtering & declared eth_call
1.1.0Supports Timeseries & Aggregations. Added support for type Int8 for id.
1.0.0Supports indexerHints feature to prune subgraphs
0.0.9Supports endBlock feature
0.0.8Added support for polling Block Handlers and Initialisation Handlers.
0.0.7Added support for File Data Sources.
0.0.6Supports fast Proof of Indexing calculation variant.
0.0.5Added support for event handlers having access to transaction receipts.
0.0.4Added support for managing subgraph features.

Obtención de ABIs

Enlace a esta sección

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 truffle compile o usando solc para compilar.
  • También puedes encontrar la ABI en Etherscan, 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 GraphQL

Enlace a esta sección

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 GraphQL API.

Definir Entidades

Enlace a esta 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.

Un buen ejemplo

Enlace a esta sección

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: Bytes
displayName: String
imageUrl: String
accepted: Boolean
}

Un mal ejemplo

Enlace a esta sección

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: Bytes
displayName: String
imageUrl: String
}
type GravatarDeclined @entity {
id: Bytes!
owner: Bytes
displayName: String
imageUrl: String
}

Campos opcionales y obligatorios

Enlace a esta sección

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.

Tipos de Scalars incorporados

Enlace a esta sección

GraphQL admite Scalars

Enlace a esta sección

Admitimos los siguientes escalares en nuestra API GraphQL:

TipoDescripción
BytesByte array, representado como un string hexadecimal. Comúnmente utilizado para los hashes y direcciones de Ethereum.
StringEscalar para valores string. Los caracteres nulos no son compatibles y se eliminan automáticamente.
BooleanEscalar para valores boolean.
IntThe GraphQL spec defines Int to be a signed 32-bit integer.
Int8An 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.
BigIntNú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.
BigDecimalBigDecimal 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.
TimestampIt 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 {
OriginalOwner
SecondOwner
ThirdOwner
}

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 GraphQL documentation.

Relaciones entre Entidades

Enlace a esta sección

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.

Relaciones Uno a Uno

Enlace a esta sección

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
}

Relaciones one-to-many

Enlace a esta sección

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

Búsquedas Inversas

Enlace a esta sección

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

Relaciones de many-to-many

Enlace a esta sección

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 entity
organization {
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.

Agregar comentarios al esquema

Enlace a esta sección

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

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

Definición de campos de búsqueda de texto completo

Enlace a esta sección

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: en
algorithm: rank
include: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }]
)
type Band @entity {
id: Bytes!
name: String!
description: String!
bio: String
wallet: Address
labels: [Label!]!
discography: [Album!]!
members: [Musician!]!
}

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 GraphQL API - Queries 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") {
id
name
description
wallet
}
}

Feature Management: Desde specVersion 0.0.4 y en adelante, fullTextSearch se debe declarar bajo la sección features en el manifiesto del subgrafo.

Idiomas admitidos

Enlace a esta sección

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ódigoDiccionario
simpleGeneral
daDanés
nlHolandés
enInglés
fiFinlandés
frFrancés
deAlemán
huHúngaro
itItaliano
noNoruego
ptPortugués
roRumano
ruRuso
esEspañol
svSueco
trTurco

Algoritmos de Clasificación

Enlace a esta sección

Algoritmos admitidos para ordenar los resultados:

AlgoritmosDescripción
rangoUsa la calidad de coincidencia (0-1) de la consulta de texto completo para ordenar los resultados.
rango de proximidadSimilar al rango, pero también incluye la proximidad de los matches.

Escribir Mappings

Enlace a esta sección

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 TypeScript llamado AssemblyScript que puede compilarse a WASM (WebAssembly). 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.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.save()
}
export function handleUpdatedGravatar(event: UpdatedGravatar): void {
let id = event.params.id
let gravatar = Gravatar.load(id)
if (gravatar == null) {
gravatar = new Gravatar(id)
}
gravatar.owner = event.params.owner
gravatar.displayName = event.params.displayName
gravatar.imageUrl = event.params.imageUrl
gravatar.save()
}

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

ID recomendados para la creación de nuevas entidades

Enlace a esta sección

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 a Bytes as the id is beneficial. Determining the id would look like

let dayID = event.block.timestamp.toI32() / 86400
let id = Bytes.fromI32(dayID)
  • Convert constant addresses to Bytes.

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

There is a Graph Typescript Library 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.

Handling of entities with identical IDs

Enlace a esta sección

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.

Generación de código

Enlace a esta sección

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:

# Yarn
yarn codegen
# NPM
npm 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.

Plantillas para fuentes de datos

Enlace a esta sección

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.

Fuente de Datos para el Contrato Principal

Enlace a esta sección

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

Plantillas de fuentes de datos para contratos creados dinámicamente

Enlace a esta sección

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

Instanciación de una plantilla de fuente de datos

Enlace a esta sección

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 contract
Exchange.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.

Contexto de la fuente de datos

Enlace a esta sección

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.

Bloques iniciales

Enlace a esta sección

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

Nota: El bloque de creación del contrato se puede buscar rápidamente en Etherscan:

  1. Busca el contrato introduciendo su dirección en la barra de búsqueda.
  2. Haz clic en el hash de la transacción de creación en la sección Contract Creator.
  3. Carga la página de detalles de la transacción, donde encontrarás el bloque inicial de ese contrato.

Indexer Hints

Enlace a esta sección

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:

  1. "never": No pruning of historical data; retains the entire history.
  2. "auto": Retains the minimum necessary history as set by the indexer, optimizing query performance.
  3. 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:

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

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

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 time travel queries, 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 Indexing Status API:

{
indexingStatuses(subgraphs: ["Qm..."]) {
subgraph
synced
health
chains {
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

Enlace a esta sección

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.

Defining an Event Handler

Enlace a esta sección

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

Call Handlers

Enlace a esta sección

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.

Definición de un Call Handler

Enlace a esta sección

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

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.

Función mapeo

Enlace a esta sección

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.hash
let transaction = new Transaction(id)
transaction.displayName = call.inputs._displayName
transaction.imageUrl = call.inputs._imageUrl
transaction.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.

Handlers de bloques

Enlace a esta sección

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.

Filtros admitidos

Enlace a esta sección
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/contract
name: Gravity
network: dev
source:
address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'
abi: Gravity
mapping:
kind: ethereum/events
apiVersion: 0.0.6
language: wasm/assemblyscript
entities:
- Gravatar
- Transaction
abis:
- name: Gravity
file: ./abis/Gravity.json
blockHandlers:
- handler: handleBlock
- handler: handleBlockWithCallToContract
filter:
kind: call

Polling Filter

Enlace a esta sección

Requires specVersion >= 0.0.8

Note: Polling filters are only available on dataSources of kind: ethereum.

blockHandlers:
- handler: handleBlock
filter:
kind: polling
every: 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: handleOnce
filter:
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()
}

Función mapeo

Enlace a esta sección

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.hash
let entity = new Block(id)
entity.save()
}

Eventos anónimos

Enlace a esta sección

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.

Recepción de transacciones en Event Handlers

Enlace a esta sección

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: handleNewGravatar
receipt: 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.

Características experimentales

Enlace a esta sección

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:

CaracterísticaNombre
Errores no fatalesnonFatalErrors
Full-text SearchfullTextSearch
Graftinggrafting

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.4
description: Gravatar for Ethereum
features:
- fullTextSearch
- nonFatalErrors
dataSources: ...

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

Enlace a esta sección

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.

Example Schema

Enlace a esta sección
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")
}

Defining Timeseries and Aggregations

Enlace a esta sección

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.

Available Aggregation Intervals

Enlace a esta sección
  • hour: sets the timeseries period every hour, on the hour.
  • day: sets the timeseries period every day, starting and ending at 00:00.

Available Aggregation Functions

Enlace a esta sección
  • 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.

Example Aggregations Query

Enlace a esta sección
{
stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {
id
timestamp
sum
}
}

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.

Read more about Timeseries and Aggregations.

Errores no fatales

Enlace a esta sección

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.4
description: Gravatar for Ethereum
features:
- 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"
}
]

Grafting sobre subgrafos existentes

Enlace a esta sección

Note: it is not recommended to use grafting when initially upgrading to The Graph Network. Learn more here.

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 subgraph
block: 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

La gestión de características: grafting se declara en features en el manifiesto del subgrafo.

IPFS/Arweave File Data Sources

Enlace a esta sección

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 existing data source templates, which are used to dynamically create new chain-based data sources.

Esto sustituye a la API ipfs.cat existente

Upgrade guide

Enlace a esta sección

Actualización de graph-ts y graph-cli

Enlace a esta sección

Las fuentes de datos de archivos requieren graph-ts >=0.29.0 y graph-cli >=0.33.1

Añadir un nuevo tipo de entidad que se actualizará cuando se encuentren archivos

Enlace a esta sección

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: BigInt
owner: User!
}

Nueva, entidad dividida:

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

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!

You can use nested filters to filter parent entities on the basis of these nested entities.

Add a new templated data source with kind: file/ipfs or kind: file/arweave

Enlace a esta sección

Esta es la fuente de datos que se generará cuando se identifique un archivo de interés.

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

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 limitations for more details.

Crear un nuevo handler para procesar archivos

Enlace a esta sección

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

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

Generar fuentes de datos de archivos cuando sea necesario

Enlace a esta sección

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 v0 and v1 content identifiers, 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 transaction ID from an Arweave gateway (example file). Arweave supports transactions uploaded via Irys (previously Bundlr), and Graph Node can also fetch files based on Irys manifests.

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.tokenId
token.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 = tokenIpfsHash
TokenMetadataTemplate.create(tokenIpfsHash)
}
token.updatedAtTimestamp = event.block.timestamp
token.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!

Deploy de tus subgrafos

Enlace a esta sección

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.

Mejores Prácticas

Enlace a esta sección

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 DataSource context 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"

Problemas conocidos

Enlace a esta sección

File Data Sources requieren actualmente ABIs, aunque no se utilicen ABIs (problema). 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" (issue). Workaround is to create file data source handlers in a dedicated file.

Migración de subgrafo Crypto Coven

GIP File Data Sources

Editar página

Anterior
Redes Admitidas
Siguiente
AssemblyScript API
Editar página