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.
Un subgraph se constitue des fichiers suivants :
-
subgraph.yaml
: un fichier YAML qui contient le manifeste du subgraph -
schema.graphql
: un schéma GraphQL qui définit les données stockées pour votre subgraph et comment les interroger via GraphQL -
Mappages AssemblyScript
: qui traduit les données d'événement en entités définies dans votre schéma (par exemplemapping.ts
dans ce tutoriel)
In order to use your subgraph on The Graph's decentralized network, you will need to . It is recommended that you to your subgraph with at least .
Before you go into detail about the contents of the manifest file, you need to install the which you will need to build and deploy a subgraph.
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.
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.
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 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.
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 nouveaudataSource
doit utiliser leseventHandlers
&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 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 .
Pour l'exemple de subgraph, subgraph.yaml
est :
version spec : 0.0.4description : Gravatar pour Ethereumréférentiel : https://github.com/graphprotocol/graph-toolingschéma:fichier : ./schema.graphqlindexeurConseils :tailler : automatiqueles sources de données:- genre : ethereum/contratnom: Gravitéréseau : réseau principalsource:adresse : '0x2E645469f354BB4F5c8a05B3b30A929361cf77eC'abi : Gravitébloc de démarrage : 6175244bloc de fin : 7175245contexte:foo :tapez : Booléendonnées : vraibar:tapez : chaînedonnées : 'barre'cartographie :genre : ethereum/événementsVersion api : 0.0.6langage : wasm/assemblyscriptentités :-Gravatarabis :- nom : Gravitéfichier : ./abis/Gravity.jsonGestionnaires d'événements :- événement : NewGravatar(uint256,adresse,chaîne,chaîne)gestionnaire : handleNewGravatar- événement : UpdatedGravatar (uint256, adresse, chaîne, chaîne)gestionnaire : handleUpdatedGravatarGestionnaires d'appels :- fonction : createGravatar(string,string)gestionnaire : handleCreateGravatargestionnaires de blocs :- gestionnaire : handleBlock- gestionnaire : handleBlockWithCallfiltre:genre : appelerfichier : ./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 is1.2.0
. See section to see more details on features & releases. -
description
: a human-readable description of what the subgraph is. This description is displayed in Graph Explorer when the subgraph is deployed to Subgraph Studio. -
repository
: the URL of the repository where the subgraph manifest can be found. This is also displayed in Graph Explorer. -
indexerHints.prune
: Définit la conservation des données de blocs historiques pour un subgraph. Voir dans la section . -
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 commeBool
,String
,Int
,Int8
,BigDecimal
,Octets
,Liste
etBigInt
. Chaque variable doit spécifier sontype
et sesdonné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 champfilter
aveckind: 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
.
Les déclencheurs d'une source de données au sein d'un bloc sont classés à l'aide du processus suivant :
- Les déclencheurs d'événements et d'appels sont d'abord classés par index de transaction au sein du bloc.
- 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.
- 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 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.
Topic filters, also known as indexed argument filters, are a powerful feature in subgraphs that allow users to precisely filter blockchain events based on the values of their indexed arguments.
-
These filters help isolate specific events of interest from the vast stream of events on the blockchain, allowing subgraphs to operate more efficiently by focusing only on relevant data.
-
This is useful for creating personal subgraphs that track specific addresses and their interactions with various smart contracts on the blockchain.
When a smart contract emits an event, any arguments that are marked as indexed can be used as filters in a subgraph's manifest. This allows the subgraph to listen selectively for events that match these indexed arguments.
- The event's first indexed argument corresponds to
topic1
, the second totopic2
, and so on, up totopic3
, since the Ethereum Virtual Machine (EVM) allows up to three indexed arguments per event.
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Token {// Event declaration with indexed parameters for addressesevent Transfer(address indexed from, address indexed to, uint256 value);// Function to simulate transferring tokensfunction transfer(address to, uint256 value) public {// Emitting the Transfer event with from, to, and valueemit Transfer(msg.sender, to, value);}}
In this example:
- The
Transfer
event is used to log transactions of tokens between addresses. - The
from
andto
parameters are indexed, allowing event listeners to filter and monitor transfers involving specific addresses. - The
transfer
function is a simple representation of a token transfer action, emitting the Transfer event whenever it is called.
Topic filters are defined directly within the event handler configuration in the subgraph manifest. Here is how they are configured:
eventHandlers:- event: SomeEvent(indexed uint256, indexed address, indexed uint256)handler: handleSomeEventtopic1: ['0xValue1', '0xValue2']topic2: ['0xAddress1', '0xAddress2']topic3: ['0xValue3']
In this setup:
topic1
corresponds to the first indexed argument of the event,topic2
to the second, andtopic3
to the third.- Each topic can have one or more values, and an event is only processed if it matches one of the values in each specified topic.
- Within a Single Topic: The logic functions as an OR condition. The event will be processed if it matches any one of the listed values in a given topic.
- Between Different Topics: The logic functions as an AND condition. An event must satisfy all specified conditions across different topics to trigger the associated handler.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleDirectedTransfertopic1: ['0xAddressA'] # Sender Addresstopic2: ['0xAddressB'] # Receiver Address
In this configuration:
topic1
is configured to filterTransfer
events where0xAddressA
is the sender.topic2
is configured to filterTransfer
events where0xAddressB
is the receiver.- The subgraph will only index transactions that occur directly from
0xAddressA
to0xAddressB
.
eventHandlers:- event: Transfer(indexed address,indexed address,uint256)handler: handleTransferToOrFromtopic1: ['0xAddressA', '0xAddressB', '0xAddressC'] # Sender Addresstopic2: ['0xAddressB', '0xAddressC'] # Receiver Address
In this configuration:
topic1
is configured to filterTransfer
events where0xAddressA
,0xAddressB
,0xAddressC
is the sender.topic2
is configured to filterTransfer
events where0xAddressB
and0xAddressC
is the receiver.- The subgraph will index transactions that occur in either direction between multiple addresses allowing for comprehensive monitoring of interactions involving all addresses.
Declarative eth_calls
are a valuable subgraph feature that allows eth_calls
to be executed ahead of time, enabling graph-node
to execute them in parallel.
This feature does the following:
- Significantly improves the performance of fetching data from the Ethereum blockchain by reducing the total time for multiple calls and optimizing the subgraph's overall efficiency.
- Allows faster data fetching, resulting in quicker query responses and a better user experience.
- Reduces wait times for applications that need to aggregate data from multiple Ethereum calls, making the data retrieval process more efficient.
- Declarative
eth_calls
: Ethereum calls that are defined to be executed in parallel rather than sequentially. - Parallel Execution: Instead of waiting for one call to finish before starting the next, multiple calls can be initiated simultaneously.
- Time Efficiency: The total time taken for all the calls changes from the sum of the individual call times (sequential) to the time taken by the longest call (parallel).
Imagine you have a subgraph that needs to make three Ethereum calls to fetch data about a user's transactions, balance, and token holdings.
Traditionally, these calls might be made sequentially:
- Call 1 (Transactions): Takes 3 seconds
- Call 2 (Balance): Takes 2 seconds
- Call 3 (Token Holdings): Takes 4 seconds
Total time taken = 3 + 2 + 4 = 9 seconds
With this feature, you can declare these calls to be executed in parallel:
- Call 1 (Transactions): Takes 3 seconds
- Call 2 (Balance): Takes 2 seconds
- Call 3 (Token Holdings): Takes 4 seconds
Since these calls are executed in parallel, the total time taken is equal to the time taken by the longest call.
Total time taken = max (3, 2, 4) = 4 seconds
- Declarative Definition: In the subgraph manifest, you declare the Ethereum calls in a way that indicates they can be executed in parallel.
- Parallel Execution Engine: The Graph Node's execution engine recognizes these declarations and runs the calls simultaneously.
- Result Aggregation: Once all calls are complete, the results are aggregated and used by the subgraph for further processing.
Declared eth_calls
can access the event.address
of the underlying event as well as all the event.params
.
Subgraph.yaml
using event.address
:
eventHandlers:event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)handler: handleSwapcalls:global0X128: Pool[event.address].feeGrowthGlobal0X128()global1X128: Pool[event.address].feeGrowthGlobal1X128()
Details for the example above:
global0X128
is the declaredeth_call
.- The text before colon(
global0X128
) is the label for thiseth_call
which is used when logging errors. - The text (
Pool[event.address].feeGrowthGlobal0X128()
) is the actualeth_call
that will be executed, which is in the form ofContract[address].function(arguments)
- The
address
andarguments
can be replaced with variables that will be available when the handler is executed.
Subgraph.yaml
using event.params
calls:- ERC20DecimalsToken0: ERC20[event.params.token0].decimals()
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 ou en utilisant solc pour compiler.
- Vous pouvez également trouver l'ABI sur , 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 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 .
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: BytesdisplayName: StringimageUrl: Stringaccepted: Boolean}
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: BytesdisplayName: StringimageUrl: String}type GravatarDeclined @entity {id: Bytes!owner: BytesdisplayName: StringimageUrl: String}
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
.
Nous prenons en charge les scalaires suivants dans notre API GraphQL :
Type | Description |
---|---|
Octets | Tableau d'octets, représenté sous forme de chaîne hexadécimale. Couramment utilisé pour les hachages et adresses Ethereum. |
String | Scalaire pour les valeurs chaîne . Les caractères nuls ne sont pas pris en charge et sont automatiquement supprimés. |
Boolean | Scalar pour boolean values. |
Int | The GraphQL spec defines Int to be a signed 32-bit integer. |
Int8 | An 8-byte signed integer, also known as a 64-bit signed integer, can store values in the range from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Prefer using this to represent i64 from ethereum. |
BigInt | Grands 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</. . |
BigDecimal | BigDecimal 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. |
Timestamp | It 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 {OriginalOwnerSecondOwnerThirdOwner}
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 .
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é.
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}
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!}
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!}
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é UserOrganizationorganization {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.
As per GraphQL spec, comments can be added above schema entity attributes using the hash symble #
. This is illustrated in the example below:
type MyFirstEntity @entity {# unique identifier and primary key of the entityid: Bytes!address: Bytes!}
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: enalgorithm: rankinclude: [{ entity: "Band", fields: [{ name: "name" }, { name: "description" }, { name: "bio" }] }])type Band @entity {id: Bytes!name: String!description: String!bio: Stringwallet: Addresslabels: [Label!]!discography: [Album!]!members: [Musician!]!}
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 à pour une description de l'API de recherche en texte intégral et d'autres exemples d'utilisation.
query {bandSearch(text: "breaks & electro & detroit") {idnamedescriptionwallet}}
: À partir de specVersion
0.0.4
et au-delà, fullTextSearch
doit être déclaré sous la section fonctionnalités
dans le manifeste du subgraph.
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 :
Code | Dictionnaire |
---|---|
simple | Général |
da | Danois |
nl | Néerlandais |
en | Anglais |
fi | Finlandais |
fr | Français |
de | Allemand |
hu | Hongrois |
it | Italien |
no | Norvégien |
pt | Portugais |
ro | Roumain |
ru | Russe |
es | Espagnol |
sv | Suédois |
tr | Turc |
Algorithmes de classement:
Algorithme | Description |
---|---|
rang | Utilisez la qualité de correspondance (0-1) de la requête en texte intégral pour trier les résultats. |
proximitéRang | Similaire au classement mais inclut également la proximité des matchs. |
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 appelé [AssemblyScript](https : //github.com/AssemblyScript/assemblyscript/wiki) qui peut être compilé en WASM (). 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.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.save()}export function handleUpdatedGravatar(event: UpdatedGravatar): void {let id = event.params.idlet gravatar = Gravatar.load(id)if (gravatar == null) {gravatar = new Gravatar(id)}gravatar.owner = event.params.ownergravatar.displayName = event.params.displayNamegravatar.imageUrl = event.params.imageUrlgravatar.save()}
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()
.
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 aBytes
as theid
is beneficial. Determining theid
would look like
let dayID = event.block.timestamp.toI32() / 86400let id = Bytes.fromI32(dayID)
- Convert constant addresses to
Bytes
.
const id = Bytes.fromHexString('0xdead...beef')
There is a which contains utilities for interacting with the Graph Node store and conveniences for handling smart contract data and entities. It can be imported into mapping.ts
from @graphprotocol/graph-ts
.
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.
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 :
# Yarnyarn codegen# NPMnpm 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.
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.
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 . 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/contractname: Factorynetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: Factorymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Directoryabis:- name: Factoryfile: ./abis/factory.jsoneventHandlers:- event: NewExchange(address,address)handler: handleNewExchange
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/contractname: Factory# ... other source fields for the main contract ...templates:- name: Exchangekind: ethereum/contractnetwork: mainnetsource:abi: Exchangemapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/exchange.tsentities:- Exchangeabis:- name: Exchangefile: ./abis/exchange.jsoneventHandlers:- event: TokenPurchase(address,uint256,uint256)handler: handleTokenPurchase- event: EthPurchase(address,uint256,uint256)handler: handleEthPurchase- event: AddLiquidity(address,uint256,uint256)handler: handleAddLiquidity- event: RemoveLiquidity(address,uint256,uint256)handler: handleRemoveLiquidity
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'échangeExchange.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.
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.
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/contractname: ExampleSourcenetwork: mainnetsource:address: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'abi: ExampleContractstartBlock: 6627917mapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptfile: ./src/mappings/factory.tsentities:- Userabis:- name: ExampleContractfile: ./abis/ExampleContract.jsoneventHandlers:- event: NewEvent(address,address)handler: handleNewEvent
Remarque : Le bloc de création de contrat peut être rapidement consulté sur Etherscan :
- Recherchez le contrat en saisissant son adresse dans la barre de recherche.
- Cliquez sur le hachage de la transaction de création dans la section
Contract Creator
. - Chargez la page des détails de la transaction où vous trouverez le bloc de départ de ce contrat.
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 :
"never"
: No pruning of historical data; retains the entire history."auto"
: Retains the minimum necessary history as set by the indexer, optimizing query performance.- 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:
- , which enable querying the past states of these entities at specific blocks throughout the subgraph's history
- Using the subgraph as a in another subgraph, at that block
- Rewinding the subgraph back to that block
If historical data as of the block has been pruned, the above capabilities will not be available.
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 , 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 :
{indexingStatuses(subgraphs: ["Qm..."]) {subgraphsyncedhealthchains {earliestBlock {number}latestBlock {number}chainHeadBlock { number }}}}
Note that the earliestBlock
is the earliest block with historical data, which will be more recent than the startBlock
specified in the manifest, if the subgraph has been pruned.
Event handlers in a subgraph react to specific events emitted by smart contracts on the blockchain and trigger handlers defined in the subgraph's manifest. This enables subgraphs to process and store event data according to defined logic.
An event handler is declared within a data source in the subgraph's YAML configuration. It specifies which events to listen for and the corresponding function to execute when those events are detected.
dataSources:- kind: ethereum/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsoneventHandlers:- event: Approval(address,address,uint256)handler: handleApproval- event: Transfer(address,address,uint256)handler: handleTransfertopic1: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', '0xc8dA6BF26964aF9D7eEd9e03E53415D37aA96325'] # Optional topic filter which filters only events with the specified topic.
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.
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/contractname: Gravitynetwork: mainnetsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsoncallHandlers:- function: createGravatar(string,string)handler: handleCreateGravatar
La 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.
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.hashlet transaction = new Transaction(id)transaction.displayName = call.inputs._displayNametransaction.imageUrl = call.inputs._imageUrltransaction.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
.
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.
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/contractname: Gravitynetwork: devsource:address: '0x731a10897d267e19b34503ad902d0a29173ba4b1'abi: Gravitymapping:kind: ethereum/eventsapiVersion: 0.0.6language: wasm/assemblyscriptentities:- Gravatar- Transactionabis:- name: Gravityfile: ./abis/Gravity.jsonblockHandlers:- handler: handleBlock- handler: handleBlockWithCallToContractfilter:kind: call
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: handleBlockfilter:kind: pollingevery: 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.
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: handleOncefilter: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()}
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.hashlet entity = new Block(id)entity.save()}
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.
À 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: handleNewGravatarreceipt: 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.
À 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 :
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.4description: Gravatar for Ethereumfeatures:- fullTextSearch- nonFatalErrorsdataSources: ...
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 enable your subgraph to track statistics like daily average price, hourly total transfers, etc.
This feature introduces two new types of subgraph entity. Timeseries entities record data points with timestamps. Aggregation entities perform pre-declared calculations on the Timeseries data points on an hourly or daily basis, then store the results for easy access via GraphQL.
type Data @entity(timeseries: true) {id: Int8!timestamp: Timestamp!price: BigDecimal!}type Stats @aggregation(intervals: ["hour", "day"], source: "Data") {id: Int8!timestamp: Timestamp!sum: BigDecimal! @aggregate(fn: "sum", arg: "price")}
Timeseries entities are defined with @entity(timeseries: true)
in schema.graphql. Every timeseries entity must have a unique ID of the int8 type, a timestamp of the Timestamp type, and include data that will be used for calculation by aggregation entities. These Timeseries entities can be saved in regular trigger handlers, and act as the “raw data” for the Aggregation entities.
Aggregation entities are defined with @aggregation
in schema.graphql. Every aggregation entity defines the source from which it will gather data (which must be a Timeseries entity), sets the intervals (e.g., hour, day), and specifies the aggregation function it will use (e.g., sum, count, min, max, first, last). Aggregation entities are automatically calculated on the basis of the specified source at the end of the required interval.
hour
: sets the timeseries period every hour, on the hour.day
: sets the timeseries period every day, starting and ending at 00:00.
sum
: Total of all values.count
: Number of values.min
: Minimum value.max
: Maximum value.first
: First value in the period.last
: Last value in the period.
{stats(interval: "hour", where: { timestamp_gt: 1704085200 }) {idtimestampsum}}
Note:
To use Timeseries and Aggregations, a subgraph must have a spec version ≥1.1.0. Note that this feature might undergo significant changes that could affect backward compatibility.
about Timeseries and Aggregations.
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.4description: Gravatar for Ethereumfeatures:- 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"}]
Remarque : il n'est pas recommandé d'utiliser le greffage lors de la mise à niveau initiale vers The Graph Network. Apprenez-en plus .
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 subgraphblock: 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
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 , which are used to dynamically create new chain-based data sources.
Cela remplace l'API ipfs.cat
existante
Les sources de données de fichiers nécessitent graph-ts >=0.29.0 et graph-cli >=0.33.1
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: BigIntowner: User!}
Nouvelle entité scindée :
type Token @entity {id: ID!tokenID: BigInt!tokenURI: String!ipfsURI: TokenMetadataupdatedAtTimestamp: BigIntowner: String!}type TokenMetadata @entity {id: ID!image: String!externalURL: String!name: String!description: String!}
Si la 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 !
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: TokenMetadatakind: file/ipfsmapping:apiVersion: 0.0.7language: wasm/assemblyscriptfile: ./src/mapping.tshandler: handleMetadataentities:- TokenMetadataabis:- name: Tokenfile: ./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 for more details.
This handler should accept one Bytes
parameter, which will be the contents of the file, when it is found, which can then be processed. This will often be a JSON file, which can be processed with graph-ts
helpers ().
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()}}
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 , 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 from an Arweave gateway (). Arweave supports transactions uploaded via Irys (previously Bundlr), and Graph Node can also fetch files based on .
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.tokenIdtoken.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 = tokenIpfsHashTokenMetadataTemplate.create(tokenIpfsHash)}token.updatedAtTimestamp = event.block.timestamptoken.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 !
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.
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 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"
Les sources de données de fichiers nécessitent actuellement des ABI, même si les ABI ne sont pas utilisées (). 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" (). Workaround is to create file data source handlers in a dedicated file.