Bem-vindo à edição inaugural da série Melhores Práticas no Desenvolvimento de Subgrafos. O objetivo desta série é compartilhar insights e recomendações com base em padrões que surgiram desde que o Graph Node foi disponibilizado em código aberto em 2018. Uma quantidade incrível de novos recursos foi lançada desde então, levando a dezenas de milhares de subgrafos sendo construídos em todo o ecossistema web3. Subgrafos (APIs descentralizadas de código aberto) permitem que os desenvolvedores criem front-ends rápidos para seus dapps e se tornaram uma camada integral do stack web3. No entanto, com o aumento do uso, vem a demanda crescente para descobrir eficiências e tanto novos quanto experientes desenvolvedores de subgrafos muitas vezes buscam descobrir e implementar as melhores práticas.
Esta nova série de desenvolvimento irá equipá-lo com dicas e truques para aprimorar suas habilidades de desenvolvimento de subgrafos, ajudando-o a criar dapps mais eficientes e rápidos! Com esta edição de estreia, vamos começar com uma das melhores práticas frequentemente negligenciadas, que é muito simples de implementar e traz enormes benefícios.
Parte 1: Reduzir as eth_calls: Uma melhoria simples de desempenho de indexação.
Pode ser frustrante quando o subgrafo está sendo indexado mais lentamente do que o esperado. Às vezes, os desenvolvedores inadvertidamente acabam testando os limites do desempenho de indexação. Os desenvolvedores do ecossistema The Graph estão trabalhando em tecnologias que irão melhorar significativamente o desempenho e as capacidades de dados descentralizados, como o Firehose e o Substreams. No entanto, os desenvolvedores de subgrafos muitas vezes podem ver melhorias significativas em termos de desempenho de indexação e velocidade de consulta simplesmente otimizando seu subgrafo. A melhoria mais mencionada é reduzir ou evitar completamente o acesso ao estado do contrato inteligente através das eth_calls. Vamos mergulhar em uma solução.
Por que Reduzir eth_calls?
Uma configuração comum de dapp se assemelha a esta ilustração:
Neste modelo, a interface do usuário do nosso dapp exibe todos os dados necessários diretamente do subgrafo, como visto no lado esquerdo, então não há necessidade de buscar dados diretamente no nó blockchain por meio do JSON RPC. Se os usuários interagem com o contrato inteligente de um dapp, eles enviam uma transação que mudará o estado do contrato inteligente, como visto no lado direito da ilustração acima. Exemplos incluem cunhagem, transferências, trocas, etc. Quando o estado do contrato inteligente muda, ele emite eventos aos quais o subgrafo está inscrito. Ouvir e indexar os dados emitidos em eventos é um padrão de desempenho muito eficiente.
Infelizmente, muitos contratos inteligentes não emitem todos os dados necessários diretamente em seus eventos. Para determinar o novo estado do contrato inteligente após uma transação concluída, muitas vezes é necessário enviar um eth_call de volta ao contrato inteligente para recuperar esses dados. No entanto, as chamadas JSON RPC são geralmente lentas. Cada chamada geralmente leva de 100ms a vários segundos para ser resolvida. É por isso que os desenvolvedores são desencorajados a usá-las em suas interfaces de usuário para exibir informações. Essas eth_calls também são lentas se executadas como parte das operações de mapeamento dentro do Graph Node. Felizmente, existem padrões para reduzi-las:
Informações de bastidores: para lidar adequadamente com reorganizações e indexar dados históricos, o Graph Node utiliza a EIP-1898 adicionando o hash do bloco como um parâmetro para o eth_call. Isso garante que o resultado da chamada venha do estado final do bloco em que o evento foi emitido. No entanto, existem casos extremos em que o estado de um contrato muda intra-bloco entre a emissão do evento e a finalização do bloco. Outra boa razão para preferir eventos. Especificar o hash do bloco também torna a chamada eth_call mais difícil para o nó Ethereum resolver.
Se ainda pudermos alterar o contrato porque ele ainda está em desenvolvimento ou é atualizável, devemos tentar emitir todos os dados necessários antecipadamente no evento. No entanto, às vezes não podemos mais alterar o contrato, então precisamos recorrer ao uso de eth_calls. Nesse caso, é aconselhável que tentemos minimizar a quantidade de eth_calls.
Conforme descrito acima, eth_calls sempre retornam o estado do contrato inteligente no final do bloco em que a chamada é feita. Se o estado do contrato não mudou à medida que a cadeia progride, fazer essas chamadas é apenas uma maneira cara de procurar dados que já foram recuperados. Portanto, um padrão comum para evitar isso é armazenar o resultado de um eth_call no subgrafo e enviar apenas essa chamada se os dados ainda não forem conhecidos.
Exemplo de Esquema (Schema)
Vamos dar uma olhada em um exemplo de subgrafo simples de NFT. Neste exercício, queremos acompanhar a quantidade total de NFTs criados para este projeto hipotético de NFTs, bem como os metadados de cada token. Nosso exemplo de subgrafo teria o seguinte esquema (schema):
type Token @entity {
id: Bytes!
owner: Bytes
uri: String
contract: Contract
}
type Contract @entity {
id: Bytes!
name: String
symbol: String
totalSupply: BigInt
tokens: [Token!]! @derivedFrom(field: "contract")
}
Um esquema de entidade pode ser lido como a definição de uma tabela de banco de dados: este esquema descreve uma tabela de token que contém todos os tokens com seus detentores e uma tabela de contrato que tem apenas uma entrada (singleton) para armazenar informações sobre o contrato.
Implementação Ingênua
Um mapeamento (mapping) ingênuo seria assim:
import { Bytes } from '@graphprotocol/graph-ts';
import { ERC721, Transfer as TransferEvent } from '../generated/ERC721/ERC721';
import { Token, Contract } from '../generated/schema';
export function handleTransfer(event: TransferEvent): void {
let instance = ERC721.bind(event.address);
let contract = new Contract(event.address);
contract.name = instance.name(); // eth_call
contract.symbol = instance.symbol(); // eth_call
contract.totalSupply = instance.totalSupply(); // eth_call
let token = new Token(Bytes.fromI32(event.params.tokenId.toI32()));
token.owner = instance.ownerOf(event.params.tokenId); // eth_call
token.uri = instance.tokenURI(event.params.tokenId); // eth_call
token.contract = contract.id;
contract.save();
token.save();
}
Nota: Para simplificar, não tratamos a possibilidade de chamadas que reverteram. Em código de produção, é altamente recomendável fazê-lo de acordo com a seção Tratamento de Chamadas Revertidas nos Documentos do The Graph.
Vamos rever esse mapeamento. Como visto na linha 6, o contrato ERC-721 é vinculado ao endereço do contrato que emitiu o evento de transferência. Normalmente, obter uma instância de contrato vinculado é um indicativo de que as eth_calls seguirão. Para encontrar as eth_calls em um subgrafo, geralmente é possível pesquisar o código-fonte por “.bind(“.
Na linha 8, uma nova entidade (ou seja, uma linha em uma tabela) é criada. Aqui é onde um desenvolvedor de subgrafo deve começar a questionar: por que estamos criando uma nova entidade? Já poderia existir uma entidade no banco de dados. Nas linhas seguintes, são acionadas as eth_class para obter o nome, o símbolo e o fornecimento total do contrato. Lembre-se de que essas chamadas são acionadas para cada evento de transferência. Um contrato NFT com muitas transferências acionaria milhares de eth_calls. A maioria delas é desnecessária, já que o nome e o símbolo de um contrato NFT geralmente não mudam. Somente o fornecimento total pode mudar, desde que este NFT possa ser cunhado.
Avançando para a linha 13 com o bloco sobre o próprio token. Um comportamento semelhante é observado aqui: o proprietário muda a cada transferência, então uma eth_call aqui pode ser razoável para recuperar o novo proprietário. O tokenURI, no entanto, não deve mudar.
Implementação Otimizada de eth_calls
Como podemos otimizar esse mapeamento (mapping) para ter menos eth_calls? Existem duas estratégias principais:
- Faça o cache do resultado das eth_calls
- Utilize os dados do próprio evento e calcule as informações no subgrafo
Assim seria um mapeamento (mapping) otimizado:
import { Address, BigInt, Bytes } from '@graphprotocol/graph-ts';
import { ERC721, Transfer as TransferEvent } from '../generated/ERC721/ERC721';
import { Token, Contract } from '../generated/schema';
const ZERO_ADDRESS = Address.fromString(
'0x0000000000000000000000000000000000000000',
);
function getOrCreateContract(address: Address): Contract {
let contract = Contract.load(address);
if (!contract) {
let instance = ERC721.bind(address);
contract = new Contract(address);
contract.name = instance.name();
contract.symbol = instance.symbol();
contract.totalSupply = BigInt.fromI32(0);
contract.save();
}
return contract;
}
function getOrCreateToken(tokenId: BigInt, address: Address): Token {
let id = Bytes.fromI32(tokenId.toI32());
let token = Token.load(id);
if (!token) {
let instance = ERC721.bind(address);
token = new Token(id);
token.uri = instance.tokenURI(tokenId);
token.contract = address;
token.save();
}
return token;
}
export function handleTransfer(event: TransferEvent): void {
let contract = getOrCreateContract(event.address);
let token = getOrCreateToken(event.params.tokenId, event.address);
token.owner = event.params.to;
token.save();
// Um novo token foi cunhado se ele vem do endereço
if (event.params.from == ZERO_ADDRESS) {
contract.totalSupply = contract.totalSupply.plus(BigInt.fromI32(1));
contract.save();
}
}
Primeiro, introduzimos funções auxiliares para o contrato e o token. As funções auxiliares tentam primeiro carregar uma entidade do banco de dados. Somente se ainda não existirem, elas enviam eth_calls para carregar os dados necessários. Elas também definem valores padrão, se necessário, como na linha 17, onde o fornecimento total de um novo contrato é definido como zero.
Na função handler, essas funções auxiliares são utilizadas. Portanto, se um token e o contrato já existirem, eles não enviarão uma única eth_call. Na linha 43, usamos os dados que vêm com o evento emitido. Sabemos quem é o novo proprietário desse token olhando para o parâmetro to
do evento.
Nas linhas 47 e 48 podemos calcular o totalSupply dentro dos mapeamentos sem quaisquer eth_calls: Se o token foi recém-criado, observaremos um parâmetro from
com o endereço zero. Se for esse o caso, podemos simplesmente aumentar o totalSupply em um.
Continuação da Otimização.
Com a introdução de funções auxiliares, conseguimos minimizar a quantidade de chamadas à ETH neste exemplo. O resultado é um aumento tremendo no desempenho de indexação do subgrafo. Padrões semelhantes podem ser aplicados a outros desafios:
- Apenas faça eth_calls uma vez
- Armazenar o resultado das eth_calls no banco de dados
- Tente calcular o estado do contrato inteligente dentro do subgrafo
Um exemplo mais complexo que vale a pena mencionar aqui é a otimização de um clone da Uniswap por Adam Fuller e David Lutterkort, registrada no Github.
Obrigado por ler a primeira edição da série Melhores Práticas no Desenvolvimento de Subgrafos. Certifique-se de seguir The Graph no Twitter ou junte-se à conversa no Discord para ler futuros artigos sobre como aprimorar e melhorar sua habilidade de desenvolvimento de subgrafos.
Este guia foi escrito em colaboração com Simon Emanuel Schmid, David Lutterkort, Michael Macaulay e Brian Berman da Edge & Node, juntamente com Slimchance e Nick Hansen da Fundação The Graph.
Sobre The Graph
The Graph é a camada de indexação e consulta da web3. Os desenvolvedores constroem e publicam APIs abertas, chamadas de subgrafos, que as aplicações podem consultar usando GraphQL. O Graph atualmente suporta indexação de dados de mais de 39 redes diferentes, incluindo Ethereum, NEAR, Arbitrum, Optimism, Polygon, Avalanche, Celo, Fantom, Moonbeam, IPFS, Cosmos Hub e PoA, com mais redes chegando em breve. Até o momento, mais de 74.000 subgrafos foram implantados no serviço hospedado. Dezenas de milhares de desenvolvedores usam The Graph para aplicações como Uniswap, Synthetix, KnownOrigin, Art Blocks, Gnosis, Balancer, Livepeer, DAOstack, Audius, Decentraland e muitas outras.
Em julho de 2021, a experiência de autoatendimento da rede The Graph para desenvolvedores foi lançada; desde então, mais de 500 subgrafos migraram para a rede, com mais de 180 indexadores servindo consultas de subgrafo, mais de 9.300 delegadores e mais de 2.400 curadores até o momento. Mais de 4 milhões de GRT foram sinalizados até o momento, com uma média de 15.000 GRT por subgrafo.
Se você é um desenvolvedor construindo uma aplicação ou aplicativo web3, pode usar subgrafos para indexar e consultar dados de blockchains. The Graph permite que aplicativos apresentem dados de forma eficiente e com bom desempenho em uma interface do usuário e permite que outros desenvolvedores usem seu subgrafo também! Você pode implantar um subgrafo na rede usando o recém-lançado Subgraph Studio ou consultar subgrafos existentes que estão no Graph Explorer. The Graph adoraria recebê-lo como Indexadores, Curadores e/ou Delegadores na rede principal do The Graph. Junte-se à comunidade do The Graph apresentando-se no Discord do The Graph para discussões técnicas, participe do chat do Telegram do The Graph e siga o The Graph no Twitter, LinkedIn, Instagram, Facebook, Reddit e Medium! Os desenvolvedores do The Graph e os membros da comunidade estão sempre ansiosos para conversar com você, e o ecossistema do The Graph tem uma comunidade crescente de desenvolvedores que se apoiam mutuamente.
A Fundação The Graph supervisiona a The Graph Network. A Fundação The Graph é supervisionada pelo Conselho Técnico. Edge & Node, StreamingFast, Semiotic Labs, The Guild, Messari e GraphOps são sete das muitas organizações dentro do ecossistema The Graph.
Esse artigo é uma tradução feita por @bananlabs. Você pode encontrar o artigo original aqui
Top comments (0)