Pular para o conteúdo principal

The Graph: Consertando a consulta de dados da Web3

solidezsmart contractsconsultandothe Graphcreate-eth-appreact
Intermediário
Markus Waas
soliditydeveloper.com(opens in a new tab)
6 de setembro de 2020
8 minutos de leitura minute read

Dessa vez, daremos uma olhada mais de perto no The Graph que essencialmente se tornou parte do stack padrão para o desenvolvimento de Dapps no último ano. Primeiro, vamos ver como faríamos as coisas da maneira tradicional...

Sem The Graph...

Então vamos começar com um exemplo simples para propósitos ilustrativos. Todos nós gostamos de jogos, então imagine um jogo simples com os usuários fazendo apostas:

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}
Exibir tudo
Copiar

Agora, digamos em nosso dapp, queremos exibir as apostas totais, os jogos perdidos/ganhos e também atualizá-lo sempre que alguém jogar novamente. A abordagem seria:

  1. Busca totalGamesPlayerWon.
  2. Busca totalGamesPlayerWon.
  3. Inscreva-se nos eventos BetPlaced.

Nós podemos escutar evento na Web3(opens in a new tab) como mostrado à direita, mas isso requer manipular alguns casos.

1AmeContract.events.BetPlaced({
2 fromBlock: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // event fired
6})
7.on('changed', function(event) {
8 // event was removed again
9})
10.on('error', function(error, receipt) {
11 // tx rejected
12});
Exibir tudo
Copiar

Por hora está de bom tamanho nosso simples exemplo. Mas digamos que queremos exibir as quantidades das apostas perdidas/ganhas apenas para o jogador atual. Se estivermos sem sorte, você pode implantar um novo contrato que armazena esses valores e busca. E agora imagine um contrato inteligente e um Dapp muito mais complicados. As coisas podem ficar confusas rapidamente.

Não basta uma simples consulta

Você pode ver que isso não é ideal:

  • Não funciona para contratos já implementados.
  • Custos de gas adicionais para armazenar esses valores.
  • Requer outra chamada para obter os dados para um nó Ethereum.

Não é bom o suficiente

Agora vamos ver uma solução melhor.

Deixe-me apresentá-lo ao GraphQL

Primeiro, vamos falar sobre GraphQL, originalmente projetado e implementado pelo Facebook. Você deve estar familiarizado com o modelo tradicional da Rest API. Agora imagine que você poderia escrever uma consulta para exatamente os dados que você queria:

GraphQL API vs. REST API

(opens in a new tab)

As duas imagens capturam praticamente a essência do GraphQL. Com a consulta à direita, podemos definir exactamente quais os dados que queremos, assim aí temos tudo ao alcance e nada mais do que aquilo de que precisamos. Um servidor GraphQL lida com a busca de todos os dados necessários, então é incrivelmente fácil para o lado frontend do consumidor. Esta é uma bela explicação(opens in a new tab) de como exatamente o servidor lida com uma consulta se estiver interessado.

Agora com esse conhecimento, vamos finalmente adentrar o espaço da blockchain e The Graph.

O que é The Graph?

Um blockchain é um banco de dados descentralizado, mas em contraste com o que é geralmente o caso, nós não temos uma linguagem de consulta para esse banco de dados. Soluções para a obtenção de dados são dolorosas ou completamente impossíveis. The Graph é um protocolo descentralizado para indexação e consulta de dados da blockchain. E você pode ter adivinhado, ele está usando GraphQL como idioma de consulta.

The Graph

Os exemplos são sempre os melhores para entender algo, então vamos usar The Graph para o nosso exemplo de GameContract.

Como criar um Subgraph

A definição de como indexar dados é chamada de subgráfico. Requer três componentes:

  1. Manifesto (subgraph.yaml)
  2. Esquema (schema.graphql)
  3. Mapping (mapping.ts)

Manifesto (subgraph.yaml)

O manifesto é nosso arquivo de configuração e define:

  • que contratos inteligentes indexar (endereço, rede, ABI...)
  • quais eventos ouvir
  • outras coisas para ouvir como chamadas de função ou blocos
  • as fnções de mapping sendo chamadas (conferir mapping.ts below)

Aqui você pode definir vários contratos e manipuladores. Uma configuração típica teria uma pasta de subgráfico dentro do projeto Truffle/Hardhat com seu próprio repositório. Então você pode facilmente se referir ao ABI.

