Vai al contenuto principale

The Graph: Risolvere l'interrogazione dei dati Web3

Solidity
contratti intelligenti
interrogazione
the graph
React
Intermedio
Markus Waas
6 settembre 2020
8 minuti di lettura

Questa volta daremo un'occhiata più da vicino a The Graph, che nell'ultimo anno è diventato essenzialmente parte dello stack standard per lo sviluppo di dApp. Vediamo prima come faremmo le cose nel modo tradizionale...

Senza The Graph...

Quindi procediamo con un semplice esempio a scopo illustrativo. A tutti piacciono i giochi, quindi immagina un semplice gioco con utenti che piazzano 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}

Ora supponiamo che nella nostra dApp vogliamo mostrare le scommesse totali, il totale delle partite perse/vinte e anche aggiornarlo 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 a destra, ma richiede la gestione di un bel po' di casi.

1GameContract.events.BetPlaced({
2 fromBlock: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // evento emesso
6})
7.on('changed', function(event) {
8 // evento rimosso di nuovo
9})
10.on('error', function(error, receipt) {
11 // tx rifiutata
12});

Ora, questo va ancora in qualche modo bene per il nostro semplice esempio. Ma supponiamo di voler mostrare ora gli importi delle scommesse perse/vinte solo per il giocatore corrente. Beh, siamo sfortunati, faresti meglio a distribuire un nuovo contratto intelligente che memorizzi quei valori e li recuperi. E ora immagina un contratto intelligente e una dApp molto più complicati, le cose possono diventare rapidamente disordinate.

One Does Not Simply Query

Puoi vedere come questo non sia ottimale:

  • Non funziona per i contratti già distribuiti.
  • Costi del gas extra per memorizzare quei valori.
  • Richiede un'altra chiamata per recuperare i dati per un nodo di Ethereum.

Thats not good enough

Ora diamo un'occhiata a una soluzione migliore.

Lascia che ti presenti GraphQL

Prima parliamo di GraphQL, originariamente progettato e implementato da Facebook. Potresti avere familiarità con il modello tradizionale delle API REST. Ora immagina invece di poter scrivere un'interrogazione esattamente per i dati che desideri:

GraphQL API vs. REST API

Dimostrazione animata di un'interrogazione GraphQL nel playground di The Graph

Le due immagini catturano praticamente l'essenza di GraphQL. Con l'interrogazione a destra possiamo definire esattamente quali dati vogliamo, in modo da ottenere tutto in una singola richiesta e niente di più di ciò di cui abbiamo esattamente bisogno. Un server GraphQL gestisce il recupero di tutti i dati richiesti, quindi è incredibilmente facile da usare per il lato consumatore del frontend. Questa è una bella spiegazione (opens in a new tab) di come esattamente il server gestisce un'interrogazione, se sei interessato.

Ora, con questa conoscenza, tuffiamoci finalmente nello spazio della blockchain e in The Graph.

Cos'è The Graph?

Una blockchain è un database decentralizzato, ma contrariamente a quanto accade di solito, non abbiamo un linguaggio di interrogazione per questo database. Le soluzioni per il recupero dei dati sono dolorose o completamente impossibili. The Graph è un protocollo decentralizzato per l'indicizzazione e l'interrogazione dei dati della blockchain. E come potresti aver intuito, utilizza GraphQL come linguaggio di interrogazione.

The Graph

Gli esempi sono sempre il modo migliore per capire qualcosa, quindi usiamo The Graph per il nostro esempio GameContract.

Come creare un Subgraph

La definizione di come indicizzare i dati è chiamata subgraph (sottografo). Richiede tre componenti:

  1. Manifesto (subgraph.yaml)
  2. Schema (schema.graphql)
  3. Mappatura (mapping.ts)

Manifesto (subgraph.yaml)

Il manifesto è il nostro file di configurazione e definisce:

  • quali contratti intelligenti indicizzare (indirizzo, rete, ABI...)
  • quali eventi ascoltare
  • altre cose da ascoltare come chiamate di funzione o blocchi
  • le funzioni di mappatura che vengono chiamate (vedi mapping.ts di seguito)

Puoi definire più contratti e gestori qui. Una configurazione tipica avrebbe una cartella subgraph all'interno del progetto Hardhat con il proprio repository. Quindi puoi facilmente fare riferimento all'ABI.

