Developing > Comment créer un subgraph

Comment créer un subgraph

Reading time: 49 min

Un subgraph récupère des données depuis une blockchain, les manipule puis les enregistre afin que ces données soient aisément accessibles via GraphQL.

Définition d'un subgraph

Un subgraph se constitue des fichiers suivants :

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.

Installation du Graph CLI

Lien vers cette section

La CLI Graph est écrite en JavaScript et vous devrez installer soit yarn ou npm pour l'utiliser ; on suppose que vous avez du fil dans ce qui suit.

Une fois que vous avez yarn, installez la CLI Graph en exécutant

Installation avec yarn :

npm install -g @graphprotocol/graph-cli

Installation avec 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.

D'un contrat existant

Lien vers cette section

La commande suivante crée un subgraph qui indexe tous les événements d'un contrat existant. Il essaie de récupérer l'ABI du contrat via Etherscan et utilise un chemin de fichier local en cas d'échec. Si l'un des arguments facultatifs manque, il vous guide à travers un formulaire interactif.

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

The <SUBGRAPH_SLUG> est l'ID de votre subgraph dans Subgraph Studio, il peut être trouvé sur la page d'information de votre subgraph.

A partir d'un exemple de subgraph

Lien vers cette section

Le second mode graph init prend en charge est la création d'un nouveau projet à partir d'un exemple de subgraph. La commande suivante le fait :

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.

Ajouter de nouvelles sources de données à un subgraph existant

Lien vers cette section

Depuis v0.31.0, le graph-cli prend en charge l'ajout de nouvelles sources de données à un subgraph existant via la commande 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")

La commande add récupérera l'ABI depuis Etherscan (sauf si un chemin ABI est spécifié avec l'option --abi) et créera une nouvelle dataSource de la même manière que la commande graph init crée un dataSource --from-contract, mettant à jour le schéma et les mappages en conséquence.

L'option --merge-entities identifie la façon dont le développeur souhaite gérer les conflits de noms d'entité et d'événement :

  • Si true : le nouveau dataSource doit utiliser les eventHandlers & entités.
  • Si false : une nouvelle entité & le gestionnaire d'événements doit être créé avec ${dataSourceName}{EventName}.

L'adresse du contrat sera écrite dans le networks.json du réseau concerné.

Remarque : Lorsque vous utilisez la Cli interactive, après avoir exécuté avec succès graph init, vous serez invité à ajouter une nouvelle dataSource.

Le manifeste du subgraph

Lien vers cette section

Le manifeste du subgraph subgraph.yaml définit les contrats intelligents que votre subgraph indexe, les événements de ces contrats auxquels prêter attention et comment mapper les données d'événements aux entités que Graph Node stocke et permet d'interroger. La spécification complète des manifestes de subgraphs peut être trouvée ici.

Pour l'exemple de subgraph, subgraph.yaml est :

version spec : 0.0.4
description : Gravatar pour Ethereum
référentiel : https://github.com/graphprotocol/graph-tooling
schéma:
fichier : ./schema.graphql
indexeurConseils :
tailler : automatique
les sources de données:
- genre : ethereum/contrat
nom: Gravité
réseau : réseau principal
source:
adresse : '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'
abi : Gravité
bloc de démarrage : 6175244
bloc de fin : 7175245
contexte:
foo :
tapez : Booléen
données : vrai
bar:
tapez : chaîne
données : 'barre'
cartographie :
genre : ethereum/événements
Version api : 0.0.6
langage : wasm/assemblyscript
entités :
-Gravatar
abis :
- nom : Gravité
fichier : ./abis/Gravity.json
Gestionnaires d'événements :
- événement : NewGravatar(uint256,adresse,chaîne,chaîne)
gestionnaire : handleNewGravatar
- événement : UpdatedGravatar (uint256, adresse, chaîne, chaîne)
gestionnaire : handleUpdatedGravatar
Gestionnaires d'appels :
- fonction : createGravatar(string,string)
gestionnaire : handleCreateGravatar
gestionnaires de blocs :
- gestionnaire : handleBlock
- gestionnaire : handleBlockWithCall
filtre:
genre : appeler
fichier : ./src/mapping.ts

Les entrées importantes à mettre à jour pour le manifeste sont :

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

  • fonctionnalités : une liste de tous les noms de fonctionnalités utilisés.

  • indexerHints.prune : Définit la conservation des données de blocs historiques pour un subgraph. Voir prune dans la section indexerHints.

  • dataSources.source : l'adresse du contrat intelligent, les sources du sous-graphe, et l'ABI du contrat intelligent à utiliser. L'adresse est facultative ; son omission permet d'indexer les événements correspondants de tous les contrats.

  • dataSources.source : l'adresse du contrat intelligent, les sources du subgraph, et l'Abi du contrat intelligent à utiliser. L'adresse est facultative ; son omission permet d'indexer les événements correspondants de tous les contrats.

  • 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 : paires clé-valeur qui peuvent être utilisées dans les mappages de subgraphs. Prend en charge différents types de données comme Bool, String, Int, Int8, BigDecimal, Octets, Liste et BigInt. Chaque variable doit spécifier son type et ses données. Ces variables de contexte sont ensuite accessibles dans les fichiers de mappage, offrant des options plus configurables pour le développement de subgraphs.

  • dataSources.mapping.entities : les entités que la source de données écrit dans le magasin. Le schéma de chaque entité est défini dans le fichier schema.graphql.

  • dataSources.mapping.abis : un ou plusieurs fichiers ABI nommés pour le contrat source ainsi que tout autre contrat intelligent avec lequel vous interagissez à partir des mappages.

  • dataSources.mapping.eventHandlers : répertorie les événements de contrat intelligent auxquels ce subgraph réagit et les gestionnaires du mappage —./src/mapping.ts dans l'exemple qui transforment ces événements en entités dans le magasin.

  • dataSources.mapping.callHandlers : répertorie les fonctions de contrat intelligent auxquelles ce smubgraph réagit et les gestionnaires du mappage qui transforment les entrées et sorties en appels de fonction en entités dans le magasin.

  • dataSources.mapping.blockHandlers : répertorie les blocs auxquels ce subgraph réagit et les gestionnaires du mappage à exécuter lorsqu'un bloc est ajouté à la chaîne. Sans filtre, le gestionnaire de bloc sera exécuté à chaque bloc. Un filtre d'appel facultatif peut être fourni en ajoutant un champ filter avec kind: call au gestionnaire. Cela n'exécutera le gestionnaire que si le bloc contient au moins un appel au contrat de source de données.

