Ir al contenido principal

The Graph: Corrección de consultas de datos web3

soliditycontratos inteligentesconsultarThe Graphcrear-eth-appreaccionar
Intermedio
Markus Waas
soliditydeveloper.com(opens in a new tab)
6 de septiembre de 2020
8 minuto leído minute read

Esta vez ahondaremos un poco más en The Graph, que esencialmente se convirtió en la pila estándar para el desarrollo de dApps el pasado año. Veamos primero cómo haríamos las cosas de la manera tradicional...

Sin The Graph...

Vamos con un ejemplo simple para fines ilustrativos. A todos nos gustan los juegos, así que imagine un juego simple en el que los usuarios hacen apuestas:

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}
Mostrar todo
Copiar

Digamos en en nuestra dApp queremos mostrar las apuestas totales, las victorias/derrotas totales y también actualizarlo si alguien juega de nuevo. El enfoque sería este:

  1. Obtener totalGamesPlayerWon.
  2. Obtener totalGamesPlayerLost.
  3. Suscribirse a eventos BetPlaced.

Podemos escuchar el evento en Web3(opens in a new tab) como se muestra a la derecha, pero esto requiere manejar algunos casos.

1GameContract.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});
Mostrar todo
Copiar

Ahora bien, esto sigue estando bien para nuestro sencillo ejemplo. Pero digamos que ahora queremos mostrar las cantidades de partidas ganadas o perdidas solo para el jugador actual. Bueno, no tenemos suerte; sería mejor implementar un nuevo contrato que almacene esos valores y permita recuperarlos. Ahora imagine un contrato inteligente y una dapp mucho más complicados; las cosas se pueden descontrolar rápidamente.

Uno no consulta simplemente

Se puede ver que esto no es lo más adecuado:

  • No funciona para contratos ya implementados.
  • Genera costos de gas extra para almacenar dichos valores.
  • Se requiere otra invocación para recuperar los datos para un nodo de Ethereum.

Eso no es lo suficientemente bueno

Veamos ahora una mejor solución.

Déjeme presentarle GraphQL

Primero hablemos de GraphQL, que fue originalmente desarrollado e implementado por Facobook. Puede que esté familiarizado con el tradicional modelo de API tipo Rest. Ahora, imagine que pudiera escribir la consulta para los datos que le interesen exactamente:

API de GraphQL vs. API tipo REST

(opens in a new tab)

Las dos imágenes representan en términos generales la esencia de GraphQL. Con la consulta de la derecha podemos definir exactamente qué datos queremos, así que ahí recibimos todo en una única solicitud y nada más que exactamente lo que necesitamos. Un servidor de GraphQL maneja la obtención de todos los datos requeridos, por lo que es increíblemente fácil de usar desde el lado del consumidor del frontend. Esta es una buena explicación(opens in a new tab) de cómo exactamente el servidor gestiona una consulta si usted está interesado.

Ahora, con ese conocimiento, vayamos finalmente al campo de la cadena de bloques y The Graph.

¿Qué es The Graph?

Una cadena de bloques es una base de datos descentralizada, pero, a diferencia de lo habitual, no tenemos un lenguaje de consulta para esta base de datos. Las soluciones para recuperar datos son complejas o completamente imposibles. The Graph es un protocolo descentralizado destinado a indexar y consultar datos de la cadena de bloques. Y puede que haya adivinado: es usar GraphQL como lenguaje de consulta.

The Graph

Los ejemplos son siempre la mejor manera de entender algo, así que utilicemos The Graph para nuestro ejemplo de GameContract.

Cómo crear un subgraph

La definición de cómo indexar datos se denomina subgraph. Requiere tres componentes:

  1. Manifiesto (subgraph.yaml)
  2. Esquema (schema.graphql)
  3. Mapeo (mapping.ts)

Manifiesto (subgraph.yaml)

El manifiesto es nuestro archivo de configuración y define:

  • qué contratos inteligentes se deben indexar (dirección, red, ABI...)
  • a qué eventos se debe escuchar
  • otros aspectos a escuchar, como llamadas a funciones o bloques
  • las funciones de mapeo invocadas (ver mapping.ts abajo)

Puede definir múltiples contratos y manejadores (handlers) aquí. Una configuración típica tendría una carpeta de subgraphs dentro del proyecto Truffle/Hardhat con su propio repositorio. Luego puede referenciar fácilmente el ABI.

