Vai al contenuto principale

The Graph: query di dati in Web3

soliditycontratti intelligentiquerythe graphcreate-eth-appreact
Intermedio
Markus Waas
soliditydeveloper.com(opens in a new tab)
6 settembre 2020
8 minuti letti minute read

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;
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}
Mostra tutto
Copia

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:

  1. Recuperare totalGamesPlayerWon.
  2. Recuperare totalGamesPlayerLost.
  3. 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: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // evento generato
6})
7.on('changed', function(event) {
8 // evento rimosso nuovamente
9})
10.on('error', function(error, receipt) {
11 // tx rifiutata
12});
Mostra tutto
Copia

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.

Non basta eseguire Query

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

Non è sufficiente

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:

API GraphQL API e API REST

(opens in a new tab)

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.

The Graph

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:

  1. Manifesto (subgraph.yaml)
  2. Schema (schema.graphql)
  3. 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 Truffle/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.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
Mostra 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}
7
8type Player @entity {
9 id: ID!
10 totalPlayedCount: Int
11 hasWonCount: Int
12 hasLostCount: Int
13 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"
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}
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})
5
6ReactDOM.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 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])
Mostra tutto

Magic

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

Explorer di The Graph

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:

  1. Gli utenti pagano gli indicizzatori per le query.
  2. Gli indicizzatori faranno staking di Graph Token (GRT).

Ultima modifica: @lukassim(opens in a new tab), 7 maggio 2024

Questo tutorial è stato utile?