Passer au contenu principal

The Graph : Résoudre le problème des requêtes de données du Web3

soliditycontrats intelligentsrequêtesthe graphcreate-eth-appreact
Intermédiaire
Markus Waas
soliditydeveloper.com(opens in a new tab)
6 septembre 2020
8 minutes de lecture minute read

Nous allons nous intéresser de plus près à The Graph qui, depuis l'année dernière, fait essentiellement partie intégrante 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, "Transfer failed");
14 totalGamesPlayerWon++;
15 } else {
16 totalGamesPlayerLost++;
17 }
18
19 emit BetPlaced(msg.sender, msg.value, hasWon);
20 }
21}
Afficher tout
Copier

Maintenant, disons que dans notre dApp, nous voulons afficher le total des mises, le total des parties perdues/gagnées et également le mettre à jour 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 considérer l'événement sur Web3(opens in a new tab) comme indiqué sur la droite, mais cela nécessite de traiter pas mal de cas.

1GameContract.events.BetPlaced({
2 fromBlock: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // event fired
6})
7.on('changed', function(event) {
8 // event was removed again
9})
10.on('error', function(error, receipt) {
11 // tx rejected
12});
Afficher tout
Copier

C'est plutôt bien pour notre simple exemple. Mais disons que nous voulons maintenant afficher les quantités de paris perdus / gagnés uniquement pour le joueur actuel. Eh bien, pas de chance, vous devriez 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 se gâter rapidement.

On ne se contente pas d'émettrer une requête

Vous pouvez voir pourquoi ce n'est pas optimal :

  • Ne fonctionne pas pour les contrats déjà déployés.
  • Frais supplémentaires (gaz) pour le stockage de ces valeurs.
  • Nécessite un autre appel pour récupérer les données d'un nœud Ethereum.

Ce n'est pas assez bon

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

(opens in a new tab)

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, il est ainsi incroyablement facile à utiliser côté consommateur. Voici une bonne explication(opens in a new tab) de la façon dont le serveur gère exactement une requête si vous êtes intéressé.

Maintenant, avec cette connaissance, parlons enfin de 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 la requête de données blockchain. Et vous l'aurez peut-être deviné, il utilise GraphQL comme langue 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 Subgraph

La définition de comment indexer les données est appelée subgraph. Il nécessite trois composants :

  1. Manifeste (subgraph.yaml)
  2. Schéma (schema.graphql)
  3. Mapping (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 à prendre en compte comme des appels de fonction ou des blocs
  • les fonctions de mapping étant appelées (voir mapping.ts ci-dessous)

Ici, vous pouvez définir plusieurs contrats et handlers. Une configuration typique a un dossier de sous-graphes à l'intérieur du projet Truffle/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 allez créer un template subgraph.template.yaml et y insérez les adresses basées sur les derniers déploiements. Pour un exemple plus avancé, vous pouvez consulter le répertoire de subgraphs 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: Placing Bets on Ethereum
3repository: - GitHub link -
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 :

  • Bytes
  • ID
  • String (Chaîne de caractères)
  • 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 » pour les paris d'un joueur. Le ! signifie que la valeur ne peut pas être vide. La documentation complète peut être consultée 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 mapping 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 mapping.

Vous devrez définir chaque fonction nommée dans le fichier subgraph.yaml. Ainsi dans notre cas, nous n'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'ID pour cela sera event.transaction.hash.toHex() + "-" + event.logIndex.toString() assurant toujours une valeur 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é du 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 mapping, 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 // create if doesn't exist yet
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 // update array like this
32 let bets = player.bets
33 bets.push(bet.id)
34 player.bets = bets
35
36 player.save()
37}
Afficher tout

L'utiliser 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// See all subgraphs: 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 retournera :

  • combien de fois l'utilisateur actuel a gagné
  • combien de fois l'utilisateur actuel a perdu
  • une liste horodatée 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

Magique

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

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 subgraph. Pour de nombreux projets, vous pouvez trouver des subgraphs existants dans l'explorateur(opens in a new tab).

Le Graph-Explorer

Exécuter votre propre nœud

Sinon, vous pouvez faire tourner votre propre nœud. Documentation 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 sont disponibles ici(opens in a new tab).

Un avenir décentralisé

GraphQL prend également en charge les flux pour les nouveaux événements à venir. Ceux-ci sont pris en charge par The Graph par le biais de Substreams(opens in a new tab) qui est actuellement en version bêta ouverte.

En 2021(opens in a new tab), The Graph a commencé 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 mettront en jeu des jetons The Graph (GRT).

Dernière modification: @MATsxm(opens in a new tab), 18 janvier 2024

Ce tutoriel vous a été utile ?