Por razones de conveniencia también puede querer usar una herramienta de plantillas como mustache. Luego creará un subgraph.template.yaml e insertará las direcciones con base en las últimas implementaciones. Para una configuración de ejemplo más avanzada, vea por ejemplo el repositorio de subgraphs de Aave(opens in a new tab).

La documentación completa se puede obtener aquí(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
Mostrar todo

Esquema (schema.graphql)

El esquema es la definición de datos de GraphQL. Le permitirá definir qué entidades existen y sus tipos. Los tipos admitidos de The Graph son:

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

También puede utilizar entidades como tipo para definir relaciones. En nuestro ejemplo definimos una relación de uno a muchos del jugador a las apuestas. El ! significa que el valor no puede estar vacío. La documentación completa se puede consultar aquí(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}
Mostrar todo

Mapeo (mapping.ts)

El archivo de mapeo de The Graph define nuestras funciones que transforman los eventos entrantes en entidades. Está escrito en AssemblblyScript, un subconjunto de Typescript. Esto significa que puede ser compilado en WASM (WebAssembly) para una ejecución más eficiente y portátil del mapeo.

Tendrá que definir cada función mencionada en el archivo subgraph.yaml, así que en nuestro caso necesitamos solo una: handleNewBet. Primero tratamos de cargar la entidad Player desde la dirección del remitente como id. Si no existe, crearemos una nueva entidad y la llenaremos con valores iniciales.

Luego creamos una nueva entidad Bet. El id para esto será event.transaction.hash.toHex() + "-" + event.logIndex.toString(), garantizando siempre un valor único. Usar solo el hash no es suficiente, ya que alguien podría estar llamando a la función placeBet varias veces en una transacción a través de un contrato inteligente.

Por último, podemos actualizar la entidad Player con todos los datos. Los arrays no pueden empujarse directamente, sino que necesitan ser actualizados como se muestra aquí. Utilizamos el id para referenciar la apuesta. .save() es necesario al final para almacenar una entidad.

La documentación completa puede obtenerse aquí: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings(opens in a new tab). También puede añadir salida de registro al archivo de mapeo; consulte aquí(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}
Mostrar todo

Uso en el frontend

Usando algo como Apollo Boost, puede integrar de forma sencilla The Graph en su dapp de React (o Apollo-Vue). Especialmente al usar hooks de React y Apollo, la obtención de datos es muy simple: solo requiere escribir una única consulta de GraphQl en su componente. Una configuración típica podría ser la siguiente:

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)
Mostrar todo

Y ahora podemos escribir por ejemplo una consulta como esta. Esto nos va a traer como resultado

  • cuántas veces ganó el usuario actual
  • cuántas veces perdió el usuario actual
  • una lista de marcas de tiempo con todas sus apuestas anteriores

Todo en una sola solicitud al servidor de 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])
Mostrar todo

Magia

Pero nos estaría faltando una última pieza del rompecabezas y es el servidor. Puede ejecutarlo usted mismo o usar el servicio alojado.

El servidor de The Graph

Graph Explorer, el servicio alojado

La forma más fácil es utilizar el servicio alojado. Siga las instrucciones que figuran aquí(opens in a new tab) para implementar un subgraph. Para muchos proyectos puede encontrar subgraphs existentes en el explorador(opens in a new tab).

The Graph Explorer

Ejecución de tu propio nodo

Alternativemente, puede ejecutar su propio nodo. Consulte la documentación aquí(opens in a new tab). Una razón para hacer esto podría ser usar una red no admitida por el servicio alojado. Las redes actualmente admitidas se pueden encontrar aquí(opens in a new tab).

El futuro descentralizado

GraphQL también soporta streams para eventos entrantes nuevos. Estos son admitidos en The Graph a través de substreams(opens in a new tab) que actualmente están en versión beta abierta.

En 2021(opens in a new tab), The Graph inició su transición a una red descentralizada de indexación. Puede leer más sobre la arquitectura de esta red descentralizada de indexación aquí(opens in a new tab).

Dos aspectos clave son:

  1. Los usuarios pagan a los indexadores por las consultas.
  2. Los indexadores apuestan Graph Tokens (GRT).

Última edición: @lukassim(opens in a new tab), 1 de mayo de 2024

¿Le ha resultado útil este tutorial?