Un seul subgraph peut indexer les données de plusieurs contrats intelligents. Ajoutez une entrée pour chaque contrat à partir duquel les données doivent être indexées dans le tableau dataSources.

Ordre de déclenchement des gestionnaires

Lien vers cette section

Les déclencheurs d'une source de données au sein d'un bloc sont classés à l'aide du processus suivant :

  1. Les déclencheurs d'événements et d'appels sont d'abord classés par index de transaction au sein du bloc.
  2. Les déclencheurs d'événements et d'appels au sein d'une même transaction sont classés selon une convention : les déclencheurs d'événements d'abord, puis les déclencheurs d'appel, chaque type respectant l'ordre dans lequel ils sont définis dans le manifeste.
  3. Les déclencheurs de bloc sont exécutés après les déclencheurs d'événement et d'appel, dans l'ordre dans lequel ils sont définis dans le manifeste.

Ces règles de commande sont susceptibles de changer.

Note: Lorsque de nouveaux sources de données dynamiques sont créés, les gestionnaires définis pour les sources de données dynamiques ne commenceront à être traités qu'une fois que tous les gestionnaires de sources de données existants auront été traités, et se répéteront dans la même séquence chaque fois qu'ils seront déclenchés.

Indexed Argument Filters / Topic Filters

Lien vers cette section

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

Lien vers cette section

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

Lien vers cette section

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

Lien vers cette section
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

Lien vers cette section
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

Lien vers cette section

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

Lien vers cette section

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

Lien vers cette section

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

Lien vers cette section

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

Lien vers cette section
VersionNotes de version
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.

Obtenir les ABI

Lien vers cette section

Le(s) fichier(s) ABI doivent correspondre à votre(vos) contrat(s). Il existe plusieurs façons d'obtenir des fichiers ABI :

  • Si vous construisez votre propre projet, vous aurez probablement accès à vos ABI les plus récents.
  • Si vous créez un subgraph pour un projet public, vous pouvez télécharger ce projet sur votre ordinateur et obtenir l'ABI en utilisant la compilation truffle ou en utilisant solc pour compiler.
  • Vous pouvez également trouver l'ABI sur Etherscan, mais ce n'est pas toujours fiable, car l'ABI qui y est téléchargé peut être obsolète. Assurez-vous d'avoir le bon ABI, sinon l'exécution de votre subgraph échouera.

Le Schema GraphQL

Lien vers cette section

Le schéma de votre subgraph se trouve dans le fichier schema.graphql. Les schémas GraphQL sont définis à l'aide du langage de définition d'interface GraphQL. Si vous n'avez jamais écrit de schéma GraphQL, il est recommandé de consulter cette introduction sur le système de types GraphQL. La documentation de référence pour les schémas GraphQL est disponible dans la section API GraphQL.

Définir des entités

Lien vers cette section

Avant de définir des entités, il est important de prendre du recul et de réfléchir à la manière dont vos données sont structurées et liées. Toutes les requêtes seront effectuées sur le modèle de données défini dans le schéma du subgraph et les entités indexées par le subgraph. Pour cette raison, il est bon de définir le schéma du subgraph d'une manière qui correspond aux besoins de votre dapp. Il peut être utile d'imaginer les entités comme des « objets contenant des données », plutôt que comme des événements ou des fonctions.

Avec The Graph, vous définissez simplement les types d'entités dans schema.graphql, et Graph Node générera des champs de niveau supérieur pour interroger des instances uniques et des collections de ce type d'entité. Chaque type qui doit être une entité doit être annoté avec une directive @entity. Par défaut, les entités sont mutables, ce qui signifie que les mappages peuvent charger des entités existantes, les modifier et stocker une nouvelle version de cette entité. La mutabilité a un prix, et pour les types d'entités dont on sait qu'elles ne seront jamais modifiées, par exemple parce qu'elles contiennent simplement des données extraites textuellement de la chaîne, il est recommandé de les marquer comme immuables avec @entity (immuable : vrai). Les mappages peuvent apporter des modifications aux entités immuables tant que ces modifications se produisent dans le même bloc dans lequel l'entité a été créée. Les entités immuables sont beaucoup plus rapides à écrire et à interroger et doivent donc être utilisées autant que possible.

L'entité Gravatar ci-dessous est structurée autour d'un objet Gravatar et constitue un bon exemple de la façon dont une entité pourrait être définie.

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

Mauvais exemple

Lien vers cette section

Les exemples d'entités GravatarAccepted et GravatarDeclined ci-dessous sont basés sur des événements. Il n'est pas recommandé de mapper des événements ou des appels de fonction à des entités 1:1.

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

Champs facultatifs et obligatoires

Lien vers cette section

Les champs d'entité peuvent être définis comme obligatoires ou facultatifs. Les champs obligatoires sont indiqués par le ! dans le schéma. Si un champ obligatoire n'est pas défini dans le mappage, vous recevrez cette erreur lors de l'interrogation du champ :

Null value resolved for non-null field 'name'

Chaque entité doit avoir un champ id, qui doit être de type Bytes ! ou String !. Il est généralement recommandé d'utiliser Bytes !, à moins que l'identifiant ne contienne du texte lisible par l'homme, car les entités avec des identifiants Bytes ! seront plus rapides à écrire. et interrogez comme ceux avec un String! id. Le champ id sert de clé primaire et doit être unique parmi toutes les entités du même type. Pour des raisons historiques, le type ID! est également accepté et est synonyme de String!.

