Passer au contenu principal

The Graph : résoudre les problèmes de requêtes de données Web3

Solidity
contrats intelligents
requêtes
the graph
react
Intermédiaire
Markus Waas
6 septembre 2020
9 minutes de lecture

Cette fois, nous allons nous intéresser de plus près à The Graph, qui est devenu au cours de l'année dernière un élément essentiel du stack standard pour le développement de dapps. Voyons d'abord comment nous ferions les choses de façon traditionnelle...

Sans The Graph...

Prenons donc un exemple simple à titre d'illustration. Nous aimons tous les jeux, alors imaginez un jeu simple avec des utilisateurs qui placent des paris :

1pragma solidity 0.7.1;
2
3contract Game {
4 uint256 totalGamesPlayerWon = 0;
5 uint256 totalGamesPlayerLost = 0;
6 event BetPlaced(address player, uint256 value, bool hasWon);
7
8 function placeBet() external payable {
9 bool hasWon = evaluateBetForPlayer(msg.sender);
10
11 if (hasWon) {
12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');
13 require(success, "Échec du transfert");
14 totalGamesPlayerWon++;
15 } else {
16 totalGamesPlayerLost++;
17 }
18
19 emit BetPlaced(msg.sender, msg.value, hasWon);
20 }
21}
Afficher tout

Maintenant, disons que dans notre dapp, nous voulons afficher le total des paris, le total des parties perdues/gagnées et également mettre à jour ces informations chaque fois que quelqu'un joue à nouveau. L'approche serait :

  1. Récupérer totalGamesPlayerWon.
  2. Récupérer totalGamesPlayerLost.
  3. S'abonner aux événements BetPlaced.

Nous pouvons écouter l'événement dans Web3 (opens in a new tab) comme illustré à droite, mais cela nécessite de gérer un certain nombre de cas.

1GameContract.events.BetPlaced({
2 fromBlock: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // événement déclenché
6})
7.on('changed', function(event) {
8 // l'événement a été retiré
9})
10.on('error', function(error, receipt) {
11 // transaction rejetée
12});
Afficher tout

Cela reste acceptable pour notre exemple simple. Mais disons que nous voulons maintenant afficher les montants des paris perdus/gagnés uniquement pour le joueur actuel. Eh bien, pas de chance, vous feriez mieux de déployer un nouveau contrat qui stocke ces valeurs et les récupère. Et maintenant, imaginez un contrat intelligent et une dapp beaucoup plus complexes, les choses peuvent vite se compliquer.

One Does Not Simply Query

Vous pouvez voir en quoi cette approche n'est pas optimale :

  • Ne fonctionne pas pour les contrats déjà déployés.
  • Coûts de gaz supplémentaires pour stocker ces valeurs.
  • Nécessite un autre appel pour récupérer les données d'un nœud Ethereum.

Thats not good enough

Voyons maintenant une meilleure solution.

Laissez-moi vous présenter GraphQL

Commençons par parler de GraphQL, initialement conçu et implémenté par Facebook. Vous connaissez peut-être le modèle d'API REST traditionnel. Imaginez maintenant que vous puissiez écrire une requête pour obtenir exactement les données que vous voulez :

API GraphQL vs API REST

Ces deux images illustrent bien l'essence de GraphQL. Avec la requête de droite, nous pouvons définir exactement les données que nous voulons. Ainsi, nous récupérons tout en une seule requête et rien de plus que ce dont nous avons exactement besoin. Un serveur GraphQL gère la récupération de toutes les données requises, ce qui le rend incroyablement facile à utiliser côté frontend. Voici une bonne explication (opens in a new tab) de la manière exacte dont le serveur gère une requête, si le sujet vous intéresse.

Maintenant, avec ces connaissances, parlons enfin de la blockchain et de The Graph.

Qu'est-ce que The Graph ?

Une blockchain est une base de données décentralisée, mais contrairement à ce qui est généralement le cas, nous n'avons pas de langage de requête pour cette base de données. Les solutions pour récupérer les données sont pénibles ou totalement impossibles. The Graph est un protocole décentralisé pour l'indexation et l'interrogation des données de la blockchain. Et vous l'aurez peut-être deviné, il utilise GraphQL comme langage de requête.

The Graph

Rien de tel que quelques exemples pour comprendre une chose, alors utilisons The Graph pour notre exemple de GameContract.

