The Graph : Résoudre le problème des requêtes de données du Web3
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;23contract Game {4 uint256 totalGamesPlayerWon = 0;5 uint256 totalGamesPlayerLost = 0;6 event BetPlaced(address player, uint256 value, bool hasWon);78 function placeBet() external payable {9 bool hasWon = evaluateBetForPlayer(msg.sender);1011 if (hasWon) {12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');13 require(success, "Transfer failed");14 totalGamesPlayerWon++;15 } else {16 totalGamesPlayerLost++;17 }1819 emit BetPlaced(msg.sender, msg.value, hasWon);20 }21}Afficher toutCopier
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 :
- Récupérer
totalGamesPlayerWon
. - Récupérer
totalGamesPlayerLost
. - 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: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // event fired6})7.on('changed', function(event) {8 // event was removed again9})10.on('error', function(error, receipt) {11 // tx rejected12});Afficher toutCopier
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.
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.
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 :
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.
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 :
- Manifeste (
subgraph.yaml
) - Schéma (
schema.graphql
) - 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 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.12description: Placing Bets on Ethereum3repository: - GitHub link -4schema:5 file: ./schema.graphql6dataSources:7 - kind: ethereum/contract8 name: GameContract9 network: mainnet10 source:11 address: '0x2E6454...cf77eC'12 abi: GameContract13 startBlock: 617524414 mapping:15 kind: ethereum/events16 apiVersion: 0.0.117 language: wasm/assemblyscript18 entities:19 - GameContract20 abis:21 - name: GameContract22 file: ../build/contracts/GameContract.json23 eventHandlers:24 - event: PlacedBet(address,uint256,bool)25 handler: handleNewBet26 file: ./src/mapping.tsAfficher 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}78type Player @entity {9 id: ID!10 totalPlayedCount: Int11 hasWonCount: Int12 hasLostCount: Int13 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"34export function handleNewBet(event: PlacedBet): void {5 let player = Player.load(event.transaction.from.toHex())67 if (player == null) {8 // create if doesn't exist yet9 player = new Player(event.transaction.from.toHex())10 player.bets = new Array<string>(0)11 player.totalPlayedCount = 012 player.hasWonCount = 013 player.hasLostCount = 014 }1516 let bet = new Bet(17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()18 )19 bet.player = player.id20 bet.playerHasWon = event.params.hasWon21 bet.time = event.block.timestamp22 bet.save()2324 player.totalPlayedCount++25 if (event.params.hasWon) {26 player.hasWonCount++27 } else {28 player.hasLostCount++29 }3031 // update array like this32 let bets = player.bets33 bets.push(bet.id)34 player.bets = bets3536 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})56ReactDOM.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 totalPlayedCount4 hasWonCount5 hasLostCount6 bets {7 time8 }9 }10`1112const { loading, error, data } = useQuery(myGraphQlQuery)1314React.useEffect(() => {15 if (!loading && !error && data) {16 console.log({ data })17 }18}, [loading, error, data])Afficher tout
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).
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 :
- Les utilisateurs paient les indexeurs pour les requêtes.
- Les indexeurs mettront en jeu des jetons The Graph (GRT).
Dernière modification: @sumitvekariya(opens in a new tab), 29 août 2024