Pour certains types d'entités, l'id est construit à partir des identifiants de deux autres entités ; cela est possible en utilisant concat, par exemple let id = left.id.concat(right.id) pour former l'identifiant à partir des identifiants de gauche</0 > et <code>à droite. De même, pour construire un identifiant à partir de l'identifiant d'une entité existante et d'un compteur count, let id = left.id.concatI32(count) peut être utilisé. La concaténation est garantie pour produire des identifiants uniques tant que la longueur de left est la même pour toutes ces entités, par exemple, parce que left.id est une adresse .

Types scalaires intégrés

Lien vers cette section

Scalaires pris en charge par GraphQL

Lien vers cette section

Nous prenons en charge les scalaires suivants dans notre API GraphQL :

TypeDescription
OctetsTableau d'octets, représenté sous forme de chaîne hexadécimale. Couramment utilisé pour les hachages et adresses Ethereum.
StringScalaire pour les valeurs chaîne. Les caractères nuls ne sont pas pris en charge et sont automatiquement supprimés.
BooleanScalar pour boolean values.
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.
BigIntGrands entiers. Utilisé pour les types uint32, int64, uint64, ..., uint256 d'Ethereum. Remarque : Tout ce qui se trouve en dessous de uint32, tel que int32, uint24 ou int8, est représenté par i32</. .
BigDecimalBigDecimal Décimales de haute précision représentées sous la forme d'une mantisse et d'un exposant. La plage des exposants va de −6143 à +6144. Arrondi à 34 chiffres significatifs.
TimestampIt is an i64 value in microseconds. Commonly used for timestamp fields for timeseries and aggregations.

Vous pouvez également créer des énumérations dans un schéma. Les énumérations ont la syntaxe suivante :

enum TokenStatus {
OriginalOwner
SecondOwner
ThirdOwner
}

Une fois l'énumération définie dans le schéma, vous pouvez utiliser la représentation sous forme de chaîne de la valeur de l'énumération pour définir un champ d'énumération sur une entité. Par exemple, vous pouvez définir tokenStatus sur SecondOwner en définissant d'abord votre entité, puis en définissant le champ avec entity.tokenStatus = "SecondOwner". L'exemple ci-dessous montre à quoi ressemblerait l'entité Token avec un champ enum :

Plus de détails sur l'écriture d'énumérations peuvent être trouvés dans la documentation GraphQL.

Relations entre entités

Lien vers cette section

Une entité peut avoir une relation avec une ou plusieurs autres entités de votre schéma. Ces relations pourront être parcourues dans vos requêtes. Les relations dans The Graph sont unidirectionnelles. Il est possible de simuler des relations bidirectionnelles en définissant une relation unidirectionnelle à chaque « extrémité » de la relation.

Les relations sont définies sur les entités comme n'importe quel autre champ sauf que le type spécifié est celui d'une autre entité.

Relations individuelles

Lien vers cette section

Définissez un type d'entité Transaction avec une relation un-à-un facultative avec un type d'entité TransactionReceipt :

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

Relations un-à-plusieurs

Lien vers cette section

Définissez un type d'entité TokenBalance avec une relation un-à-plusieurs requise avec un type d'entité Token :

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

Recherches inversées

Lien vers cette section

Des recherches inversées peuvent être définies sur une entité via le champ @derivedFrom. Cela crée un champ virtuel sur l'entité qui peut être interrogé mais ne peut pas être défini manuellement via l'API de mappages. Il découle plutôt de la relation définie sur l’autre entité. Pour de telles relations, il est rarement judicieux de stocker les deux côtés de la relation, et les performances d'indexation et de requête seront meilleures lorsqu'un seul côté est stocké et l'autre est dérivé.

Pour les relations un-à-plusieurs, la relation doit toujours être stockée du côté « un » et le côté « plusieurs » doit toujours être dérivé. Stocker la relation de cette façon, plutôt que de stocker un tableau d'entités du côté « plusieurs », entraînera des performances considérablement meilleures pour l'indexation et l'interrogation du sous-graphe. En général, le stockage de tableaux d’entités doit être évité autant que possible.

Nous pouvons rendre les soldes d'un jeton accessibles à partir du jeton en dérivant un champ tokenBalances :

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

Relations plusieurs-à-plusieurs

Lien vers cette section

Pour les relations plusieurs-à-plusieurs, telles que les utilisateurs pouvant appartenir à un nombre quelconque d'organisations, la manière la plus simple, mais généralement pas la plus performante, de modéliser la relation consiste à créer un tableau dans chacune des deux entités impliquées. Si la relation est symétrique, un seul côté de la relation doit être stocké et l’autre côté peut être dérivé.

Définissez une recherche inversée d'un type d'entité Utilisateur vers un type d'entité Organisation. Dans l'exemple ci-dessous, cela est réalisé en recherchant l'attribut membres à partir de l'entité Organisation. Dans les requêtes, le champ Organisations sur Utilisateur sera résolu en recherchant toutes les entités Organisation qui incluent l'ID de l'utilisateur.

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

Un moyen plus performant de stocker cette relation consiste à utiliser une table de mappage qui comporte une entrée pour chaque paire Utilisateur / Organisation avec un schéma tel que

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

Cette approche nécessite que les requêtes descendent vers un niveau supplémentaire pour récupérer, par exemple, les organisations des utilisateurs :

query usersWithOrganizations {
users {
organizations {
# ceci est une entité UserOrganization
organization {
name
}
}
}
}

Cette manière plus élaborée de stocker des relations plusieurs-à-plusieurs entraînera moins de données stockées pour le subgraph, et donc vers un subgraph qui est souvent considérablement plus rapide à indexer et à interroger.

Ajouter des commentaires au schéma

Lien vers cette section

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

Définir les champs de recherche en texte intégral

Lien vers cette section

Les requêtes de recherche en texte intégral filtrent et classent les entités en fonction d'une entrée de recherche de texte. Les requêtes en texte intégral sont capables de renvoyer des correspondances pour des mots similaires en traitant le texte de la requête saisi en radicaux avant de les comparer aux données textuelles indexées.

