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
Ou
yarn global add @graphprotocol/graph-cli
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
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);
}
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);
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
}
}
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
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!
}
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()
}
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
}
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()
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!
}
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
e consultar os dados na interface do usuário assim:
{
deposits(first: 10) {
id
commitment
leafIndex
timestamp
transactionHash
from_
value_
}
}
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
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.
Oldest comments (0)