The Graph: Corrección de consultas de datos web3
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;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}Mostrar todoCopiar
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:
- Obtener
totalGamesPlayerWon
. - Obtener
totalGamesPlayerLost
. - 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: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // event fired6})7.on('changed', function(event) {8 // event was removed again9})10.on('error', function(error, receipt) {11 // tx rejected12});Mostrar todoCopiar
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.
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.
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:
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.
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:
- Manifiesto (
subgraph.yaml
) - Esquema (
schema.graphql
) - 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 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.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.tsMostrar 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}78type Player @entity {9 id: ID!10 totalPlayedCount: Int11 hasWonCount: Int12 hasLostCount: Int13 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"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}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})56ReactDOM.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 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])Mostrar todo
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).
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:
- Los usuarios pagan a los indexadores por las consultas.
- Los indexadores apuestan Graph Tokens (GRT).
Última edición: @sumitvekariya(opens in a new tab), 29 de agosto de 2024