WEB3DEV

Cover image for Como acessar facilmente os dados do Tornado Cash usando os subgraphs do The Graph
Adriano P. Araujo
Adriano P. Araujo

Posted on

Como acessar facilmente os dados do Tornado Cash usando os subgraphs do The Graph

Olá a todos, vou compartilhar minha experiência com o The Graph, que me ajuda a acessar os dados de qualquer contrato inteligente em tempo real escrevendo algumas linhas de código.

Primeiro, o que é o The Graph?

O The Graph é um protocolo descentralizado que disponibiliza o acesso aos dados da blockchain (contratos inteligentes) indexados pelos "indexadores" (indexers) descentralizados, selecionados pelos "curadores" (curators) e patrocinados pelos "delegadores" (delegators). Você pode ler mais sobre o protocolo em thegraph.com.

Em segundo lugar, o The Graph também é uma tecnologia que ajuda a criar um processo ETL (Extract Transform Load - Extração, Transformação e Carregamento em tradução livre), chamado "subgrafo" (subgraph), que coletará os dados de que você precisa, armazenará no banco de dados e tornará acessível via GraphQL.

Em terceiro lugar, você não precisa executar hardware específico, nós de arquivo de blockchain, indexadores, etc., porque você pode usar provedores existentes na infraestrutura do The Graph e implantar seus subgrafos em um deles.

Todos esses pontos tornam os subgrafos atraentes para desenvolvedores Web3, analistas ou pesquisadores. Então, vamos começar.

Como começar com subgrafos

Um código modelo do subgrafo pode ser criado usando o programa de linha de comando graph-cli. Para instalá-lo, execute


npm install -g @graphprotocol/graph-cli



Enter fullscreen mode Exit fullscreen mode

Ou


yarn global add @graphprotocol/graph-cli



Enter fullscreen mode Exit fullscreen mode

Então, se você executar o comando com os 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



Enter fullscreen mode Exit fullscreen mode

Você também pode alterar o parâmetro "start-block" para o bloco com o qual você realmente deseja começar. Por exemplo, poderia ser o bloco em que o contrato foi implantado. Você pode ir ao Etherscan, pesquisar este contrato pelo endereço 0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF e rolar até a primeira transação. O número do bloco é 9117720.

Como resultado da execução deste comando, obteremos a pasta do projeto que pode ser implantada em qualquer provedor de hospedagem de subgrafos. Mas, neste caso, os dados serão limitados apenas pelas variáveis emitidas pelos eventos.

O que significa "as variáveis emitidas pelos eventos"

Se você abrir o código solidity deste contrato inteligente, verá algumas classes de contrato que incluem algumas funções como deposit ou withdraw (saque):


sertedIndex = _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

Você pode notar que no final dessas funções, o evento Deposit/Withdrawal está sendo emitido. Isso significa que as variáveis entre parênteses serão salvas nos logs que podem ser facilmente acessados pelo nosso novo projeto de subgrafo(que acabamos de gerar). Esses eventos são descritos da seguinte 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

Se essas variáveis forem tudo o que você precisa, você pode simplesmente implantar o subgrafo na plataforma de hospedagem de subgrafos e pronto. Você obterá o endpoint GraphQL, que pode ser chamado com as consultas como:


{

  withdrawals(first: 10) {

    id

    to

    nullifierHash

    relayer

    fee

    blockNumber

    blockTimestamp

    transactionHash

  }

}

Enter fullscreen mode Exit fullscreen mode

Este é um manual e uma demonstração de como implantar um subgrafo na plataforma Chainstack, mas você também pode usar qualquer outra plataforma.

Agora é hora de ver o que realmente geramos com o comando "graph init". Para controlar o comportamento do subgrafo, você precisa trabalhar com 3 arquivos.

Primeiro, o manifesto "subgraph.yaml". Este código será semelhante a isto:

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

As coisas importantes são: cadeia/rede, startBlock, nomes dos eventos e caminhos para as fontes. Tudo é intuitivamente claro. Você pode deixar este arquivo como está.

Segundo, o schema.graphql - este arquivo descreve como nossos dados dos eventos serão armazenados. O arquivo gerado por padrão para este contrato inteligente terá este aspecto:


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

Se desejar adicionar algo, você pode fazer a modificação diretamente neste arquivo. No entanto, esta é apenas a descrição de como armazenar os dados, não de como obtê-los. Aqui você pode encontrar um guia explicando os esquemas de subgrafos.

Terceiro, src/tornado-contract.ts - este arquivo contém a lógica real de como obter os dados dos eventos (e não apenas dos eventos!) e como inseri-los nas tabelas que acabamos de descrever acima. Este arquivo se parece com isso:


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 você pode ver, ele simplesmente copia os dados dos campos da variável "event" para o objeto Deposit/Withdrawal nos espaços apropriados. Todo esse código foi gerado e pode ser implantado sem nenhuma alteração!

Mas e se precisarmos de mais informações para cada transação relacionada ao Tornado Cash? Por exemplo, não há informações sobre o endereço que está enviando seu "dinheiro" para o contrato inteligente do Tornado Cash. Vamos adicioná-lo em algumas linhas de código!

Uma coisa que você precisa saber aqui. Quando você obtém a variável "event", ela também contém muito mais informações além dos parâmetros emitidos. Entidades de dados completas que podem ser facilmente extraídas:


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

Quero adicionar o "from" e o "value" da entidade Transaction. Para fazer isso, adicionei duas linhas de código em src/tornado-contract.ts:


  entity.blockNumber = event.block.number

  entity.blockTimestamp = event.block.timestamp

  entity.transactionHash = event.transaction.hash



// LINHA #1 O endereço que acionou o evento pode ser acessado através //do

 event.transaction.from

  entity.from_ = event.transaction.from



  // LINHA #2 O valor da transação em Wei pode ser acessado através do event.transaction.value

  entity.value_ = event.transaction.value



  entity.save()



Enter fullscreen mode Exit fullscreen mode

Além disso, preciso adicionar duas linhas ao arquivo 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

Sim, é tão fácil. Agora você pode implantar seu  subgrafo com uma linha como esta:


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

e consultar os dados na interface do usuário assim:

{

  deposits(first: 10) {

    id

    commitment

    leafIndex

    timestamp

    transactionHash

    from_

    value_

  }

}

Enter fullscreen mode Exit fullscreen mode

ou na linha de comando:


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

Mas e se você precisar salvar alguns resultados de chamadas de contrato inteligente como um valor? Também é possível executar eth_call de um  subgrafo. Irei deixar isso para você brincar, vou apenas adicionar este tutorial chamado "Indexing ERC-20 token balance", que cobre esse aspecto também.

Se tiver alguma dúvida, ficarei feliz em responder ou discutir no chat do Telegram chamado "Subgraphs Experience Sharing".

A lista completa de tutoriais está aqui:


Este artigo foi escrito por  Kirill Balakhonov | Chainstack e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Latest comments (0)