Uma das primeiras barreiras que encontramos quando queremos iniciar um trabalho de monitorar uma dex qualquer, como a SushiSwap, é que como ela roda inteiramente na blockchain (web3 baby!), não há APIs REST para usarmos do modo que faríamos com corretoras centralizadas como Binance, por exemplo. Ao invés disso, precisamos interagir com smart contracts na blockchain usando bibliotecas com Web3.js ou EthersJS, que já mostrei como algumas vezes aqui no blog (clique nos links para conhecer os tutoriais).
No entanto, as dex não costumam fornecer em seus smart contracts funções que exibam os preços dos ativos e aí mora nosso primeiro e maior problema. Isso não é possível de maneira tão simples quanto em exchanges centralizadas porque não existe um book de ofertas para se consultar o preço atual em 99% das dex, pois elas operam de maneira diferente, baseadas na Constant Product Formula e outras “matemáticas” derivadas dela. Assim, com base na oferta de um par de moedas em um pool de liquidez vs o tamanho do lote a ser negociado, um “preço” é estipulado para aquela operação, na própria transação do swap em si.
Ou seja, não é algo impossível de ser calculado, apenas um pouco mais complexo do que gostaríamos e é justamente isso que vou te mostrar como fazer neste tutorial. É importante entender que para conseguir acompanhar este tutorial você deve ter conhecimentos básicos de Node.js e de funcionamento da SushiSwap, como usuário. Ajuda um pouco se você conhecer o básico de smart contracts em Solidity também, mas não é algo obrigatório.
Vamos lá!
Estruturando o Projeto
Para fazer chamadas aos smart contracts da SushiSwap você vai precisar ter acesso a um nó da blockchain que desejará monitorar. Você pode obter um nó gratuitamente com a Infura, um dos maiores provedores de Blockchain as a Service do mundo. Crie uma conta gratuita no site deles e depois crie um node da rede que deseja (eu vou usar Ethereum) para você assim que conseguir entrar no painel. Guarde a API Key que vai receber, vamos precisar dela mais tarde.
Agora vamos criar nosso projeto Node.js, começando pela criação de uma pasta pancakeswap-monitor e inicialização de um projeto Node.js nela.
1 2 3 4 5 |
mkdir sushiswap-monitor cd sushiswap-monitor npm init -y |
Depois, vamos instalar as dependências que vamos precisar.
1 2 3 |
npm install dotenv ethers |
A saber:
- DotEnv: pacote para carregamento das variáveis de ambiente;
- Ethers: pacote para comunicação com a blockchain;
Agora crie um arquivo .env na raiz do seu projeto e coloque nele as seguintes variáveis:
- INFURA_API_KEY: informe sua API Key da Infura;
- INTERVAL: informe o intervalo em que o bot vai coletar o preço atualizado. Ex: 300000 (5min);
- NETWORK: o nome da rede que vai monitorar, em minúsculas. Ex: mainnet
- QUOTER_ADDRESS: o endereço do contrato quoter (cotação) da SushiSwap V3 na rede que deseja monitorar. Lista completa de endereços neste link.
- TOKEN_IN_ADDRESS: o endereço do contrato ERC20 do token in na rede que vai monitorar. Ex: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (WETH na Ethereum Mainnet)
- TOKEN_OUT_ADDRESS: o endereço do contrato ERC20 do token out na rede que vai monitorar. Ex: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC na Ethereum Mainnet)
Muita atenção aqui à variável INTERVAL, pois os nós possuem uma limitação de chamadas por dia. É bastante, mas não custa nada tomar cuidado né. Outro ponto de atenção são os endereços dos tokens in e out, sendo que aqui vamos ter como resultado a cotação em dólares do ETH, através do par WETH/USDC (Wrapped ETH, pareado com ETH e USDC, pareado com dólar).
Agora crie um arquivo Quoter.abi.json na raiz do projeto onde você deverá colocar o código abaixo, que especifica um ABI (Application Binary Interface) do contrato de QuoterV2 da dex. Ele serve para fazermos a comunicação com o respectivo contrato na blockchain.
1 2 3 |
[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint256","name":"amountIn","type":"uint256"}],"name":"quoteExactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint160[]","name":"sqrtPriceX96AfterList","type":"uint160[]"},{"internalType":"uint32[]","name":"initializedTicksCrossedList","type":"uint32[]"},{"internalType":"uint256","name":"gasEstimate","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IQuoterV2.QuoteExactInputSingleParams","name":"params","type":"tuple"}],"name":"quoteExactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceX96After","type":"uint160"},{"internalType":"uint32","name":"initializedTicksCrossed","type":"uint32"},{"internalType":"uint256","name":"gasEstimate","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"quoteExactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint160[]","name":"sqrtPriceX96AfterList","type":"uint160[]"},{"internalType":"uint32[]","name":"initializedTicksCrossedList","type":"uint32[]"},{"internalType":"uint256","name":"gasEstimate","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IQuoterV2.QuoteExactOutputSingleParams","name":"params","type":"tuple"}],"name":"quoteExactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceX96After","type":"uint160"},{"internalType":"uint32","name":"initializedTicksCrossed","type":"uint32"},{"internalType":"uint256","name":"gasEstimate","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"path","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"view","type":"function"}] |
Por fim, crie um arquivo index.js na raiz do seu projeto e configure no package.json para que ele seja executado no comando de start, já que é nele que irá toda a lógica do nosso bot.
1 2 3 4 5 |
"scripts": { "start": "node index" }, |
E com isso temos toda a “infraestrutura” necessária para desenvolver o nosso robô de monitoramento de preços na SushiSwap, vamos em frente.
Preparando o Monitoramento
Primeiro, carregamos os pacotes e as variáveis de ambiente em constantes locais no index.js.
1 2 3 4 5 6 |
require("dotenv").config(); const { ethers } = require("ethers"); const { INTERVAL, QUOTER_ADDRESS, TOKEN_IN_ADDRESS, TOKEN_OUT_ADDRESS, NETWORK, INFURA_API_KEY } = process.env; |
Em seguida, vamos criar objetos com as configurações do provider (nó da blockchain) e do contrato de cotação (Quoter). Para isso usaremos as variáveis de ambiente que carregamos anteriormente e também o ABI. Repare que você terá de mudar estas configurações de provider e contrato conforme a rede que for monitorar (a SushiSwap possui deploy em várias redes). O mesmo vale para os endereços das moedas no .env.
1 2 3 4 5 |
const provider = new ethers.InfuraProvider(NETWORK, INFURA_API_KEY); const QUOTER_ABI = require("./Quoter.abi.json"); const quoterContract = new ethers.Contract(QUOTER_ADDRESS, QUOTER_ABI, provider); |
Com estas informações todas estamos com tudo pronto para finalizar nosso bot.
Programando o Monitoramento
Agora é hora de começarmos a implementar a função de monitoramento de fato.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
async function executionCycle() { const [amountOut] = await quoterContract.quoteExactInputSingle.staticCall({ tokenIn: TOKEN_IN_ADDRESS,//WETH tokenOut: TOKEN_OUT_ADDRESS,//USDC amountIn: ethers.parseEther("1"), fee: 3000, sqrtPriceLimitX96: 0 }) console.log("WETH 1 is equals to USDC " + ethers.formatUnits(amountOut, 6)); } |
Aqui chamamos a função quoteExactInputSingle do contrato QuoterV2 que espera os dados dos tokens (in e out, WETH e USDC), da fee (3000 ou 0.3% é a taxa média), a quantidade de token que vamos gastar (1 unidade inteira de tokenIn ou WETH) e o mínimo que esperamos receber (“0” quer dizer qualquer quantia no sqrtPriceLimitX96). Resumindo: esta função faz a seguinte pergunta: quantos USDC eu vou receber, considerando taxa média, se eu vender 1 WETH inteiro?
Repare que após a chamada da função eu usei o recurso staticCall. Esse recurso é crucial para nosso robô pois a função quoteExactInputSingle é uma transaction, ou seja: escreve na blockchain e consequentemente gera custos na sua execução. No entanto, ao usarmos uma static call nós dizemos ao nó da blockchain que ele deve executar a transação mas não deve persisti-la no disco, nos livrando dos custos envolvidos. Como é apenas uma informação que queremos, esse comportamento de transação “efêmera” é perfeitamente aceitável.
Agora que finalizamos nossa função de execução do ciclo de monitoramento, vamos implementar o monitoramento em si.
1 2 3 4 5 |
setInterval(() => executionCycle(), INTERVAL); executionCycle(); |
Aqui configuro o timer que rodará a cada x tempo refazendo o monitoramento. Como não quero ter de esperar pela execução do primeiro ciclo, já chamo a executionCycle logo na sequência.
Internamente, a executionCycle já vai imprimir pra gente o preço obtido na cotação, então com isso programado e rodando o projeto com npm start, você terá o preço impresso no seu console a cada 5 minutos.
O resultado você confere abaixo.
A própria UniSwap criou uma Graph API que faz exatamente o que mostrei neste tutorial, mas expõe de maneira mais simples, saiba mais aqui.
Também possuo tutoriais que podem lhe ajudar a transformar esse conhecimento em um bot de sinais, como esse de Telegram, de email, de SMS e até de Whatsapp.
E se quiser fazer um bot que faça os swaps automaticamente, aprenda aqui.
E com isso finalizamos mais este tutorial. Até o próximo!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.