Une définition de requête en texte intégrale inclut le nom de la requête, le dictionnaire de langue utilisé pour traiter les champs de texte, l'algorithme de classement utilisé pour classer les résultats et les champs inclus dans la recherche. Chaque requête en texte intégral peut s'étendre sur plusieurs champs, mais tous les champs inclus doivent provenir d'un seul type d'entité.

Pour ajouter une requête de texte intégral, incluez un type _Schema_ avec une directive de texte intégral dans le schéma 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!]!
}

L'exemple de champ bandSearch peut être utilisé dans les requêtes pour filtrer les entités Band en fonction des documents texte dans nom, description</0. > et <code>bio. Accédez à API GraphQL - Requêtes pour une description de l'API de recherche en texte intégral et d'autres exemples d'utilisation.

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

Gestion des fonctionnalités : À partir de specVersion 0.0.4 et au-delà, fullTextSearch doit être déclaré sous la section fonctionnalités dans le manifeste du subgraph.

Langues prises en charge

Lien vers cette section

Le choix d'une langue différente aura un effet définitif, bien que parfois subtil, sur l'API de recherche en texte intégral. Les champs couverts par un champ de requête en texte intégral sont examinés dans le contexte de la langue choisie, de sorte que les lexèmes produits par les requêtes d'analyse et de recherche varient d'une langue à l'autre. Par exemple : lorsque vous utilisez le dictionnaire turc pris en charge, "token" est dérivé de "toke", tandis que, bien sûr, le dictionnaire anglais le dérivera de "token".

Dictionnaires de langues pris en charge :

CodeDictionnaire
simpleGénéral
daDanois
nlNéerlandais
enAnglais
fiFinlandais
frFrançais
deAllemand
huHongrois
itItalien
noNorvégien
ptPortugais
roRoumain
ruRusse
esEspagnol
svSuédois
trTurc

Algorithmes de classement

Lien vers cette section

Algorithmes de classement:

AlgorithmeDescription
rangUtilisez la qualité de correspondance (0-1) de la requête en texte intégral pour trier les résultats.
proximitéRangSimilaire au classement mais inclut également la proximité des matchs.

Écriture de mappages

Lien vers cette section

Les mappages prennent les données d'une source particulière et les transforment en entités définies dans votre schéma. Les mappages sont écrits dans un sous-ensemble de TypeScript appelé [AssemblyScript](https : //github.com/AssemblyScript/assemblyscript/wiki) qui peut être compilé en WASM (WebAssembly). AssemblyScript est plus strict que TypeScript normal, mais fournit une syntaxe familière.

Pour chaque gestionnaire d'événements défini dans subgraph.yaml sous mapping.eventHandlers, créez une fonction exportée du même nom. Chaque gestionnaire doit accepter un seul paramètre appelé event avec un type correspondant au nom de l'événement qui est géré.

Dans le subgraph d'exemple, src/mapping.ts contient des gestionnaires pour les événements NewGravatar et 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()
}

Le premier gestionnaire prend un événement NewGravatar et crée une nouvelle entité Gravatar avec new Gravatar(event.params.id.toHex()), remplissant les champs d'entité en utilisant les paramètres d'événement correspondants. Cette instance d'entité est représentée par la variable gravatar, avec une valeur d'identifiant de event.params.id.toHex().

Le deuxième gestionnaire essaie de charger le Gravatar existant à partir du magasin Graph Node. S'il n'existe pas encore, il est créé à la demande. L'entité est ensuite mise à jour pour correspondre aux nouveaux paramètres d'événement avant d'être réenregistrée dans le magasin à l'aide de gravatar.save().

ID recommandés pour la création de nouvelles entités

Lien vers cette section

Il est fortement recommandé d'utiliser Bytes pour les champs id et de n'utiliser String que pour les attributs qui contiennent vraiment du texte lisible par l'homme, comme le nom d'un jeton. Vous trouverez ci-dessous quelques valeurs de id à prendre en compte lors de la création de nouvelles entités.

  • transfer.id = événement.transaction. hachage

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

Traitement des entités ayant des identifiants identiques

Lien vers cette section

Lors de la création et de l'enregistrement d'une nouvelle entité, si une entité avec le même ID existe déjà, les propriétés de la nouvelle entité sont toujours préférées lors du processus de fusion. Cela signifie que l'entité existante sera mise à jour avec les valeurs de la nouvelle entité.

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.

Si aucune valeur n'est définie pour un champ de la nouvelle entité avec le même ID, le champ aura également la valeur null.

Génération de code

Lien vers cette section

Afin de faciliter et de sécuriser le travail avec les contrats intelligents, les événements et les entités, la CLI Graph peut générer des types AssemblyScript à partir du schéma GraphQL du subgraph et des ABI de contrat inclus dans les sources de données.

Cela se fait avec

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

mais dans la plupart des cas, les subgraphs sont déjà préconfigurés via package.json pour vous permettre d'exécuter simplement l'une des opérations suivantes pour obtenir le même résultat :

# Yarn
yarn codegen
# NPM
npm run codegen

Cela générera une classe AssemblyScript pour chaque contrat intelligent dans les fichiers ABI mentionnés dans subgraph.yaml, vous permettant de lier ces contrats à des adresses spécifiques dans les mappages et d'appeler des méthodes de contrat en lecture seule contre le bloc en cours. traité. Il générera également une classe pour chaque événement de contrat afin de fournir un accès facile aux paramètres de l'événement, ainsi qu'au bloc et à la transaction d'où provient l'événement. Tous ces types sont écrits dans <OUTPUT_DIR>/<DATA_SOURCE_NAME>/<ABI_NAME>.ts. Dans le sous-graphe d'exemple, ce serait generated/Gravity/Gravity.ts, permettant aux mappages d'importer ces types avec.

import {
// La classe de contrat :
Gravity,
// Les classes d'événements :
NewGravatar,
UpdatedGravatar,
} from '../generated/Gravity/Gravity'