Comment créer un sous-graphe

La définition de la manière d'indexer les données s'appelle un sous-graphe. Il nécessite trois composants :

  1. Manifeste (subgraph.yaml)
  2. Schéma (schema.graphql)
  3. Mappage (mapping.ts)

Manifeste (subgraph.yaml)

Le manifeste est notre fichier de configuration et définit :

  • quels contrats intelligents indexer (adresse, réseau, ABI...)
  • quels événements écouter
  • d'autres éléments à écouter, comme les appels de fonction ou les blocs
  • les fonctions de mappage qui sont appelées (voir mapping.ts ci-dessous)

Ici, vous pouvez définir plusieurs contrats et handlers. Une configuration typique aurait un dossier de sous-graphe à l'intérieur du projet Hardhat avec son propre dépôt. Ensuite, vous pouvez facilement référencer l'ABI.

Pour des raisons de commodité, vous pouvez également utiliser un outil de template comme Mustache. Ensuite, vous créez un fichier subgraph.template.yaml et y insérez les adresses en fonction des derniers déploiements. Pour un exemple de configuration plus avancé, voir par exemple le dépôt du sous-graphe Aave (opens in a new tab).

Et la documentation complète peut être consultée ici (opens in a new tab).

1specVersion: 0.0.1
2description: Placer des paris sur Ethereum
3repository: - Lien GitHub -
4schema:
5 file: ./schema.graphql
6dataSources:
7 - kind: ethereum/contract
8 name: GameContract
9 network: mainnet
10 source:
11 address: '0x2E6454...cf77eC'
12 abi: GameContract
13 startBlock: 6175244
14 mapping:
15 kind: ethereum/events
16 apiVersion: 0.0.1
17 language: wasm/assemblyscript
18 entities:
19 - GameContract
20 abis:
21 - name: GameContract
22 file: ../build/contracts/GameContract.json
23 eventHandlers:
24 - event: PlacedBet(address,uint256,bool)
25 handler: handleNewBet
26 file: ./src/mapping.ts
Afficher tout

Schéma (schema.graphql)

Le schéma est la définition des données GraphQL. Il vous permettra de définir quelles entités existent et leurs types. Les types pris en charge par The Graph sont :

  • Octets
  • ID
  • String
  • Boolean
  • Int
  • BigInt
  • BigDecimal

Vous pouvez également utiliser des entités comme type pour définir des relations. Dans notre exemple, nous définissons une relation « un à plusieurs » entre un joueur et ses paris. Le ! signifie que la valeur ne peut pas être vide. La documentation complète est disponible ici (opens in a new tab).

1type Bet @entity {
2 id: ID!
3 player: Player!
4 playerHasWon: Boolean!
5 time: Int!
6}
7
8type Player @entity {
9 id: ID!
10 totalPlayedCount: Int
11 hasWonCount: Int
12 hasLostCount: Int
13 bets: [Bet]!
14}
Afficher tout

Mappage (mapping.ts)

Le fichier de mappage dans The Graph définit nos fonctions qui transforment les événements entrants en entités. Il est écrit en AssemblyScript, un sous-ensemble de Typescript. Cela signifie qu'il peut être compilé en WASM (WebAssembly) pour une exécution plus efficace et portable du mappage.

Vous devrez définir chaque fonction nommée dans le fichier subgraph.yaml, donc dans notre cas, nous n'en avons besoin que d'une seule : handleNewBet. Nous essayons d'abord de charger l'entité Player depuis l'adresse de l'expéditeur en tant qu'identifiant. Si elle n'existe pas, nous créons une nouvelle entité et la remplissons avec des valeurs de départ.

Puis nous créons une nouvelle entité Bet. L'identifiant pour cela sera event.transaction.hash.toHex() + "-" + event.logIndex.toString() ce qui garantit une valeur toujours unique. Utiliser uniquement le hachage n'est pas suffisant, car quelqu'un peut appeler la fonction placeBet plusieurs fois dans une transaction via un contrat intelligent.

Enfin, nous pouvons mettre à jour l'entité Player avec toutes les données. Les tableaux ne peuvent pas être poussés directement, mais doivent être mis à jour comme indiqué ici. Nous utilisons l'ID pour référencer le pari. Et .save() est requis à la fin pour stocker une entité.

