As exchanges decentralizadas ou DEX, como são popularmente chamadas, são uma alternativa às exchanges tradicionais, ou centralizadas, como Binance e Coinbase. Enquanto que nas exchanges centralizadas você compra e vende moedas com a corretora como mediadora das negociações e sem a custódia das suas próprias moedas (“not your keys, not your money”), nas DEX temos todas as negociações, chamadas de swaps (trocas), sendo feitas através de usuários que se conectam à rede com suas próprias carteiras de criptomoedas (como MetaMask) e a mediação acontecendo através de contratos inteligentes (smart contracts). Ou seja, não é um P2P “cru”, direto, ainda existe um intermediador, mas ele é um software aberto e transparente, publicado na blockchain.
Como Chanpeng Zhao afirma, o próprio fundador da Binance, as DEX são o futuro das corretoras de criptomoedas já que por rodarem nativamente na blockchain eles garantem a segurança, o anonimato e a descentralização a todas as partes envolvidas, diferente das exchanges tradicionais que sofrem cada vez mais ataques dos órgãos reguladores nos países em que operam. No entanto, operar em uma DEX é ligeiramente mais complicado do que operar em exchanges comuns, pela própria natureza das criptomoedas e blockchain em si. Da mesma forma, programar bots para este tipo de corretora é um desafio maior também.
Desta forma, neste tutorial eu espero lhe ajudar a entender como criar um primeiro bot sniper de criptomoedas que fará swaps o mais rápido possível, assim que um novo pool de liquidez seja listado na dex Uniswap (V3). A escolha por essa DEX não é por acaso: além dela ser a maior DEX existente, com quase metade do volume mundial de transações em dex, ela opera principalmente em cima da Ethereum, a blockchain com suporte a smart contracts mais bem sucedida e detentora da segunda maior criptomoeda em valor de mercado, a Ether (ETH). Não obstante, a Uniswap detém mais de 70% do volume transacionado em dex na rede Ethereum, e esse suporte garante que ela segue muitos padrões de desenvolvimento para redes “EVM compatible” (compatíveis com Ethereum Virtual Machine). Assim, uma vez que consiga programar um bot sniper para Uniswap, possivelmente conseguirá criar bots para outras dex “fork” no futuro, como PancakeSwap e SushiSwap.
É importante você entender, antes de começarmos a programar, que operar no mercado cripto é muito arriscado por este ser um mercado muito volátil e auto-regulado. Não posso prometer ou me responsabilizar por qualquer lucro ou prejuízo que você possa ter ao colocar o seu dinheiro em uma Uniswap ou qualquer outra DEX, independente se usar os códigos que ensino ou não. Além disso, é imprescindível que você já possua conhecimentos básicos de programação para poder fazer este tutorial e também que já tenha feito swaps manualmente na Uniswap antes, pois são coisas que não ensinarei aqui.
Dito isso, vamos ao tutorial.
#1 – O que é e como funciona um bot sniper?
Sniping é o ato de ficar mirando em um alvo, para acertá-lo no momento e local certos e sniper é o profissional especializado nesse tipo de tática. Inclusive aproveito para recomendar o filme Sniper Americano, com Bradley Cooper, que conta a história real de um famoso sniper. Mas e no mundo dos bots?
Antes de explicar sobre o bot sniper em si, é importante entender a oportunidade que torna-os interessantes. Quando uma nova criptomoeda surge em uma corretora, seja uma criptomoeda 100% original ou apenas uma nova oferta dela (não estava na exchange antes) é comum que ocorra uma corrida para aproveitar o seu preço antes de sua possível valorização. Acontece que essa corrida de compradores justamente é o que faz o valor da cripto inflacionar rapidamente graças à alta demanda logo no seu lançamento. Assim, não é incomum em bons projetos que o preço suba dezenas ou até mesmo milhares de % em poucas horas assim que o ativo é listado, o que permite que se você vender no topo, tenha um lucro expressivo mesmo com investimentos pequenos.
No entanto, considerando que a janela de oportunidade é curta, como conseguir comprar o mais cedo possível no lançamento e vender antes do preço cair novamente?
Aí que entram os sniping bots ou bots sniper.
No mundo dos bots cripto chamamos de sniper os robôs que ficam monitorando um ativo que ainda não foi lançado, para comprá-lo assim que ele for listado, seja em um exchange centralizada ou descentralizada. Em exchanges centralizadas, como a Binance, podemos usar dos anúncios que a exchange promove para nos prepararmos, alertando o mercado de que um novo token será listado e então comprarmos ele rapidamente (sniperbot para Binance aqui).
Já em exchanges descentralizadas (dex), como Uniswap, não temos anúncios comerciais porque qualquer investidor pode, a qualquer momento, criar um liquidity pool de um novo par de tokens e colocá-lo à disposição para swap na dex. Assim, a todo momento surgem “novas” criptomoedas nas dex o que parece impossibilitar o trabalho para um bot sniper, certo? Errado.
Acontece que toda vez que um novo pool de liquidez é criado em uma dex, ele é feito diretamente na blockchain e dispara um evento. Esses eventos podem ser monitorados e portanto identificados por um robô que então pode iniciar um swap tão logo receba o “sinal”. Depois para vender, é mais simples ainda, ficando a critério do desenvolvedor a lógica da decisão de venda baseado em %, tempo, etc conforme preferir.
O que vamos fazer neste tutorial é justamente isso: um bot que vai ficar monitorando eventos da dex Uniswap na blockchain, e assim que um evento de pool de liquidez novo for identificado, ele vai comprar uma quantidade de tokens assim que possível.
#2 – Ambiente
Usaremos neste tutorial a tecnologia Node.js, cujo setup deve ser feito antes do projeto começar de fato. Baixe e instale diretamente do site oficial e caso tenha dificuldade, pode usar o vídeo abaixo. O vídeo também ensina a instalar o Visual Studio Code, que é a ferramenta de desenvolvimento que uso.
Você vai precisar também de uma carteira de criptomoedas e se já é usuário da Uniswap certamente você já tem uma. Usarei aqui a MetaMask como exemplo pois é uma carteira bem popular e que consigo dar algum suporte. Via de regra, qualquer carteira compatível com rede Ethereum vai servir. Caso ainda não tenha uma MetaMask, no vídeo abaixo eu ensino a criar uma (é grátis), apenas ignore a parte que ensino a configurar a rede BSC, o que não será necessário aqui, já que usaremos a rede de teste da Ethereum. Usarei aqui a rede Ethereum Sepolia (Testnet), mas você pode usar qualquer rede suportada pela Uniswap, com saldos de teste obtidos neste faucet.
Agora com saldo na carteira e ela apontada para a Sepolia Testnet, pode testar as suas configurações na Uniswap apontada para Sepolia também. Experimente se autenticar no app da Uniswap com sua carteira apontada para Testnet, escolher a rede testnet Sepolia na lista de redes e fazer alguns swaps a fim de ter algumas moedas na carteira de testes, recomendo WETH (wrapped token do ETH), pois costuma ser escolhida como par de novas moedas que é justamente o que estamos buscando aqui. No entanto, entenda que se um novo par, exemplo LTOOLS-XYZ for listado e você não tiver tokens XYZ na carteira, seu bot não conseguirá fazer swap, ok? É impossível prever e aproveitar 100% das oportunidades de listagens.
O próximo passo é obter um full node RPC da rede Ethereum para podermos nos conectar tanto para o monitoramento quanto para o envio dos swaps. Você pode obter um 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 Sepolia para você assim que conseguir entrar no painel. Guarde a API Key que vai receber, vamos precisar dela mais tarde, além de pegar também o endereço de websockets do seu nó (é o que começa com wss).
E com isso nós estamos com tudo preparado para começar a programar.
#3 – Criando o Projeto
Agora vamos criar nosso projeto Node.js, começando pela criação de uma pasta uniswap-sniper e inicialização de um projeto Node.js nela.
1 2 3 4 5 |
mkdir uniswap-sniper cd uniswap-sniper npm init -y |
Depois, vamos instalar as dependências que vamos precisar:
1 2 3 |
npm install dotenv ethers |
A saber:
- DotEnv: dependência para carregamento das variáveis de ambiente;
- Ethers: biblioteca para integração com a blockchain;
Agora crie um arquivo .env na raiz do seu projeto e coloque nele as seguintes variáveis:
- INFURA_WS_URL: a sua URL de WebSockets da Infura;
- TOKEN_ADDRESS: o endereço do contrato do token que vai usar para pagar/receber. Se estiver na Sepolia, use 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14 para o token WETH, por exemplo;
- WALLET: endereço público da carteira que vai usar para o bot (você pega ela na MetaMask);
- PRIVATE_KEY: chave privada da carteira que vai usar para o bot (você pega ela dentro da MetaMask, em Detalhes da Conta);
- NETWORK: o nome da rede que vai operar, no meu caso, sepolia;
- INFURA_API_KEY: a sua API Key da Infura;
- ROUTER_ADDRESS: o endereço do contrato de swap routing da Uniswap na rede em questão. No caso da Sepolia, é o SwapRouter02 0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E;
- FACTORY_ADDRESS: o endereço do contrato de pool factory da Uniswap na rede em questão. No caso da Sepolia, é 0x0227628f3F023bb0B980b67D528571c95c6DaC1c;
- AMOUNT_TO_BUY: a quantidade de TOKEN que será usada em cada compra, na escala ‘ether’, com até 18 casas decimais. Ex: 0.1;
Um ponto de atenção é com os endereços dos tokens ERC-20 que você vai monitorar, pois eles mudam de rede para rede. Usarei como exemplo aqui o monitoramento de pools criados que usem WETH, tudo na rede Ethereum Sepolia (Testnet).
Crie o arquivo index.js e coloque o código abaixo dentro dele, para carregar o .env e também para testar o carregamento.
1 2 3 4 5 6 7 8 9 10 11 12 |
require("dotenv").config(); const { ethers } = require("ethers"); const ROUTER_ADDRESS = process.env.ROUTER_ADDRESS; const FACTORY_ADDRESS = process.env.FACTORY_ADDRESS; const WALLET = process.env.WALLET; const TOKEN = process.env.TOKEN_ADDRESS; const AMOUNT_TO_BUY = ethers.parseUnits(process.env.AMOUNT_TO_BUY, "ether"); let isOpened = false, isApproved = false, amountOut = 0; |
Além de carregar o .env para memória, o código acima importa as bibliotecas que vamos usar e guarda as variáveis de ambiente em constantes locais, para facilitar seu uso mais tarde. Também adicionei algumas variáveis auxiliares ao final do código, que vamos precisar e recomendo que coloque algum console.log também, para ver mais facilmente se subiu o projeto com sucesso.
Ainda sobre o código acima, a maioria das constantes e variáveis ainda serão usadas ao longo do código, quando serão melhor explicadas.
Agora ajuste o seu package.json, na seção scripts, para iniciar o index.js, que será o arquivo principal da aplicação.
1 2 3 4 5 |
"scripts": { "start": "node index.js" }, |
Assim, se quiser fazer um teste, use sempre o comando abaixo.
1 2 3 |
npm start |
O resultado deve ser o código que colocou no index.js, se tiver algum console.log.
#4 – Monitorando os Eventos
A primeira coisa que vamos programar em nosso bot é o monitoramento dos eventos de criação de pools na Uniswap. Eu discuti bastante o monitoramento ou escuta de eventos, nos mínimos detalhes, neste tutorial, então aqui vamos ser mais diretos.
Primeiro, vamos precisar de 3 ABIs, sendo que o primeiro é para comunicação com a Pool Factory da Uniswap e os outros dois usaremos mais à frente, quando estivermos nos preocupando com swaps.
Abaixo o ABI da Uniswap V3 Factory, discutido aqui, que você deve salvar na raiz do seu projeto em um arquivo abi.factory.json.
1 2 3 |
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint24","name":"fee","type":"uint24"},{"indexed":true,"internalType":"int24","name":"tickSpacing","type":"int24"}],"name":"FeeAmountEnabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token0","type":"address"},{"indexed":true,"internalType":"address","name":"token1","type":"address"},{"indexed":true,"internalType":"uint24","name":"fee","type":"uint24"},{"indexed":false,"internalType":"int24","name":"tickSpacing","type":"int24"},{"indexed":false,"internalType":"address","name":"pool","type":"address"}],"name":"PoolCreated","type":"event"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"}],"name":"createPool","outputs":[{"internalType":"address","name":"pool","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"}],"name":"enableFeeAmount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint24","name":"","type":"uint24"}],"name":"feeAmountTickSpacing","outputs":[{"internalType":"int24","name":"","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint24","name":"","type":"uint24"}],"name":"getPool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"parameters","outputs":[{"internalType":"address","name":"factory","type":"address"},{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"setOwner","outputs":[],"stateMutability":"nonpayable","type":"function"}] |
Abaixo o ABI da Uniswap V3 SwapRouter02, discutido aqui, que você deve salvar na raiz do seu projeto em um arquivo abi.router.json.
1 2 3 |
[{"inputs":[{"internalType":"address","name":"_factoryV2","type":"address"},{"internalType":"address","name":"factoryV3","type":"address"},{"internalType":"address","name":"_positionManager","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":[{"internalType":"address","name":"token","type":"address"}],"name":"approveMax","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveMaxMinusOne","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveZeroThenMax","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveZeroThenMaxMinusOne","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes","name":"data","type":"bytes"}],"name":"callPositionManager","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"paths","type":"bytes[]"},{"internalType":"uint128[]","name":"amounts","type":"uint128[]"},{"internalType":"uint24","name":"maximumTickDivergence","type":"uint24"},{"internalType":"uint32","name":"secondsAgo","type":"uint32"}],"name":"checkOracleSlippage","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint24","name":"maximumTickDivergence","type":"uint24"},{"internalType":"uint32","name":"secondsAgo","type":"uint32"}],"name":"checkOracleSlippage","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct IV3SwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IV3SwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"}],"internalType":"struct IV3SwapRouter.ExactOutputParams","name":"params","type":"tuple"}],"name":"exactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IV3SwapRouter.ExactOutputSingleParams","name":"params","type":"tuple"}],"name":"exactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"factoryV2","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"getApprovalType","outputs":[{"internalType":"enum IApproveAndCall.ApprovalType","name":"","type":"uint8"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"}],"internalType":"struct IApproveAndCall.IncreaseLiquidityParams","name":"params","type":"tuple"}],"name":"increaseLiquidity","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"internalType":"struct IApproveAndCall.MintParams","name":"params","type":"tuple"}],"name":"mint","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"previousBlockhash","type":"bytes32"},{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"positionManager","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"pull","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"}],"name":"swapTokensForExactTokens","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"value","type":"uint256"}],"name":"wrapETH","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}] |
E abaixo o ABI de ERC20 Token, largamente discutido aqui, que você deve salvar na raiz do seu projeto em um arquivo abi.erc20.json.
1 2 3 |
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}] |
Agora vamos carregar esses três ABIs logo abaixo das demais variáveis e constantes que definimos no index.js:
1 2 3 4 5 |
const ABI_ROUTER = require("./abi.router.json"); const ABI_FACTORY = require("./abi.factory.json"); const ABI_ERC20 = require("./abi.erc20.json"); |
Ao final do index.js, adicione uma função start que será responsável pela inicialização do robô, com o código abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function start() { const wsProvider = new ethers.WebSocketProvider(process.env.INFURA_WS_URL); const factory = new ethers.Contract(FACTORY_ADDRESS, ABI_FACTORY, wsProvider); factory.on("PoolCreated", async (token0, token1, fee, tickSpacing, pool) => { console.log('Snipe!'); console.log(token0, token1, fee, tickSpacing, pool); }); setInterval(() => wsProvider.websocket.ping(), 60000); console.log("Esperando um pool ser criado!"); } start(); |
O que esse código faz é:
- configura um provedor de websocket apontado para o seu server na Infura (para receber dados em tempo real);
- configura um objeto de comunicação com o contrato Factory da Uniswap V3 (responsável pela criação dos pools);
- definimos um listener que vai ficar escutando via websocket os eventos do tipo PoolCreated, que são disparados sempre que um novo pool é criado;
- imprime no console as informações do novo pool
Adicionei ao final um PING a cada 1 minuto, para garantir que a conexão com a Infura se mantenha ativa, caso contrário ela pode ser derrubada por ociosidade se ficar vários minutos sem uso.
Com isso rodando, você já tem um bot monitorando o surgimento de novos pools de liquidez (aka novos tokens na dex). Deixe ele rodando e vai perceber como funciona ou então use o contrato de factory manualmente para criar um pool e ver funcionando mais rápido (o pool não terá liquidez, mas irá disparar nosso monitoramento). Caso queira adicionar um pool já com liquidez, terá de criar um token ERC20 novo e adicionar pela interface web da Uniswap na Testnet.
A próxima etapa é justamente fazer a compra, mas este é um assunto para a parte dois deste tutorial, neste link.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.
Boa noite Luiz, você disse: “TOKEN_ADDRESS: o endereço do contrato do token que vai usar para pagar/receber. Se estiver na Goerli, use 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6 para o token WETH, por exemplo, aonde encontro o endereço pagar/receber da SEPOLIA ?
Tem várias formas de achar esse endereço. Uma forma é ir no block explorer e pesquisar pelo nome do token, apenas dê uma olhada no contrato para ver se é o correto (existem muitos fakes). Outra forma é ir no app da dex, selecionar a sua rede e fazer um swap, aí vai aparecer na sua transação os endereços corretos que a dex usa.
Obrigado Luiz pela presteza mano.
Eu que agradeço pela gentileza do comentário!