De plus, une classe est générée pour chaque type d'entité dans le schéma GraphQL du subgraph. Ces classes fournissent un chargement d'entités de type sécurisé, un accès en lecture et en écriture aux champs d'entité ainsi qu'une méthode save() pour écrire les entités à stocker. Toutes les classes d'entités sont écrites dans <OUTPUT_DIR>/schema.ts, permettant aux mappages de les importer avec

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

Remarque : La génération de code doit être effectuée à nouveau après chaque modification du schéma GraphQL ou des ABI inclus dans le manifeste. Elle doit également être effectuée au moins une fois avant de construire ou de déployer le subgraph.

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.

Modèles de sources de données

Lien vers cette section

Un modèle courant dans les contrats intelligents compatibles EVM est l'utilisation de contrats de registre ou d'usine, dans lesquels un contrat crée, gère ou référence un nombre arbitraire d'autres contrats qui ont chacun leur propre état et leurs propres événements.

Les adresses de ces sous-traitants peuvent ou non être connues à l'avance et bon nombre de ces contrats peuvent être créés et/ou ajoutés au fil du temps. C'est pourquoi, dans de tels cas, définir une seule source de données ou un nombre fixe de sources de données est impossible et une approche plus dynamique est nécessaire : des modèles de sources de données.

Source de données pour le contrat principal

Lien vers cette section

Tout d’abord, vous définissez une source de données régulière pour le contrat principal. L'extrait ci-dessous montre un exemple simplifié de source de données pour le contrat d'usine d'échange Uniswap. Notez le gestionnaire d'événements NewExchange(address,address). Ceci est émis lorsqu'un nouveau contrat d'échange est créé en chaîne par le contrat d'usine.

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

Modèles de source de données pour les contrats créés dynamiquement

Lien vers cette section

Ensuite, vous ajoutez des modèles de source de données au manifeste. Celles-ci sont identiques aux sources de données classiques, sauf qu'il leur manque une adresse de contrat prédéfinie sous source. Généralement, vous définirez un modèle pour chaque type de sous-contrat géré ou référencé par le contrat parent.

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

Instanciation d'un modèle de source de données

Lien vers cette section

Dans la dernière étape, vous mettez à jour votre mappage de contrat principal pour créer une instance de source de données dynamique à partir de l'un des modèles. Dans cet exemple, vous modifieriez le mappage de contrat principal pour importer le modèle Exchange et appeleriez la méthode Exchange.create(address) dessus pour commencer à indexer le nouveau contrat d'échange.

import { Exchange } from '../generated/templates'
export function handleNewExchange(event: NewExchange): void {
// Commence à indexer l'échange ; `event.params.exchange` est le
// adresse du nouveau contrat d'échange
Exchange.create(event.params.exchange)
}

Remarque : Une nouvelle source de données traitera uniquement les appels et les événements du bloc dans lequel elle a été créée et de tous les blocs suivants, mais ne traitera pas les données historiques, c'est-à-dire les données. qui est contenu dans les blocs précédents.

Si les blocs précédents contiennent des données pertinentes pour la nouvelle source de données, il est préférable d'indexer ces données en lisant l'état actuel du contrat et en créant des entités représentant cet état au moment de la création de la nouvelle source de données.

Data Source Context

Lien vers cette section

Les contextes de source de données permettent de transmettre une configuration supplémentaire lors de l'instanciation d'un modèle. Dans notre exemple, disons que les échanges sont associés à une paire de transactions particulière, qui est incluse dans l'événement NewExchange. Ces informations peuvent être transmises à la source de données instanciée, comme suit :

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

A l'intérieur d'un mappage du modèle Exchange, le contexte est alors accessible :

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

Il existe des setters et des getters comme setString et getString pour tous les types de valeur.

Blocs de démarrage

Lien vers cette section

Le startBlock est un paramètre facultatif qui vous permet de définir à partir de quel bloc de la chaîne la source de données commencera l'indexation. La définition du bloc de départ permet à la source de données d'ignorer potentiellement des millions de blocs non pertinents. En règle générale, un développeur de subgraphs définira startBlock sur le bloc dans lequel le contrat intelligent de la source de données a été créé.

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

Remarque : Le bloc de création de contrat peut être rapidement consulté sur Etherscan :

  1. Recherchez le contrat en saisissant son adresse dans la barre de recherche.
  2. Cliquez sur le hachage de la transaction de création dans la section Contract Creator.
  3. Chargez la page des détails de la transaction où vous trouverez le bloc de départ de ce contrat.

Conseils pour l'indexeur

Lien vers cette section

Le paramètre indexerHints dans le manifeste d'un subgraph fournit des directives aux indexeurs sur le traitement et la gestion d'un subgraph. Il influence les décisions opérationnelles concernant la gestion des données, les stratégies d'indexation et les optimisations. Actuellement, il propose l'option prune pour gérer la conservation ou l'élagage des données historiques.

This feature is available from specVersion: 1.0.0

indexerHints.prune : définit la conservation des données de bloc historiques pour un subgraph. Les options incluent :

  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. Un nombre spécifique : Fixe une limite personnalisée au nombre de blocs historiques à conserver.
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.

L'utilisation de "auto" est généralement recommandée car elle maximise les performances des requêtes et est suffisante pour la plupart des utilisateurs qui n'ont pas besoin d'accéder à de nombreuses données historiques.

Pour les subgraphs exploitant les requêtes de voyage dans le temps, il est conseillé soit de définir un nombre spécifique de blocs pour la conservation des données historiques, soit d'utiliser prune : never pour conserver tous les états historiques des entités. Vous trouverez ci-dessous des exemples de configuration des deux options dans les paramètres de votre subgraph :

Pour conserver une quantité spécifique de données historiques :

indexerHints:
prune: 1000 # Replace 1000 with the desired number of blocks to retain

Préserver l'histoire complète des États de l'entité :

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

Lien vers cette section

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

Lien vers cette section

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.

Gestionnaires d'appels

