The Graph: Remedierea interogării datelor Web3
De data aceasta vom arunca o privire mai atentă la „The Graph” care, în esență, a devenit parte din stiva standard pentru dezvoltarea aplicațiilor dapp în ultimul an. Să vedem mai întâi cum am face lucrurile în mod tradițional...
Fără „The Graph”...
Vom folosi un exemplu simplu în scopul de ilustrare. Tuturor ne plac jocurile, deci să ne imaginăm un joc simplu, cu utilizatori care plasează pariuri:
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, "Transferul nu a reușit");14 totalGamesPlayerWon++;15 } else {16 totalGamesPlayerLost++;17 }1819 emit BetPlaced(msg.sender, msg.value, hasWon);20 }21}22Afișează totCopiere
Acum să spunem că în aplicația noastră dapp, vrem să afișăm totalul jocurilor pierdute/câștigate și, de asemenea, să le actualizăm ori de câte ori cineva joacă din nou. Abordarea ar fi:
- Preia
totalGamesPlayerWon
. - Preia
totalGamesPlayerLost
. - Abonează-te la
evenimente BetPlaced
.
Putem asculta evenimentul în Web3 așa cum se arată în dreapta, dar necesită manipularea destul de multor cazuri.
1GameContract.events.BetPlaced({2 fromBlock: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // eveniment declanșat6})7.on('changed', function(event) {8 // evenimentul a fost eliminat din nou9})10.on('error', function(error, receipt) {11 // tx respins12});13Afișează totCopiere
Acum, acest lucru este încă destul de bun pentru exemplul nostru simplu. Dar să presupunem că vrem să afișăm numai câte pariuri a pierdut/câștigat doar jucătorul actual. Ei bine, nu avem noroc, ar fi bine să implementezi un nou contract care stochează valorile respective și să le afișeze. Și acum să ne imaginăm un contract inteligent și o aplicație dapp mult mai complicate, lucrurile pot deveni repede foarte confuze.
Putem vedea de ce acest lucru nu este optim:
- Nu funcționează pentru contracte deja implementate.
- Costuri suplimentare de gaz pentru stocarea acestor valori.
- Necesită un alt apel pentru a prelua datele pentru un nod Ethereum.
Acum să analizăm o soluție mai bună.
Permite-mi să-ți prezint GraphQL
În primul rând, să vorbim despre GraphQL, inițial proiectat și implementat de Facebook. Este posibil să fii familiarizat cu modelul tradițional API Rest. Acum imaginează-ți că ai putea scrie o interogare pentru exact datele pe care le-ai dorit:
Cele două imagini surprind destul de mult esența GraphQL. Cu interogarea din dreapta putem defini exact ce date vrem, astfel încât vom obține totul într-o singură cerere și nimic mai mult decât exact ceea ce avem nevoie. Un server GraphQL se ocupă de preluarea tuturor datelor necesare, astfel încât este incredibil de ușor de partea front-end a consumatorului. Aceasta este o explicație frumoasă a modului în care serverul gestionează o interogare dacă ești interesat.
Acum, cu aceste cunoștințe, să sărim în cele din urmă în spațiul blockchain și „The Graph”.
Ce este „The Graph”?
Un blockchain este o bază de date descentralizată, dar spre deosebire de ceea ce se întâmplă de obicei, nu avem un limbaj de interogare pentru această bază de date. Soluțiile pentru obținerea datelor sunt dureroase sau complet imposibile. „The Graph” este un protocol descentralizat pentru indexarea și interogarea datelor blockchain. Și s-ar putea să fi ghicit, se utilizează GraphQL ca limbaj de interogare.
Exemplele sunt cele mai bune ca să înțelegem ceva, așa că hai să folosim „The Graph” pentru exemplul nostru „GameContract”
Cum să creăm un Subgraph
Definiția modului de indexare a datelor se numește „subgraph”. Acesta necesită trei componente:
- Manifestul (subgraph.yaml)
- Schema (schema.graphql)
- Maparea (mapping.ts)
Manifest (subgraph.yaml)
Manifestul este fișierul nostru de configurare și definește:
- ce contracte inteligente să indexeze (adresa, rețea, ABI...)
- ce evenimente să asculte
- alte lucruri de ascultat, cum ar fi apeluri de funcții sau blocuri
- funcțiile de mapare apelate (vezi mapping.ts mai jos)
Aici poți defini mai multe contracte și manipulatoare. O configurare tipică ar avea un folder subgraph în proiectul Truffle/Hardhat cu propriul depozit. Apoi, poți face cu ușurință referire la ABI.
Din motive de confort, probabil ai vrea să utilizezi un instrument șablon, cum ar fi „mustache”. Apoi să creezi un subgraph.template.yaml și să introduci adresele pe baza celor mai recente implementări. Pentru un exemplu de configurare mai avansat, consultă depozitul Aave subgraph.
Și documentația completă poate fi văzută aici: https://thegraph.com/docs/define-a-subgraph#the-subgraph-manifest.
1specVersion: 0.0.12description: Plasarea pariurilor pe 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.ts27Afișează tot
Schema (schema.graphql)
Schema este definiția datelor GraphQL. Aceasta îți va permite să definești ce entități există și tipurile lor. Tipurile acceptate din „The Graph” sunt
- Bytes
- ID
- String
- Boolean
- Int
- BigInt
- BigDecimal
De asemenea, poți utiliza entitățile ca „tip” pentru a defini relațiile. În exemplul nostru definim o relație 1-la-mulți de la jucător la pariuri. Simbolul ! înseamnă că valoarea nu poate fi goală. Documentația completă poate fi consultată aici: https://thegraph.com/docs/define-a-subgraph#the-graphql-schema.
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}15Afișează tot
Mapping (mapping.ts)
Fișierul de mapare din „The Graph” definește funcțiile noastre care transformă evenimentele primite în entități. Este scris în AssemblyScript, un subset de Typescript. Deci poate fi compilat în WASM (WebAssembly) pentru executarea mai eficientă și portabilă a mapării.
Va trebui să definești fiecare funcție numită în fișierul subgraph.yaml, deci în cazul nostru avem nevoie doar de una: handleNewBet. Mai întâi încercăm să încărcăm entitatea Player din adresa expeditorului ca id. Dacă nu există, creăm o entitate nouă și o completăm cu valorile inițiale.
Apoi vom crea o nouă entitate Bet. Id-ul pentru aceasta va fi event.transaction.hash.toHex() + "-" + event.logIndex.toString() asigurând întotdeauna o valoare unică. Numai utilizarea hash-ului nu este suficientă deoarece cineva poate apela funcția „placeBet” de mai multe ori într-o singură tranzacție printr-un contract inteligent.
În cele din urmă, putem actualiza entitatea „Player” cu toate datele. Matricele nu pot fi împinse direct, dar trebuie actualizate așa cum se arată aici. Folosim id-ul pentru a face referire la pariu. Și ".save()" este necesară la sfârșit pentru a stoca o entitate.
Documentația completă poate fi consultată aici: https://thegraph.com/docs/define-a-subgraph#writing-mappings. De asemenea, poți adăuga rezultatele jurnalizării în fișierul de mapare, consultă aici.
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 // creare dacă nu există încă9 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 // actualizează matrice ca aceasta32 let bets = player.bets33 bets.push(bet.id)34 player.bets = bets3536 player.save()37}38Afișează tot
Folosindu-l în Front-end
Folosind ceva de genul Apollo Boost, poți integra cu ușurință „The Graph” în aplicația ta React Dapp (sau Apollo Vue). Mai ales atunci când utilizezi hooks React și Apollo, preluarea datelor este la fel de simplă ca scrierea unei singure interogări GrafQl în componenta ta. O configurare tipică ar putea arăta astfel:
1// Vezi toate subgraph-urile: 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)12Afișează tot
Și acum putem scrie, de exemplu, o interogare ca aceasta. Aceasta ne va aduce
- de câte ori a câștigat utilizatorul curent
- de câte ori a pierdut utilizatorul curent
- o listă a marcajelor temporale cu toate pariurile sale anterioare
Toate într-o singură cerere către GraphQL server.
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])19Afișează tot
Dar ne lipsește o ultimă piesă din puzzle și acesta este serverul. Îl poți rula singur sau poți utiliza serviciul găzduit.
Serverul „The Graph”
Exploratorul Graph: Serviciul găzduit
Cel mai simplu mod de a utiliza serviciul găzduit. Urmează instrucțiunile aici pentru a implementa un subgraph. Pentru multe proiecte, poți găsi de fapt, „subgraph”-uri deja realizate în explorer la https://thegraph.com/explorer/.
Rularea propriului tău nod
Alternativ, poți rula propriul nod: https://github.com/graphprotocol/graph-node#quick-start. Un motiv pentru a face acest lucru poate fi utilizarea unei rețele care nu este acceptată de serviciul găzduit. În prezent, sunt acceptate rețeaua principală, Kovan, Rinkeby, Ropsten, Goerli, PoA-Core, xDAI și Sokol.
Viitorul descentralizat
GraphQL suportă fluxuri și pentru evenimente nou primite. Acest lucru nu este încă pe deplin susținut de „The Graph”, dar va fi lansat în curând.
Un aspect care lipsește este totuși descentralizarea. „The Graph” are planuri viitoare pentru a deveni în cele din urmă un protocol complet descentralizat. Acestea sunt două articole mari care explică planul în detaliu:
- https://thegraph.com/blog/the-graph-network-in-depth-part-1
- https://thegraph.com/blog/the-graph-network-in-depth-part-2
Două aspecte esențiale sunt:
- Utilizatorii vor plăti indexurile pentru interogări.
- Indexurile vor miza tokenuri Graph (GRT).