Hola a todos. Compartiré mi experiencia con The Graph, la cual me ayuda a acceder a los datos de cualquier contrato inteligente en tiempo real, escribiendo un par de líneas de código.
Antes que nada, ¿qué es The Graph?
The Graph es un protocolo descentralizado que permite el acceso a los datos indexados de la blockchain (contrato inteligente) por los “indexers” descentralizados, curados por “curadores” y patrocinados por “delegadores”. Puedes leer sobre el protocolo en thegraph.com
En segundo lugar, The Graph también es una tecnología que ayuda a crear un proceso (llamado “subgráfico”) ETL (Extract Transform Load), el cual recolecciona los datos que necesitas, almacenarlos en la base de datos y hacerlos disponibles a través de GraphQL.
En tercer lugar, no necesitas ejecutar un hardware en específico, el archivo de nodos de blockchain, indexadores, etc. porque puedes usar proveedores existentes en la infraestructura de The Graph y desplegar tus subgráficos en uno de ellos.
Todos estos puntos hacen que los subgráficos sean atractivos para los desarrolladores de la Web3, analistas o investigadores. Así que, vamos al grano.
Cómo empezar con los subgráficos
El código boilerplate de un subgráfico puede ser creado usando la línea de comando de utilidad graph-cli. Para instalarlo ejecuta
npm install -g @graphprotocol/graph-cli
O
yarn global add @graphprotocol/graph-cli
Luego, si ejecutas el comando con los parámetros especificados:
graph init tornado_subgraph /path/to/new/project/tornado --protocol=ethereum --product=hosted-service --allow-simple-name --contract-name TornadoContract --from-contract=0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF --index-events --start-block=17000000 --network=mainnet
También puedes cambiar el parámetro “start-block” al bloque en el que realmente quieres comenzar. Por ejemplo, puede ser el bloque cuando el contrato haya sido desplegado. Puedes ir a Etherscan, busca este contrato con la dirección 0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF y ve a la primera transacción. El número del bloque es 9117720.
Como resultado de la ejecución de este comando, obtendremos la carpeta de este proyecto que pueda ser desplegada en cualquier proveedor de alojamientos subgráficos. Pero, en este caso, los datos serán limitados sólo a las variables emitidas por los eventos.
¿Qué significa “las variables emitidas por eventos”?
Si abres el código de solidity de este contrato inteligente, verás un par de clases de Contrato, las cuales incluyen algunas funciones como depósito o retirada:
function deposit(bytes32 _commitment) external payable nonReentrant {
require(!commitments[_commitment], "The commitment has been submitted");
uint32 insertedIndex = _insert(_commitment);
commitments[_commitment] = true;
_processDeposit();
emit Deposit(_commitment, insertedIndex, block.timestamp);
}
function withdraw(bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund) external payable nonReentrant {
require(_fee <= denomination, "Fee exceeds transfer value");
require(!nullifierHashes[_nullifierHash], "The note has been already spent");
require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one
require(verifier.verifyProof(_proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]), "Invalid withdraw proof");
nullifierHashes[_nullifierHash] = true;
_processWithdraw(_recipient, _relayer, _fee, _refund);
emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
}
Puedes darte cuenta que al final de estas funciones, el evento de Depósito/Retirada está siendo emitido. Esto quiere decir que las variables en los corchetes serán salvadas en los registros que pueden ser fácilmente accedidos por nuestro nuevo proyecto subgráfico (que acabamos de generar). Estos eventos están descritos de la siguiente forma:
event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
Si estas variables son sólo las únicas cosas que necesitas, puedes desplegar el subgráfico en la plataforma de alojamiento de subgráficos y eso es todo. Obtendrás el endpoint GraphQL, el cual puedes llamar con las consultas como:
{
withdrawals(first: 10) {
id
to
nullifierHash
relayer
fee
blockNumber
blockTimestamp
transactionHash
}
}
Este manual y demo explica cómo desplegar un subgráfico en la plataforma Chainstack, pero puedes usar cualquier otra plataforma.
Y ahora, es tiempo de ver qué hemos generado en el comando “graph init”. Para controlar el comportamiento del subgráfico, necesitas trabajar con 3 archivos.
Primero la llamada “subgraph.yaml” se manifiesta. Este código se verá así:
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: TornadoContract
network: mainnet
source:
address: "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF"
abi: TornadoContract
startBlock: 17000000
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Deposit
- Withdrawal
abis:
- name: TornadoContract
file: ./abis/TornadoContract.json
eventHandlers:
- event: Deposit(indexed bytes32,uint32,uint256)
handler: handleDeposit
- event: Withdrawal(address,bytes32,indexed address,uint256)
handler: handleWithdrawal
file: ./src/tornado-contract.ts
Las cosas importantes son: la cadena/red, startBlocks, los nombres de los eventos y las vías a las fuentes. Todo es intuitivamente claro. Puedes dejar este archivo como está.
Segundo, schema.graphql, este archivo describe cómo nuestros datos de los eventos serán almacenados. El archivo por defecto para este contrato inteligente se verá así:
type Deposit @entity(immutable: true) {
id: Bytes!
commitment: Bytes! # bytes32
leafIndex: BigInt! # uint32
timestamp: BigInt! # uint256
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
type Withdrawal @entity(immutable: true) {
id: Bytes!
to: Bytes! # address
nullifierHash: Bytes! # bytes32
relayer: Bytes! # address
fee: BigInt! # uint256
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
Si te gustaría añadir algo más, puedes modificarlo ahora mismo en este archivo. Pero esto sólo es la descripción de cómo almacenar los datos, no cómo obtenerlos. Aquí puedes encontrar una guía explicando los esquemas.
Tercero, src/tornado-contract.ts, este archivo contiene la lógica de cómo obtener los datos desde los eventos (¡y no sólo los eventos!) y cómo ponerlos en las tablas que acabamos de describir. Este archivo se verá así:
import {
Deposit as DepositEvent,
Withdrawal as WithdrawalEvent
} from "../generated/TornadoContract/TornadoContract"
import { Deposit, Withdrawal } from "../generated/schema"
import { Address, BigInt } from "@graphprotocol/graph-ts"
import { TornadoContract } from "../generated/TornadoContract/TornadoContract"
export function handleDeposit(event: DepositEvent): void {
let entity = new Deposit(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
entity.commitment = event.params.commitment
entity.leafIndex = event.params.leafIndex
entity.timestamp = event.params.timestamp
entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash
entity.save()
}
export function handleWithdrawal(event: WithdrawalEvent): void {
let entity = new Withdrawal(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
entity.to = event.params.to
entity.nullifierHash = event.params.nullifierHash
entity.relayer = event.params.relayer
entity.fee = event.params.fee
entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash
entity.save()
}
Como puedes ver, sólo es necesario copiar los datos desde el campo de la variable “evento” en el objeto de Depósito/Retirar en los placeholders apropiados. ¡Todo este código ha sido generado y puede ser desplegado sin ningún cambio!
Pero, ¿qué sucede si necesitamos más información para cada transacción relacionada a Tornado Cash? Por ejemplo, no hay información sobre la dirección que enviará su “dinero” al contrato inteligente de Tornado Cash. ¡Vamos a añadir un par de líneas de código!
Necesitas saber una cosa. Cuando obtienes una variable “evento” y también contiene mucha más información, además de los parámetros emitidos. Las entidades de los datos completos que pueden ser fácilmente extraídos son:
class Event {
address: Address
logIndex: BigInt
transactionLogIndex: BigInt
logType: string | null
block: Block
transaction: Transaction
parameters: Array<EventParam>
receipt: TransactionReceipt | null
}
class Block {
hash: Bytes
parentHash: Bytes
unclesHash: Bytes
author: Address
stateRoot: Bytes
transactionsRoot: Bytes
receiptsRoot: Bytes
number: BigInt
gasUsed: BigInt
gasLimit: BigInt
timestamp: BigInt
difficulty: BigInt
totalDifficulty: BigInt
size: BigInt | null
baseFeePerGas: BigInt | null
}
class Transaction {
hash: Bytes
index: BigInt
from: Address
to: Address | null
value: BigInt
gasLimit: BigInt
gasPrice: BigInt
input: Bytes
nonce: BigInt
}
class TransactionReceipt {
transactionHash: Bytes
transactionIndex: BigInt
blockHash: Bytes
blockNumber: BigInt
cumulativeGasUsed: BigInt
gasUsed: BigInt
contractAddress: Address
logs: Array<Log>
status: BigInt
root: Bytes
logsBloom: Bytes
}
class Log {
address: Address
topics: Array<Bytes>
data: Bytes
blockHash: Bytes
blockNumber: Bytes
transactionHash: Bytes
transactionIndex: BigInt
logIndex: BigInt
transactionLogIndex: BigInt
logType: string
removed: bool | null
}
Quiero añadir “desde” y “valor” desde la entidad de Transacción. Para hacerlo, he añadido dos líneas de códigos en src/tornado-contract.ts:
entity.blockNumber = event.block.number
entity.blockTimestamp = event.block.timestamp
entity.transactionHash = event.transaction.hash
// LÍNEA#1 La dirección que ha desencadenado el evento puede accederse a través de event.transaction.from
entity.from_ = event.transaction.from
// LÍNEA#2 El valor de la transacción en Wei puede accederse a través de event.transaction.value
entity.value_ = event.transaction.value
entity.save()
También tenemos que añadir dos líneas al archivo schema.graphql:
type Deposit @entity(immutable: true) {
id: Bytes!
from_: Bytes! # LINE#1
value_: BigInt! # LINE#2
commitment: Bytes! # bytes32
leafIndex: BigInt! # uint32
timestamp: BigInt! # uint256
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
Sí, es muy fácil. Ahora puedes desplegar tu subgráfico con una línea así:
graph deploy --node https://api.graph-eu.p2pify.com/3a57099edc73524c2807cafeefaa82e1/deploy --ipfs https://api.graph-eu.p2pify.com/3a57099edc36235c2807cafeefaa82e1/ipfs tornado_subgraph
Y los datos de consulta desde la UI así:
{
deposits(first: 10) {
id
commitment
leafIndex
timestamp
transactionHash
from_
value_
}
}
O en la línea de comandos:
curl -g \\
-X POST \\
-H "Content-Type: application/json" \\
-d '{"query":"{deposits(first: 10) { id commitment leafIndex timestamp transactionHash from_ value_}}"}' \\
https://ethereum-mainnet.graph-eu.p2pify.com/3c6e0b8a9c432532a8228b9a98ca1531d/tornado_subgraph
Pero, ¿qué pasa si necesitas guardar una llamada de contrato inteligente que resulta como valor? También es posible ejecutar etc_call desde un subgráfico. Te lo dejaré para que lo pruebes, añadiendo este tutorial llamado “Indexing ERC-20 token balance” el cual también, cubre este aspecto.
Si tienes cualquier pregunta, estoy feliz de responder o discutirlas en el grupo de telegram llamado “Subgraphs Experience Sharing”.
La lista completa de tutoriales está aquí:
- A beginner~s guide to getting started with The Graph
- Deploying a Lido subgraph with Chainstack
- Explaining subgraph schemas
- Debugging subgraphs with a local Graph Node
- Indexing ERC-20 token balance
- Indexing Uniswap data
- Fetching subgraph data using JS
Este artículo es una traducción de Kirill Balakhonov, hecha por Gabriella Martínez. Puedes encontrar el artículo original aquí
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.
Discussion (0)