Lien vers cette section

Bien que les événements constituent un moyen efficace de collecter les modifications pertinentes apportées à l'état d'un contrat, de nombreux contrats évitent de générer des journaux pour optimiser les coûts du gaz. Dans ces cas, un subgraph peut s'abonner aux appels effectués vers le contrat de source de données. Ceci est réalisé en définissant des gestionnaires d'appels faisant référence à la signature de la fonction et au gestionnaire de mappage qui traitera les appels à cette fonction. Pour traiter ces appels, le gestionnaire de mappage recevra un ethereum.Call comme argument avec les entrées et sorties saisies de l'appel. Les appels effectués à n'importe quelle profondeur dans la chaîne d'appels d'une transaction déclencheront le mappage, permettant de capturer l'activité avec le contrat de source de données via des contrats proxy.

Les gestionnaires d'appels ne se déclencheront que dans l'un des deux cas suivants : lorsque la fonction spécifiée est appelée par un compte autre que le contrat lui-même ou lorsqu'elle est marquée comme externe dans Solidity et appelée dans le cadre d'une autre fonction du même contrat.

Remarque : Les gestionnaires d'appels dépendent actuellement de l'API de suivi de parité. Certains réseaux, tels que la chaîne BNB et Arbitrum, ne prennent pas en charge cette API. Si un subgraph indexant l’un de ces réseaux contient un ou plusieurs gestionnaires d’appels, il ne démarrera pas la synchronisation. Les développeurs de subgraphs devraient plutôt utiliser des gestionnaires d'événements. Ceux-ci sont bien plus performants que les gestionnaires d'appels et sont pris en charge sur tous les réseaux evm.

Définir un gestionnaire d'appels

Lien vers cette section

Pour définir un gestionnaire d'appels dans votre manifeste, ajoutez simplement un tableau callHandlers sous la source de données à laquelle vous souhaitez vous abonner.

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 fonction est la signature de fonction normalisée permettant de filtrer les appels. La propriété handler est le nom de la fonction de votre mappage que vous souhaitez exécuter lorsque la fonction cible est appelée dans le contrat de source de données.

Fonction de cartographie

Lien vers cette section

Chaque gestionnaire d'appel prend un seul paramètre dont le type correspond au nom de la fonction appelée. Dans l'exemple de subgraph ci-dessus, le mappage contient un gestionnaire lorsque la fonction createGravatar est appelée et reçoit un paramètre CreateGravatarCall comme argument :

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 fonction handleCreateGravatar prend un nouveau CreateGravatarCall qui est une sous-classe de ethereum. Call, fournie par @graphprotocol/graph-ts, qui inclut les entrées et sorties saisies de l’appel. Le type CreateGravatarCall est généré pour vous lorsque vous exécutez graph codegen.

Block Handlers

Lien vers cette section

En plus de s'abonner à des événements de contrat ou à des appels de fonction, un subgraph peut souhaiter mettre à jour ses données à mesure que de nouveaux blocs sont ajoutés à la chaîne. Pour y parvenir, un subgraph peut exécuter une fonction après chaque bloc ou après des blocs correspondant à un filtre prédéfini.

Filtres pris en charge

Lien vers cette section

Filtre d'appel

Lien vers cette section
filter:
kind: call

Le gestionnaire défini sera appelé une fois pour chaque bloc contenant un appel au contrat (source de données) sous lequel le gestionnaire est défini.

Remarque : Le filtre call dépend actuellement de l'API de traçage de parité. Certains réseaux, tels que la chaîne BNB et Arbitrum, ne prennent pas en charge cette API. Si un subgraph indexant l'un de ces réseaux contient un ou plusieurs gestionnaires de blocs avec un filtre call, il ne démarrera pas la synchronisation.

L'absence de filtre pour un gestionnaire de bloc garantira que le gestionnaire est appelé à chaque bloc. Une source de données ne peut contenir qu'un seul gestionnaire de bloc pour chaque type de filtre.

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

Filtre d'interrogation

Lien vers cette section

Nécessite specVersion >= 0.0.8

Remarque : Les filtres d'interrogation ne sont disponibles que sur les sources de données de genre : ethereum.

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

Le gestionnaire défini sera appelé une fois pour tous les blocs n, où n est la valeur fournie dans le champ every. Cette configuration permet au sugraph d'effectuer des opérations spécifiques à intervalles réguliers.

Le filtre Once

Lien vers cette section

Nécessite specVersion >= 0.0.8

Remarque : Les filtres Once ne sont disponibles que sur les sources de données de genre : Ethereum.

blockHandlers:
- handler: handleOnce
filter:
kind: once

Le gestionnaire défini avec le filtre once ne sera appelé qu'une seule fois avant l'exécution de tous les autres gestionnaires. Cette configuration permet au subgraph d'utiliser le gestionnaire comme gestionnaire d'initialisation, effectuant des tâches spécifiques au début de l'indexation.

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

Fonction de cartographie

Lien vers cette section

La fonction de mappage recevra un ethereum.Block comme seul argument. Comme les fonctions de mappage pour les événements, cette fonction peut accéder aux entités de subgraphs existantes dans le magasin, appeler des contrats intelligents et créer ou mettre à jour des entités.

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

Événements anonymes

Lien vers cette section

Si vous devez traiter des événements anonymes dans Solidity, cela peut être réalisé en fournissant le sujet 0 de l'événement, comme dans l'exemple :

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

Un événement ne sera déclenché que lorsque la signature et le sujet 0 correspondent. Par défaut, topic0 est égal au hachage de la signature de l'événement.

Reçus de transaction dans les gestionnaires d'événements

Lien vers cette section

À partir de specVersion 0.0.5 et apiVersion 0.0.7, les gestionnaires d'événements peuvent avoir accès au reçu du transaction qui les a émis.

Pour ce faire, les gestionnaires d'événements doivent être déclarés dans le manifeste du subgraph avec la nouvelle clé receipt: true, qui est facultative et vaut par défaut false.

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

