The Graph: query di dati in Web3
Questa volta daremo un'occhiata più da vicino a The Graph, che è essenzialmente diventato parte dello stack standard per sviluppare le dapp nell'ultimo anno. Prima però vediamo come ci comporteremmo tradizionalmente...
Senza The Graph...
Procediamo con un semplice esempio a scopo illustrativo. A chi non piacciono i giochi? Immaginiamo quindi un gioco semplice, dove gli utenti fanno scommesse:
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}Mostra tuttoCopia
Ora, diciamo che nella nostra dapp, vogliamo mostrare le scommesse totali, le partite perse/vinte totali e, inoltre, aggiornarle ogni volta che qualcuno gioca di nuovo. L'approccio sarebbe:
- Recuperare
totalGamesPlayerWon
. - Recuperare
totalGamesPlayerLost
. - Iscriversi agli eventi
BetPlaced
.
Possiamo ascoltare l'evento in Web3(opens in a new tab) come mostrato sulla destra, ma richiederebbe la gestione di alcuni casi.
1GameContract.events.BetPlaced({2 fromBlock: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // evento generato6})7.on('changed', function(event) {8 // evento rimosso nuovamente9})10.on('error', function(error, receipt) {11 // tx rifiutata12});Mostra tuttoCopia
Questo va comunque bene per il nostro esempio semplice. Diciamo però che adesso vogliamo mostrare le quantità di scommesse perse/vinte solo per il giocatore corrente. In questo caso siamo sfortunati, è meglio distribuire un nuovo contratto che memorizzi questi valori e li recuperi. E, ora, immaginiamo un contratto intelligente e una dapp molto più complicati; le cose si complicano in fretta.
È facile capire perché questo non sia ottimale:
- Non funziona per i contratti già distribuiti.
- Costi aggiuntivi del gas per memorizzare tali valori.
- Serve un'altra chiamata per recuperare i dati per un nodo Ethereum.
Cerchiamo allora una soluzione migliore.
Ti presento GraphQL
Parliamo prima di GraphQL, originariamente progettato e implementato da Facebook. Potresti conoscere il modello API Rest tradizionale. Ora immagina di poter scrivere invece una query proprio per i dati che volevi:
Le due immagini catturano quasi perfettamente l'essenza di GraphQL. Con la query sulla destra possiamo definire esattamente i dati che vogliamo, così otteniamo tutto in un'unica richiesta e niente di più di quanto necessario. Un server GraphQL gestisce il recupero di tutti i dati necessari, quindi è incredibilmente facile da usare dal lato frontend client. Questa è una spiegazione efficace(opens in a new tab) e accurata di come il server gestisce una query.
Con queste informazioni, passiamo finalmente allo spazio della blockchain e a The Graph.
Cos'è The Graph?
Una blockchain è un database decentralizzato, ma a differenza di quanto avviene normalmente, in questo caso non abbiamo un linguaggio per interrogare il database. Le soluzioni per recuperare i dati sono complicate o assolutamente impraticabili. The Graph è un protocollo decentralizzato per indicizzare e interrogare i dati della blockchain. E, come forse avrai capito, usa GraphQL come linguaggio di query.
Gli esempi sono sempre la strategia migliore per comprendere qualcosa, quindi usiamo The Graph per il nostro esempio GameContract.
Come creare un Subgraph
La definizione di come indicizzare i dati è detta subgraph. Richiede tre componenti:
- Manifesto (
subgraph.yaml
) - Schema (
schema.graphql
) - Mappatura (
mapping.ts
)
Manifesto (subgraph.yaml
)
Il manifest è il nostro file di configurazione e definisce:
- quali Smart Contract indicizzare (indirizzo, rete, ABI...)
- quali eventi attendere
- altri elementi da attendere, come chiamate a funzioni o blocchi
- le funzioni di mapping chiamate (vedi
mapping.ts
sotto)
Qui puoi definire più contratti e gestori. Una configurazione tipica avrebbe una cartella subgraph nel progetto Hardhat con un proprio repository. A questo punto puoi facilmente fare riferimento all'ABI.
Per motivi di comodità potresti anche usare uno strumento di modelli come mustache. Poi crei un subgraph.template.yaml
e inserisci gli indirizzi in base alle distribuzioni più recenti. Per una configurazione più avanzata, vedi ad esempio il repo del subgraph Aave(opens in a new tab).
E la documentazione completa può essere consultata qui(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.tsMostra tutto
Schema (schema.graphql
)
Lo schema è la definizione dei dati di GraphQL. Ti consentirà di definire quali entità esistono e i loro tipi. I tipi supportati da The Graph sono
- Byte
- ID
- Stringa
- Booleano
- Int
- BigInt
- BigDecimal
Puoi anche usare le entità come tipo per definire le relazioni. Nel nostro esempio definiamo una relazione 1 a tanti dal giocatore alle scommesse. Il punto esclamativo ! significa che il valore non può essere vuoto. La documentazione completa può essere consultata qui(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}Mostra tutto
Mappatura (mapping.ts
)
Il file di mapping in The Graph definisce le nostre funzioni che trasformano gli eventi in ingresso in entità. È scritto in AssemblyScript, un subset di Typescript. Significa che è compilabile in WASM (WebAssembly) per un'esecuzione più portatile ed efficace del mapping.
Devi definire ogni funzione nominata nel file subgraph-yaml
, quindi nel nostro caso ne occorrerà una sola: handleNewBet
. Prima proviamo a caricare l'entità Player dall'indirizzo del mittente come id. Se non esiste, creiamo una nuova entità e la compiliamo con i valori iniziali.
Poi creiamo una nuova entità Bet. L'id sarà event.transaction.hash.toHex() + "-" + event.logIndex.toString()
che assicura sempre un valore unico. Usare solo l'hash non è abbastanza poiché qualcuno potrebbe chiamare la funzione placeBet diverse volte in una transazione tramite uno smart contract.
Infine possiamo aggiornare l'entità Player con tutti i dati. Non è possibile eseguire direttamente il push degli array, bensì devono essere aggiornati come indicato qui. Usiamo l'id per fare riferimento alla scommessa. E occorre aggiungere .save()
alla fine per memorizzare un'entità.
La documentazione completa può essere consultata qui: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings(opens in a new tab). Puoi anche aggiungere l'output di registrazione al file di mapping, vedi qui(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}Mostra tutto
Uso nel frontend
Usando qualcosa come Apollo Boost, puoi facilmente integrare The Graph nella tua dapp di React (o di Apollo-Vue). Specialmente se si utilizzano hook React e Apollo, per recuperare i dati basta scrivere una sola query GraphQI nel componente. Una configurazione tipica potrebbe somigliare a:
1// Vedi tutti i sotto-grafici: 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)Mostra tutto
E ora possiamo scrivere per esempio una query come questa. Otterremo una serie di informazioni:
- quante volte l'utente corrente ha vinto
- quante volte l'utente corrente ha perso
- un elenco di indicatori data/ora con tutte le scommesse precedenti dell'utente corrente
Tutto con una sola richiesta al server 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])Mostra tutto
Ma ci manca l'ultimo pezzo del puzzle: il server. Puoi eseguirlo personalmente o tramite un servizio di hosting.
Il server The Graph
Graph Explorer: il servizio ospitato
Il modo più semplice è usare il servizio di hosting. Segui le istruzioni qui(opens in a new tab) per distribuire un subgraph. Per molti progetti è possibile trovare i sottografi esistenti nell'esploratore(opens in a new tab).
Esecuzione di un nodo personalizzato
In alternativa, può eseguire il suo nodo personale. Documenti qui(opens in a new tab). Un motivo per farlo potrebbe essere l'uso di una rete non supportata dal servizio di hosting. Le reti attualmente supportate possono essere trovate qui(opens in a new tab).
Il futuro decentralizzato
GraphQL supporta i flussi e anche nuovi eventi in ingresso Queste sono supportate su The Graph Substreams(opens in a new tab) che è attualmente in fase open beta.
Nel 2021(opens in a new tab) The Graph ha iniziato la sua transizione per diventare una rete di indicizzazione decentralizzata. Puoi leggere di più sull'architettura di questa rete di indicizzazione decentralizzata qui(opens in a new tab).
Due aspetti chiave sono:
- Gli utenti pagano gli indicizzatori per le query.
- Gli indicizzatori faranno staking di Graph Token (GRT).
Ultima modifica: @sumitvekariya(opens in a new tab), 29 agosto 2024