Por conveniência você também pode querer usar uma ferramenta modelo tipo um bigode. Em seguida, você cria um subgraph.template.yaml e insere os endereços com base nas mais recentes implantações. Para uma configuração de exemplo mais avançada, veja, por exemplo, o repositório de subgráfico Aave(opens in a new tab).

E a documentação completa pode ser vista aqui(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
Exibir tudo

Esquema (schema.graphql)

O esquema é a definição de dados do GraphQL. Permitirá que você defina quais entidades existem e seus tipos. Tipos suportados do The Graph são

  • Bytes
  • ID
  • String
  • Booleano
  • Int
  • BigInt
  • BigDecimal

Você também pode usar entidades como tipo para definir relações. No nosso exemplo, definimos uma relação entre 1 e muitos entre jogadores e apostas. O ! significa que o valor não pode ser vazio. A documentação completa pode ser vista aqui(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}
Exibir tudo

Mapping (mapping.ts)

O arquivo de mapeamento no The Graph define nossas funções que transformam eventos recebidos em entidades. É escrito em AssemblyScript, um subconjunto de Typescript. Isto significa que pode ser compilado em WASM (WebAssembly) para uma execução mais eficiente e portátil do mapeamento.

Você precisará definir cada função nomeada no arquivo subgraph.yaml, portanto, no nosso caso, precisamos apenas uma: handleNewBet. Primeiro, tentamos carregar a entidade Jogador a partir do endereço do remetente como id. Se não existir, nós criamos uma nova entidade e a preenchemos com os valores iniciais.

Em seguida, criamos uma nova entidade Bet. O ID para isso sempre event.transaction.hash.toHex() + "-" + event.logIndex.toString() garantirá um valor exclusivo. Usar somente o hash não é o suficiente porque alguém pode chamar a função placeBet várias vezes em uma transação através de um contrato inteligente.

Finalmente, nós podemos atualizar a entidade "Player" com todos os dados. Arrays não podem ser empurrados diretamente, mas precisam ser atualizados como mostrado aqui. Usamos o id para fazer referência à aposta. E .save() é necessário no final para armazenar uma entidade.

A documentação completa pode ser vista aqui: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings(opens in a new tab). Você também pode adicionar a saída do log ao arquivo de mapeamento, consultando aqui(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}
Exibir tudo

Usando isso no Frontend

Usando algo como Apollo Boost, você pode facilmente integrar o The Graph em seu React Dapp (ou Apollo-Vue). Especialmente ao usar React hooks e Apollo, buscar dados é tão simples quanto escrever uma única consulta GraphQl no seu componente. Uma típica configuração pode se parecer com isso:

1// See all subgraphs: 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)
Exibir tudo

E agora podemos escrever, por exemplo, uma consulta como esta. Isso vai nos ajudar

  • quantas vezes o usuário atual ganhou
  • quantas vezes o usuário atual perdeu
  • uma lista de horários com todas as suas apostas anteriores

Tudo em um único pedido para o servidor do 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])
Exibir tudo

Magic

Mas precisamos de uma última peça do quebra-cabeças: o servidor. Você pode também executá-lo por conta própria ou usar o serviço hospedado.

Servidor The Graph

Graph Explorer: o serviço hospedado

O jeito mais fácil é usar o serviço hospedado. Siga as instruções aqui(opens in a new tab) para publicar um subgráfico. Para muitos projetos, você pode encontrar subgrafos existentes no explorer(opens in a new tab).

O Graph-Explorer

Executando seu próprio nó

Como alternativa, você pode executar seu próprio nó. Documentação aqui(opens in a new tab). Uma das razões para isso: você pode estar usando uma rede não suportada pelo serviço hospedado. As redes que contam atualmente com suporte podem ser encontradas aqui(opens in a new tab).

O futuro descentralizado

GraphQL também suporta streams para os próximos eventos. Elas tem suporte no grafo por meio de Subfluxos(opens in a new tab), que se encontram atualmente em versão beta de código aberto.

Em 2021(opens in a new tab) O Grafo iniciou sua transição para uma rede de indexação descentralizada. Leia mais sobre a arquitetura dessa rede de indexação descentralizada aqui(opens in a new tab).

Dois aspectos fundamentais são:

  1. Os usuários pagam aos indexadores pelas perguntas.
  2. Os indexadores fazem stake dos Graph Tokens (GRT).

Última edição: @junowoz(opens in a new tab), 19 de janeiro de 2024

Este tutorial foi útil?