La documentation complète est disponible ici : https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings (opens in a new tab). Vous pouvez également ajouter une sortie de journalisation au fichier de mappage, voir ici (opens in a new tab).

1import { Bet, Player } from "../generated/schema"
2import { PlacedBet } from "../generated/GameContract/GameContract"
3
4export function handleNewBet(event: PlacedBet): void {
5 let player = Player.load(event.transaction.from.toHex())
6
7 if (player == null) {
8 // créer si elle n'existe pas encore
9 player = new Player(event.transaction.from.toHex())
10 player.bets = new Array<string>(0)
11 player.totalPlayedCount = 0
12 player.hasWonCount = 0
13 player.hasLostCount = 0
14 }
15
16 let bet = new Bet(
17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()
18 )
19 bet.player = player.id
20 bet.playerHasWon = event.params.hasWon
21 bet.time = event.block.timestamp
22 bet.save()
23
24 player.totalPlayedCount++
25 if (event.params.hasWon) {
26 player.hasWonCount++
27 } else {
28 player.hasLostCount++
29 }
30
31 // mettre à jour le tableau comme ceci
32 let bets = player.bets
33 bets.push(bet.id)
34 player.bets = bets
35
36 player.save()
37}
Afficher tout

Utilisation dans le frontend

En utilisant quelque chose comme Apollo Boost, vous pouvez facilement intégrer The Graph dans votre dapp React (ou Apollo-Vue). Surtout lorsque vous utilisez des hooks React et Apollo, récupérer des données est aussi simple que d'écrire une requête GraphQL dans votre composant. Une configuration type pourrait ressembler à ceci :

1// Voir tous les sous-graphes : https://thegraph.com/explorer/
2const client = new ApolloClient({
3 uri: "{{ subgraphUrl }}",
4})
5
6ReactDOM.render(
7 <ApolloProvider client={client}>
8 <App />
9 </ApolloProvider>,
10 document.getElementById("root")
11)
Afficher tout

Et maintenant, nous pouvons écrire par exemple une requête comme celle-ci. Elle nous récupérera :

  • combien de fois l'utilisateur actuel a gagné
  • combien de fois l'utilisateur actuel a perdu
  • une liste d'horodatages de tous ses paris précédents

Le tout en une seule requête au serveur GraphQL.

1const myGraphQlQuery = gql`
2 players(where: { id: $currentUser }) {
3 totalPlayedCount
4 hasWonCount
5 hasLostCount
6 bets {
7 time
8 }
9 }
10`
11
12const { loading, error, data } = useQuery(myGraphQlQuery)
13
14React.useEffect(() => {
15 if (!loading && !error && data) {
16 console.log({ data })
17 }
18}, [loading, error, data])
Afficher tout

Magie

Mais il nous manque une dernière pièce du puzzle et c'est le serveur. Vous pouvez soit l'exécuter vous-même, soit utiliser le service hébergé.

Le serveur The Graph

Graph Explorer : le service hébergé

Le moyen le plus simple est d'utiliser le service hébergé. Suivez les instructions ici (opens in a new tab) pour déployer un sous-graphe. Pour de nombreux projets, vous pouvez trouver des sous-graphes existants dans l'explorateur (opens in a new tab).

The Graph-Explorer

Exécuter votre propre nœud

Sinon, vous pouvez faire tourner votre propre nœud. La documentation se trouve ici (opens in a new tab). Une raison d'agir de la sorte peut être d'utiliser un réseau qui n'est pas pris en charge par le service hébergé. Les réseaux actuellement pris en charge se trouvent ici (opens in a new tab).

L'avenir décentralisé

GraphQL prend également en charge les flux pour les nouveaux événements entrants. Ces derniers sont pris en charge sur The Graph par le biais de Substreams (opens in a new tab), qui sont actuellement en bêta ouverte.

En 2021 (opens in a new tab), The Graph a entamé sa transition vers un réseau d'indexation décentralisé. Vous pouvez en savoir plus sur l'architecture de ce réseau d'indexation décentralisé ici (opens in a new tab).

Les deux aspects clés sont :

  1. Les utilisateurs paient les indexeurs pour les requêtes.
  2. Les indexeurs mettent en jeu des jetons Graph (GRT).

Dernière mise à jour de la page : 26 février 2026

Ce tutoriel vous a été utile ?