Per motivi di comodità potresti anche voler usare uno strumento di template come mustache. Quindi crei un subgraph.template.yaml e inserisci gli indirizzi in base alle ultime distribuzioni. Per un esempio di configurazione più avanzato, vedi ad esempio il repository del subgraph di 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

Schema (schema.graphql)

Lo schema è la definizione dei dati GraphQL. Ti permetterà di definire quali entità esistono e i loro tipi. I tipi supportati da The Graph sono

  • Bytes
  • ID
  • String
  • Boolean
  • Int
  • BigInt
  • BigDecimal

Puoi anche usare le entità come tipo per definire le relazioni. Nel nostro esempio definiamo una relazione 1-a-molti dal giocatore alle scommesse. Il ! 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}

Mappatura (mapping.ts)

Il file di mappatura in The Graph definisce le nostre funzioni che trasformano gli eventi in arrivo in entità. È scritto in AssemblyScript, un sottoinsieme di TypeScript. Questo significa che può essere compilato in WASM (WebAssembly) per un'esecuzione più efficiente e portabile della mappatura.

Dovrai definire ogni funzione nominata nel file subgraph.yaml, quindi nel nostro caso ne serve solo una: handleNewBet. Per prima cosa cerchiamo di caricare l'entità Player dall'indirizzo del mittente come id. Se non esiste, creiamo una nuova entità e la riempiamo con i valori iniziali.

Quindi creiamo una nuova entità Bet. L'id per questa sarà event.transaction.hash.toHex() + "-" + event.logIndex.toString() garantendo sempre un valore univoco. Usare solo l'hash non è sufficiente poiché qualcuno potrebbe chiamare la funzione placeBet più volte in una singola transazione tramite un contratto intelligente.

Infine possiamo aggiornare l'entità Player con tutti i dati. Gli array non possono essere inseriti direttamente, ma devono essere aggiornati come mostrato qui. Usiamo l'id per fare riferimento alla scommessa. E .save() è richiesto 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 mappatura, 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 // crea se non esiste ancora
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 // aggiorna l'array in questo modo
32 let bets = player.bets
33 bets.push(bet.id)
34 player.bets = bets
35
36 player.save()
37}

Usarlo nel Frontend

Usando qualcosa come Apollo Boost, puoi facilmente integrare The Graph nella tua dApp React (o Apollo-Vue). Specialmente quando si usano gli hook di React e Apollo, recuperare i dati è semplice come scrivere una singola interrogazione GraphQL nel tuo componente. Una configurazione tipica potrebbe apparire così:

1// Vedi tutti i sottografi: 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)

E ora possiamo scrivere ad esempio un'interrogazione come questa. Questo ci recupererà

  • quante volte l'utente corrente ha vinto
  • quante volte l'utente corrente ha perso
  • un elenco di timestamp con tutte le sue scommesse precedenti

Tutto in una singola 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])

Magic

Ma ci manca un ultimo pezzo del puzzle e questo è il server. Puoi eseguirlo tu stesso o usare il servizio ospitato.

Il server The Graph

Graph Explorer: Il servizio ospitato

Il modo più semplice è usare il servizio ospitato. Segui le istruzioni qui (opens in a new tab) per distribuire un subgraph. Per molti progetti puoi effettivamente trovare subgraph esistenti nell'esploratore (opens in a new tab).

The Graph-Explorer

Eseguire il proprio nodo

In alternativa puoi eseguire il tuo nodo. Documentazione qui (opens in a new tab). Un motivo per farlo potrebbe essere l'utilizzo di una rete che non è supportata dal servizio ospitato. Le reti attualmente supportate possono essere trovate qui (opens in a new tab).

Il futuro decentralizzato

GraphQL supporta anche i flussi (stream) per i nuovi eventi in arrivo. Questi sono supportati su The Graph tramite i Substreams (opens in a new tab) che sono attualmente in open beta.

Nel 2021 (opens in a new tab) The Graph ha iniziato la sua transizione verso 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 interrogazioni.
  2. Gli indicizzatori mettono in stake i Graph Token (GRT).

Ultimo aggiornamento della pagina: 3 marzo 2026

Questo tutorial è stato utile?