Dans la fonction de gestionnaire, le reçu est accessible dans le champ Event.receipt. Lorsque la clé receipt est définie sur false ou omise dans le manifeste, une valeur null sera renvoyée à la place.

Fonctionnalités expérimentales

Lien vers cette section

À partir de specVersion 0.0.4, les fonctionnalités de subgraph doivent être explicitement déclarées dans la section features au niveau supérieur du fichier manifeste, en utilisant leur camelCase, comme indiqué dans le tableau ci-dessous :

FonctionnalitéNom
Erreurs non fataleserreursnonfatales
Recherche en texte intégralfullTextSearch
La greffegrafting

Par exemple, si un subgraph utilise les fonctionnalités Recherche en texte intégral et Erreurs non fatales features, le features dans le manifeste doit être :

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

Notez que l'utilisation d'une fonctionnalité sans la déclarer entraînera une erreur de validation lors du déploiement du sous-graphe, mais aucune erreur ne se produira si une fonctionnalité est déclarée mais n'est pas utilisée.

Timeseries and Aggregations

Lien vers cette section

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

Lien vers cette section
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

Lien vers cette section

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

Lien vers cette section
  • 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

Lien vers cette section
  • 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

Lien vers cette section
{
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.

Erreurs non fatales

Lien vers cette section

Les erreurs d'indexation sur les subgraphs déjà synchronisés entraîneront, par défaut, l'échec du subgraph et l'arrêt de la synchronisation. Les subgraphs peuvent également être configurés pour continuer la synchronisation en présence d'erreurs, en ignorant les modifications apportées par le gestionnaire qui a provoqué l'erreur. Cela donne aux auteurs de subgraphs le temps de corriger leurs subgraphs pendant que les requêtes continuent d'être traitées sur le dernier bloc, bien que les résultats puissent être incohérents en raison du bogue à l'origine de l'erreur. Notez que certaines erreurs sont toujours fatales. Pour être non fatale, l'erreur doit être connue pour être déterministe.

Remarque : Le réseau Graph ne prend pas encore en charge les erreurs non fatales et les développeurs ne doivent pas déployer de subgraphs utilisant cette fonctionnalité sur le réseau via le Studio.

L'activation des erreurs non fatales nécessite la définition de l'indicateur de fonctionnalité suivant sur le manifeste du subgraph :

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

La requête doit également choisir d'interroger les données présentant des incohérences potentielles via l'argument subgraphError. Il est également recommandé d'interroger _meta pour vérifier si le subgraph a ignoré des erreurs, comme dans l'exemple :

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

Si le subgraph rencontre une erreur, cette requête renverra à la fois les données et une erreur graphql avec le message "indexing_error", comme dans cet exemple de réponse :

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

Greffe sur des subgraphs existants

Lien vers cette section

Remarque : il n'est pas recommandé d'utiliser le greffage lors de la mise à niveau initiale vers The Graph Network. Apprenez-en plus ici.

Lorsqu'un subgraph est déployé pour la première fois, il commence à indexer les événements au niveau du bloc Genesis de la chaîne correspondante (ou au startBlock défini avec chaque source de données). Dans certaines circonstances ; il est avantageux de réutiliser les données d'un subgraph existant et de commencer l'indexation à un bloc beaucoup plus tard. Ce mode d'indexation est appelé Grafting. Le greffage est, par exemple, utile pendant le développement pour surmonter rapidement de simples erreurs dans les mappages ou pour faire fonctionner à nouveau temporairement un subgraph existant après son échec.

Un subgraph est greffé sur un subgraph de base lorsque le manifeste du soubgraph dans subgraph.yaml contient un bloc graft au niveau supérieur :

description: ...
graft:
base: Qm... # Subgraph ID of base subgraph
block: 7345624 # Block number

Lorsqu'un subgraph dont le manifeste contient un bloc graft est déployé, Graph Node copiera les données du subgraph base jusqu'au bloc donné inclus. puis continuez à indexer le nouveau subgraph à partir de ce bloc. Le subgraph de base doit exister sur l'instance Graph Node cible et doit avoir été indexé jusqu'au moins au bloc donné. En raison de cette restriction, le greffage ne doit être utilisé que pendant le développement ou en cas d'urgence pour accélérer la production d'un subgraph équivalent non greffé.

Étant donné que le greffage copie plutôt que l'indexation des données de base, il est beaucoup plus rapide d'amener le susgraph dans le bloc souhaité que l'indexation à partir de zéro, bien que la copie initiale des données puisse encore prendre plusieurs heures pour de très gros subgraphs. Pendant l'initialisation du subgraph greffé, le nœud graphique enregistrera des informations sur les types d'entités qui ont déjà été copiés.

Le subgraph greffé peut utiliser un schéma GraphQL qui n'est pas identique à celui du subgraph de base, mais simplement compatible avec celui-ci. Il doit s'agir d'un schéma de subgraph valide à part entière, mais il peut s'écarter du schéma du subgraph de base des manières suivantes :

  • Il ajoute ou supprime des types d'entités
  • Il supprime les attributs des types d'entités
  • Il ajoute des attributs nullables aux types d'entités
  • Il transforme les attributs non nullables en attributs nullables
  • Il ajoute des valeurs aux énumérations
  • Il ajoute ou supprime des interfaces
  • Cela change pour quels types d'entités une interface est implémentée

Gestion des fonctionnalités : le greffage doit être déclaré sous featuresdans le manifeste du subgraph.

IPFS/Arweave File Data Sources

Lien vers cette section

Les sources de données de fichiers sont une nouvelle fonctionnalité de subgraph permettant d'accéder aux données hors chaîne pendant l'indexation de manière robuste et extensible. Les sources de données de fichiers prennent en charge la récupération de fichiers depuis IPFS et Arweave.

Cela jette également les bases d’une indexation déterministe des données hors chaîne, ainsi que de l’introduction potentielle de données arbitraires provenant 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.

Cela remplace l'API ipfs.cat existante

Guide de mise à niveau

Lien vers cette section

Mettre à jour graph-ts et graph-cli

Lien vers cette section

Les sources de données de fichiers nécessitent graph-ts >=0.29.0 et graph-cli >=0.33.1

Ajouter un nouveau type d'entité qui sera mis à jour lorsque des fichiers seront trouvés

Lien vers cette section

Les sources de données de fichier ne peuvent pas accéder ni mettre à jour les entités basées sur une chaîne, mais doivent mettre à jour les entités spécifiques au fichier.

Cela peut impliquer de diviser les champs des entités existantes en entités distinctes, liées entre elles.

Entité combinée d'origine :

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

Nouvelle entité scindée :

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 relation est 1:1 entre l'entité parent et l'entité de source de données de fichier résultante, le modèle le plus simple consiste à lier l'entité parent à une entité de fichier résultante en utilisant le CID IPFS comme recherche. Contactez Discord si vous rencontrez des difficultés pour modéliser vos nouvelles entités basées sur des fichiers !

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

Ajoutez une nouvelle source de données modélisée avec kind: file/ipfs ou kind: file/arweave

Lien vers cette section

Il s'agit de la source de données qui sera générée lorsqu'un fichier d'intérêt est identifié.

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

Actuellement, les abis sont requis, bien qu'il ne soit pas possible d'appeler des contrats à partir de sources de données de fichiers

The file data source must specifically mention all the entity types which it will interact with under entities. See limitations for more details.

Créer un nouveau gestionnaire pour traiter les fichiers

Lien vers cette section

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

Le CID du fichier sous forme de chaîne lisible est accessible via dataSource comme suit :

const cid = dataSource.stringParam()

Exemple de gestionnaire :

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

Générer des sources de données de fichiers si nécessaire

Lien vers cette section

Vous pouvez désormais créer des sources de données de fichiers lors de l'exécution de gestionnaires basés sur une chaîne :

  • Importez le modèle à partir des modèles générés automatiquement
  • appeler TemplateName.create(cid : string) à partir d'un mappage, où le cid est un identifiant de contenu valide pour IPFS ou Arweave

Pour IPFS, Graph Node prend en charge les identifiants de contenu v0 et v1, ainsi que les identifiants de contenu avec des répertoires (par exemple 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.

Exemple:

import { TokenMetadata as TokenMetadataTemplate } from '../generated/templates'
const ipfshash = 'QmaXzZhcYnsisuue5WRdQDH6FDvqkLQX1NckLqBYeYYEfm'
//Cet exemple de code concerne un sous-graphe de Crypto coven. Le hachage ipfs ci-dessus est un répertoire contenant les métadonnées des jetons pour toutes les NFT de l'alliance cryptographique.
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
//Ceci crée un chemin vers les métadonnées pour un seul Crypto coven NFT. Il concatène le répertoire avec "/" + nom de fichier + ".json"
token.ipfsURI = tokenIpfsHash
TokenMetadataTemplate.create(tokenIpfsHash)
}
token.updatedAtTimestamp = event.block.timestamp
token.owner = event.params.to.toHexString()
token.save()
}

