Um olhar sobre a busca de dados em Blockchain usando SudoSwap e Multicall
Lidando com dados da blockchain, fornecedores de nós e contratos inteligentes utilizando técnicas avançadas
Durante bastante tempo, fiquei fascinado pelo SudoSwap. Para os não familiarizados com o projeto Sudo, o seu objetivo é criar uma experiência semelhante à de DEXs tal como Uniswap, Curve, e Sushiswap. Pode-se comprar e vender NFTs como se fizessem parte de um par de tokens no Uniswap. Pode comprar ou vender NFTs apenas interagindo com os pools da AMM e obter instantaneamente o seu desejo. Este design muito inteligente e simples com muito sucesso em DEXs bem conhecidas é interessante, por isso decidi aprofundar mais no assunto.
O caso de uso imediato que me ocorreu, para além das sofisticadas Defi primitivas que se podem construir em sudoswap, foi uma melhor arbitragem NFT. Quando o MEV entra em jogo, pode-se esperar um volume de comércio e utilização cada vez mais significativo. Neste momento, o sudoswap está funcionando muito bem, com mais de 35.000 pools e 200.000 negócios com nft. Espero que o sudoswap cresça em grande escala no futuro.
Análise Dune: https://dune.com/0xRob/sudoamm
Seria uma boa experiência de aprendizagem sujar as minhas mãos e tentar criar uma base de dados que pudesse ser útil para encontrar oportunidades de arbitragem (pelo menos conceitualmente), mas que logo conduziu a uma típica toca de coelho empilhada na web3
Em que consistirá esta base de dados? Muito simples, Uma tabela com o endereço do pool e algumas propriedades fixas importantes do pool, tais como a colecção NFT (endereço), Delta (o efeito de uma venda ou de uma compra sobre o preço) e endereço do token ERC20 (embora não totalmente suportado no sudoswap). Decidi torná-la a mais simples, por isso vamos nos concentrar apenas no endereço NFT e nos pools a ele associados.
Como mencionei anteriormente, o sudoswap tem uma estrutura semelhante à das DEXs conhecidas, tem um contrato de Fábrica que cria uma nova instância de pool de pares, e depois o novo pool em si tem algumas propriedades se quisermos obter todos os pools e os seus respectivos dados de nft precisaremos de duas etapas
Obter todos os endereços de pools sobre sudoswap, o que pode ser feito utilizando eventos
Obter o endereço de colecção NFT de cada pool, o que pode ser feito através de uma chamada de função básica apenas de leitura
Para o primeiro passo, precisamos compreender como vamos obter de forma confiável (utilizando apenas dados na chain) mais de 35k de endereços de pool. Primeiro, precisamos verificar o contrato de fábrica através do etherscan. Uma prática bem conhecida no desenvolvimento de contratos inteligentes é utilizar eventos para indicar que a função de chamada foi bem sucedida e que algumas mudanças na chain foram realizadas. Olhando para o código do contrato de fábrica, podemos ver uma lista de eventos:
O novo evento de pares é uma combinação perfeita. Portanto, para conseguirmos todos os eventos, precisamos ir buscar todos os eventos de "novos pares". Vamos escrever o código e executá-lo:
const { ethers } = require("ethers");
const { Interface } = require("ethers/lib/utils");
const fs = require("fs");
const interface1 = new Interface(["event NewPair (address poolAddress)"]);
const hexToDecimal = (hex) => parseInt(hex, 16);
async function Create_Index() {
const provider = new ethers.providers.AlchemyProvider(
"homestead",
"API_KEY"
);
const addr = "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4"; // Sudo Factory address
const Factory = new ethers.Contract(addr, interface1, provider);
var first_block = 14645816; // SudoSwap Factory deployment's block
const final_block = await provider.getBlockNumber();
res = await Factory.queryFilter("NewPair", first_block, final_block);
}
Create_Index();
Você precisará adicionar o pacote Ethers.js e criar um fornecedor de nós.Estou usando Alchemy, e pela minha experiência, seu plano gratuito é muito vantajoso. Você também pode criar seu próprio nó, mas pode ser um exagero. Nós executamos o código e...
OOPS, temos um erro no servidor:
Tamanho de resposta dos logs excedido. Você pode fazer solicitações eth_getLogs com um intervalo de blocos de até 2K e sem limite no tamanho da resposta, ou pode solicitar qualquer intervalo de blocos com um limite de 10K logs na resposta. Com base em seus parâmetros e no limite do tamanho de resposta, esta faixa de blocos deve funcionar: [0xdf7a38, 0xea0fe0]
Portanto, aparentemente, não podemos ter os logs inteiros (mais de 35k de eventos) em uma única chamada. Mas temos duas soluções que podemos deduzir da resposta do servidor
percorrer toda a faixa de blocos em 2000 intervalos
usar a faixa exata de blocos que nos dá 10k de logs usando apenas 4 chamadas API
Vamos tentar a primeira solução. Se adicionarmos o código e o executarmos, perceberemos muito rapidamente um problema.
for(first_block; first_block<= final_block; first_block +=2000){
res = await Factory.queryFilter("NewPair", first_block, first_block+2000);
arr.push(res);
}
Se nossa gama de blocos começa em 14.645.816 e termina após 15.700.000, temos mais de 1 milhão de blocos. Teremos que fazer mais de 500 iterações, o que significa mais de 500 chamadas API para nosso fornecedor. Isto nos custará muito em termos de unidades de computação e também levará alguns minutos devido ao rendimento limitado do fornecedor do nó.
Para a segunda solução, obviamente podemos implementar uma função de ajuda que calculará a faixa exata de blocos de 10k de logs, mas é bastante complicada e exigirá uma quantidade razoável de chamadas API mesmo com alguns truques que usamos com base na natureza e no código do contrato de fábrica, podemos usar seu nonce, mas também podemos intencionalmente "falhar" nossa chamada para obter do provedor do nó a faixa de blocos apropriada:
const { ethers } = require("ethers");
const { Interface } = require("ethers/lib/utils");
const fs = require("fs");
const interface1 = new Interface(["event NewPair (address poolAddress)"]);
const hexToDecimal = (hex) => parseInt(hex, 16);
async function Create_Index() {
const provider = new ethers.providers.AlchemyProvider(
"homestead",
"API_KEY"
);
const addr = "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4";// Sudo Factory address
const addr1 = "0x16F71D593bc6446a16eF84551cF8D76ff5973db1";
const Factory = new ethers.Contract(addr, interface1, provider);
var events = [];
var first_block = 14645816; //Bloco de implantação da fábrica SudoSwap
var last_block = first_block;
const final_block = await provider.getBlockNumber();
//Obtendo todos os eventos do "novo par" em quantidade mínima de chamadas API
while (first_block <= final_block) {
try {
// Falha em cada chamada até a última
if (
events.push(
(res = await Factory.queryFilter("NewPair", last_block, final_block))
)
) {
console.log("-------------------------------------------------------");
console.log(
`fetching data from block ${last_block} to block ${final_block} ...`
);
console.log("-------------------------------------------------------");
break;
}
// Usando a resposta de erro do servidor da Alchemy para determinar exatamente a faixa de blocos para a quantidade máxima de 10K logs
} catch (e) {
const text = `${e}`;
console.log("-------------------------------------------------------");
// corte de texto básico codificado com conversão de string hexadecimal para decimal
first_block = hexToDecimal(text.slice(397, 405));
last_block = hexToDecimal(text.slice(407, 415));
console.log(
`fetching data from block ${first_block} to block ${last_block} ...`
);
events.push(
await Factory.queryFilter("NewPair", first_block, last_block)
);
}
}
events = events.flat();
}
Create_Index();
primeiro, criamos uma matriz de eventos que irá armazenar todos os dados
Em seguida, usamos um padrão de try-catch para processar a resposta do servidor e obter o intervalo exato de blocos
Devemos esperar que cada chamada dentro do bloco de tentativas falhe até a última chamada, que deve ser válida.
obtemos a faixa de blocos através de um corte de texto muito simples e codificado e conversão de string hexadecimal para decimal para ajudar na legibilidade
A cada chamada em que recebemos uma nova matriz de resultados em uma nova posição na matriz de eventos, precisaremos usar o flat() para obter uma matriz de eventos unificada
Isto resulta em uma execução bastante rápida e eficiente de mais de 500 chamadas API para menos de 10. Cada evento tem este aspecto, podemos obter o endereço do pool usando eventos[i].args.poolAddress:
Agora temos ~35k de endereços. Precisamos obter para cada um desses pools o endereço de coleta do nft. Se olharmos para o código do LSSVMPair, podemos ver que na linha 394, temos uma função básica de leitura que nos dá o endereço de coleta do NFT. Esta é a única função que precisamos especificar em nosso objeto ABI.
function nft() public pure returns (IERC721 _nft)
então nosso objeto de contrato e chamada deve se parecer com isto:
nft = new ethers.Contract(events[i].args.poolAddress, abi, provider ).nft();
Mas o mesmo problema surge se temos contratos de 35k e precisamos chamar a função nft() em cada um deles. O resultado também são chamadas API de 35k. Você pode executar e ver por si mesmo. Para mim, levou mais de 3 horas para ser concluído. Então, qual será a solução? Obviamente, estamos lidando aqui com chamadas de função e não eventos, portanto, a abordagem que usamos anteriormente não é aplicada aqui. Uma nova abordagem MULTICALL pode ser útil.
…
Multicall é um contrato implantado na chain que permite a agregação de chamadas contratuais de tal forma que você pode combinar algumas chamadas em apenas 1 chamada API. Há uma série de implementações e pacotes de multicall: Makerdao multicall.js com MULTICALL3 é muito otimizado e flexível, também ethereum-multicall (Uniswap também tem um), mas eu achei 0xsequence multicall muito conveniente para nossos propósitos, se você quiser mergulhar mais fundo, você pode verificar o contrato de multicall aqui.
Note que, embora não haja um limite fixo para o número de chamadas que você pode incluir em um lote, o uso de lotes muito grandes pode levar a comportamentos e erros inesperados. Olhando para as melhores práticas da alchemy
** Ao enviar solicitações de lotes, visar lotes abaixo de 50 solicitações por chamada. **
Podemos usar uma solução muito conservadora e cautelosa, e ainda podemos obter uma melhoria de 50x no tempo e nas chamadas API. De mais de 35k a cerca de 700 chamadas e uma média de 30 ms de tempo de resposta para cada uma, todo o processo deve levar menos de um minuto.
o código final é:
const { ethers } = require("ethers");
const { Interface } = require("ethers/lib/utils");
const fs = require("fs");
const { providers } = require("@0xsequence/multicall");
const interface1 = new Interface(["event NewPair (address poolAddress)"]);
const abi = ["function nft() public view returns (address _nft)"];
const hexToDecimal = (hex) => parseInt(hex, 16);
async function Create_Index() {
const provider = new ethers.providers.AlchemyProvider(
"homestead",
"API_KEY"
);
// Segundo fornecedor a utilizar apenas para pedidos de lotes
const provider1 = new providers.MulticallProvider(
new ethers.providers.AlchemyProvider(
"homestead",
"API_KEY"
)
);
const addr = "0xb16c1342E617A5B6E4b631EB114483FDB289c0A4";// Sudo Factory address
const Factory = new ethers.Contract(addr, interface1, provider);
var events = [];
var first_block = 14645816; // Bloco de implantação da fábrica SudoSwap
var last_block = first_block;
const final_block = await provider.getBlockNumber();
//Obter todos os eventos do "novo par" em quantidade mínima de chamadas API
while (first_block <= final_block) {
try {
// Falha em cada chamada até a última
if (
events.push(
(res = await Factory.queryFilter("NewPair", last_block, final_block))
)
) {
console.log("-------------------------------------------------------");
console.log(
`fetching data from block ${last_block} to block ${final_block} ...`
);
console.log("-------------------------------------------------------");
break;
}
// Usando a resposta de erro do servidor da Alchemy para determinar exatamente a faixa de blocos para a quantidade máxima de 10K logs
} catch (e) {
const text = `${e}`;
console.log("-------------------------------------------------------");
//Corte de texto básico codificado com conversão de string hexadecimal para decimal
first_block = hexToDecimal(text.slice(397, 405));
last_block = hexToDecimal(text.slice(407, 415));
console.log(
`fetching data from block ${first_block} to block ${last_block} ...`
);
events.push(
await Factory.queryFilter("NewPair", first_block, last_block)
);
}
}
events = events.flat();
var calls = [];
//Construindo um conjunto de promessas de chamadas contratuais
for (i = 0; i < events.length; i++) {
const Pair = new ethers.Contract(
events[i].args.poolAddress,
abi,
provider1
).nft();
calls.push(Pair);
}
console.log("Fetching collections data ...");
//Usando Multicall
var res = await Promise.all(calls);
var final = [];
for (i = 0; i < events.length; i++) {
final.push({ addr: events[i].args.poolAddress, nft: res[i] });
}
const t = JSON.stringify(final);
fs.writeFile("Pairs.json", t, function (err, result) {
if (err) console.log("error", err);
});
console.log("Finished");
}
Create_Index();
Estamos usando o pacote mutlicall, a primeira parte (0-63 linhas) é idêntica ao código compartilhado anteriormente com algumas modificações, adicionamos ABI para as funções ** nft() ** somente leitura e criamos 2 provedores, um provedor regular para obter os eventos e um segundo que é um provedor multicall para as chamadas do contrato
a partir da linha 65, criamos uma matriz de chamadas que deve conter todas as chamadas contratadas de 35k sem executá-las (ou seja, enviá-las como chamadas API no momento em que o programa chega a contract.nft() )
usamos então promise.all() na matriz de chamadas que unifica todas as promessas de chamadas contratuais a uma promessa
então procedemos à criação de um conjunto de objetos de dados do pool chamados final
-
finalmente, criamos um arquivo JSON com todos os dados dos pares
…
Alguns pontos importantes
Lidar com dados na chain é muito desafiador, os dados podem parecer muito acessíveis através de serviços como o Etherscan, mas está longe de ser acessível para os POCs, sem mencionar a produção pronta.
Usar fornecedores de nós pode ser uma maneira muito eficiente de superar a rotação de seu próprio nó, mas a produção limitada deve ser levada em conta. Criatividade e soluções prontas devem ser exercitadas para superar as limitações.
No nível infra, os serviços de blockchain não fornecem uma solução razoável para a obtenção e indexação de dados de uso geral. Há um sentimento de que muitas equipes estão usando soluções ad-hoc para dados em blockchain, o que levanta algumas questões sobre a capacidade de lidar com escala intensa em produtos e serviços web3 com uso intensivo de dados.
Há alguns avanços na área, tais como API aprimorada ou NFT API, e empresas como Moralis e The Graph, mas nenhuma delas poderia nos fornecer uma solução fácil. Pesquisando estas soluções, achei-as muito gerais, sem capacidade de modificar para certas necessidades ou muito restritas e fornecendo casos de uso muito específicos.
…
O código completo do repo está disponível no GitHub com algumas integrações adicionais para o MongoDB
Este Artigo foi escrito por Natan B e traduzido para o português por Rafael Ojeda
Você encontra o artigo original em inglês aqui
Latest comments (0)