WEB3DEV Español

Cover image for Cómo acceder a los datos de Tornado Cash fácilmente usando los subgráficos de The Graph
Gabriella Alexandra Martinez Viloria
Gabriella Alexandra Martinez Viloria

Posted on

Cómo acceder a los datos de Tornado Cash fácilmente usando los subgráficos de The Graph

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

O

yarn global add @graphprotocol/graph-cli
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
}
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Y los datos de consulta desde la UI así:

{
deposits(first: 10) {
id
commitment
leafIndex
timestamp
transactionHash
from_
value_
}
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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í:

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)