Cela créera une nouvelle source de données de fichier, qui interrogera le point d'extrémité IPFS ou Arweave configuré du nœud de graphique, en réessayant si elle n'est pas trouvée. Lorsque le fichier est trouvé, le gestionnaire de la source de données de fichier est exécuté.

Cet exemple utilise le CID comme recherche entre l'entité Token parent et l'entité TokenMetadata résultante.

Auparavant, c'est à ce stade qu'un développeur de subgraphs aurait appelé ipfs.cat(CID) pour récupérer le fichier

Félicitations, vous utilisez des sources de données de fichiers !

Déployer vos subgraphs

Lien vers cette section

Vous pouvez maintenant construire et déployer votre subgraph sur n'importe quel nœud de graph >=v0.30.0-rc.0.

Les entités et les gestionnaires de sources de données de fichiers sont isolés des autres entités du subgraph, ce qui garantit que leur exécution est déterministe et qu'il n'y a pas de contamination des sources de données basées sur des chaînes. Pour être plus précis :

  • Les entités créées par les sources de données de fichiers sont immuables et ne peuvent pas être mises à jour
  • Les gestionnaires de sources de données de fichiers ne peuvent pas accéder à des entités provenant d'autres sources de données de fichiers
  • Les entités associées aux sources de données de fichiers ne sont pas accessibles aux gestionnaires basés sur des chaînes

Cette contrainte ne devrait pas poser de problème pour la plupart des cas d'utilisation, mais elle peut en compliquer certains. N'hésitez pas à nous contacter via Discord si vous rencontrez des problèmes pour modéliser vos données basées sur des fichiers dans un subgraph !

En outre, il n'est pas possible de créer des sources de données à partir d'une source de données de fichier, qu'il s'agisse d'une source de données onchain ou d'une autre source de données de fichier. Cette restriction pourrait être levée à l'avenir.

Meilleures pratiques

Lien vers cette section

Si vous liez des métadonnées NFT aux jetons correspondants, utilisez le hachage IPFS des métadonnées pour référencer une entité Metadata à partir de l'entité Token. Enregistrez l'entité Metadata en utilisant le hachage IPFS comme identifiant.

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 vous avez des entités qui sont actualisées plusieurs fois, créez des entités uniques basées sur des fichiers en utilisant le hachage & IPFS ; l'ID de l'entité, et référencez-les en utilisant un champ dérivé dans l'entité basée sur la chaîne.

Nous travaillons à l'amélioration de la recommandation ci-dessus, afin que les requêtes ne renvoient que la version "la plus récente"

Problèmes connus

Lien vers cette section

Les sources de données de fichiers nécessitent actuellement des ABI, même si les ABI ne sont pas utilisées (problème). La solution consiste à ajouter n'importe quel 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.

Crypto Coven Subgraph migration

Les Références

Lien vers cette section

Sources de données du fichier GIP

Modifier une page

Précédente
Réseaux supportés
Suivante
API AssemblyScript
Modifier une page