Recentemente escrevi um tutorial onde ensinei a primeira parte da construção de um bot cripto para a dex (exchange decentralizada) PancakeSwap. Na primeira parte eu ensinei como preparar o ambiente de desenvolvimento, como estruturar o projeto e a fazer o monitoramento do preço, usando uma lógica simples para decidir quando deveríamos comprar e quando deveríamos vender.
Nesta segunda parte a minha missão é lhe ajudar a implementar a compra e venda (swap) de tokens. Para isso temos dois cenários possíveis: swap envolvendo BNB e swap não envolvendo BNB. Essa diferença é porque o BNB é o token nativo da rede BNB Smart Chain (BSC) e portanto possui funções de swap específicas para ele. Já qualquer outro token usará as funções genéricas de swap.
Sendo assim, nesta etapa nós vamos implementar o swap de tokens que não sejam BNB, combinado? Este algoritmo servirá como base para no futuro implementar o swap de BNB também, além de deixar uma série de funções prontas.
Então vamos lá!
#1 – Conectando na Carteira
O primeiro passo é obter uma conexão do seu bot com a sua carteira de criptomoedas, sendo que neste exemplo estou usando a MetaMask. Quando você criou a sua MetaMask (o que fizemos no passo anterior) você recebeu um endereço público e um frase mnemônica, certo? Estas duas informações devem estar agora no seu arquivo .env, sob as variáveis WALLET e MNEMONIC. Também incluímos no .env uma variável PROVIDER_URL que indica a URL de um full node que usaremos para nos conectar na BSC.
Resumindo: usaremos o full node para conexão na blockchain e a carteira para realizar as transações.
Abrindo o nosso api.js, vamos importar a biblioteca Ethers e vamos criar uma função getProvider, que faz a configuração de um objeto de conexão com o full node caso ela ainda não exista, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 |
const { ethers } = require('ethers'); let provider; function getProvider() { if (!provider) provider = new ethers.JsonRpcProvider(process.env.PROVIDER_URL); return provider; } |
Repare que uso a classe JsonRpcProvider passando a URL do nosso full node. Simples assim, armazenando a conexão em uma variável “estilo” Singleton. Isto ainda não é a conexão com o full node, mas a configuração do provider.
Agora com esta função, vamos fazer a conexão no full node usando a carteira MetaMask do usuário através de uma função getWallet.
1 2 3 4 5 6 7 8 9 10 11 12 |
let walletInstance; function getWallet() { if (!walletInstance) { const provider = getProvider(); const wallet = ethers.Wallet.fromPhrase(process.env.MNEMONIC); walletInstance = wallet.connect(provider); } return walletInstance; } |
Primeiro obtemos o objeto de configuração do provider, depois recuperamos a carteira usando a frase mnemônica (que é equivalente a ter a chave privada da carteira) e com isso finalmente fazemos a conexão no full node usando a mesma. Com essa conexão, podemos enviar nossas transações para a blockchain a fim de fazer qualquer operação permitida por um smart contract, como os swaps.
Experimente chamar a função getWallet apenas para fins de teste, no index.js e verá se ela está funcionando ou não, já que caso tenha algo errado você terá um erro indicando o que errou. O erro mais comum nestes casos é o de endereço da carteira ou a frase mnemônica estarem errados ou não serem da mesma carteira, então verificar mais de uma vez pode ser uma boa dica aqui.
#2 – Fazendo Swap de Tokens
Agora que já sabemos como fazer a conexão no full node com controle total da nossa carteira cripto, é hora de implementarmos a primeira etapa da função que faz o swap dos tokens, lembrando que você não pode fazer swap de BNB desta forma que vou ensinar aqui, vai dar erro se tentar. Além disso, é importante salientar que todas transações na BSC exigem o pagamento da taxa de gás, que deve ser paga sempre em BNB. Ou seja, apesar de não estar negociando BNBs neste bot, você deve ter BNB na carteira a fim de pagar as taxas, ok?
Volte ao seu arquivo index.js e crie uma nova função, swapTokens, que vai ser usada para…bem…swap de tokens, hehe.
Para que você possa fazer swap de tokens é importante entender um pouco do ABI, ou Application Binary Interface, do contrato de roteamento da PancakeSwap. O contrato de roteamento é o Smart Contract responsável pelo swap dos tokens nas DEX. O ABI nada mais diz do que as regras para chamar as funções do contrato, sendo que existem várias funções ligadas a swap nele e a que julgo interessante de usarmos aqui é a swapExactTokensForTokens.
Nesta função, você tem a seguinte assinatura no ABI e consequentemente na documentação.
1 2 3 |
function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts) |
Isso quer dizer que o nome da função swapExactTokensForTokens e que ela espera 5 parâmetros.
- amountIn: quantos tokens você quer gastar/enviar neste swap;
- amountOutMin: o mínimo que você aceita receber do token-alvo neste swap (pode usar “0” se aceitar o máximo que der, não importando a cotação);
- path: um array contendo os endereços de roteamento para que essa negociação ocorra (tipicamente o endereço do contrato do token a ser gasto e o do token a ser recebido);
- to: qual carteira vai fazer a transação;
- deadline: a data limite para a transação ser aprovada;
Além dos parâmetros o ABI define outras coisas, mas que não nos interessam aqui. Basicamente o que precisamos fazer é usar a biblioteca Ethers para nos conectar no full node, o que já vimos como fazer anteriormente, nos conectarmos no contrato de roteamento e fazermos a transação a partir dele. Tem alguns detalhes neste processo e vou explicando aos poucos pois realmente são vários detalhes.
Abaixo, a primeira parte da função. Nos parâmetros dela, vamos esperar que o usuário informe o endereço da carteira, o endereço do contrato ‘from’ (token que será gasto), a quantidade que vai gastar e o endereço do contrato ‘to’ (token que vai receber).
1 2 3 4 5 6 7 8 9 10 11 |
const ROUTER_CONTRACT="0xD99D1c33F9fC3444f8101754aBC46c52416550D1"; async function swapTokens(wallet, tokenFrom, quantity, tokenTo) { const account = getWallet(); const contract = new ethers.Contract( ROUTER_CONTRACT, ["function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts)"], account); } |
O primeiro passo é a conexão no full node através da carteira, o que podemos fazer facilmente apenas chamando a função anteriormente criada, getWallet.
A seguir, vamos usar a classe Contract da biblioteca Ethers para fazer a configuração do objeto que vai transacionar com o contrato da DEX. O endereço deste contrato eu coloquei em uma constante, sendo que neste exemplo de código está o endereço da PancakeSwap na Testnet. Mais tarde, troque para Mainnet (0x10ED43C718714eb63d5aA57B78B54704E256024E) quando estiver pronto para operar em produção.
O segundo parâmetro do constructor da classe Contract é um array contendo as funções presentes no ABI. Via de regra deveríamos colocar todas funções do ABI aí, cada uma em uma string do array, mas como só vamos usar uma, só vou colocar ela. Esse código você tira do ABI do contrato, disponível na BSC Scan (basta acessar o endereço do contrato de roteamento) ou na documentação oficial da PancakeSwap e da UniSwap (já que a primeira é um fork da segunda).
E por fim, o terceiro parâmetro do constructor é o objeto account devidamente conectado no full node com a carteira.
Agora temos de configurar a quantidade de tokens que vamos enviar, em wei e o preço do gás a ser pago nesta transação. Faremos isso na linha logo abaixo da configuração do objeto contract.
1 2 3 4 |
const value = ethers.parseEther(quantity).toString(); const gasPrice = ethers.parseUnits('10', 'gwei'); |
A variável quantity veio nos parâmetros da nossa função, lembra? Aqui convertemos ela para a notação 10^18 (wei) e depois em string hexadecimal para garantir a precisão da informação.
Na linha de baixo estimamos o preço de gás em 10 gwei, sendo que esse valor pode variar com o passar do tempo, então caso tenha problemas de transação rejeitada por causa de preço de gás muito baixo (“Error: transaction underpriced”), ajuste o valor de acordo. Na data que escrevo este tutorial 10 gwei tem funcionado bem, mas um erro comum que você pode ter é o “Error: insufficient funds for gas * price + value” caso tenha especificado algum desses valores exageradamente alto ou o “Error: insufficient funds for intrinsic transaction cost”, na mesma situação.
Agora, para finalizarmos a função, vamos chamar a função do contrato e passarmos todos os parâmetros necessários para que a transação aconteça, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const tx = await contract.swapExactTokensForTokens(value, 0, [ tokenFrom, tokenTo ], wallet, Date.now() + 10000, { gasPrice, gasLimit: 250000 }); return tx; |
A função swapExacyTokensForTokens vai disparar a função homônima no contrato, onde o primeiro parâmetro é a string hexadecimal contendo a quantidade de tokens a serem gastos/enviados, o segundo parâmetro é o mínimo que você espera receber, o terceiro parâmetro é o array com os endereços dos contratos que fazem parte do swap (recebidos via parâmetro da função da api.js), seguido do endereço da carteira que vai fazer a transação, do deadline (prazo máximo) para a transação ser concluída e as configurações de gás.
A deadline eu coloquei em 10 segundos no futuro, mas isso talvez você precise ajustar se quiser correr menos risco de variação de preço (diminua a deadline) ou estiver tendo problemas de não estar tendo tempo suficiente para as transações acontecerem (aumente a deadline, caso a rede esteja sobrecarregada).
Já as configurações de gás estou usando o gasPrice que definimos antes e o gasLimit como 25 mil, que você deve ajustar caso tenha problemas já que essas taxas de gás mudam dependendo da rede e do congestionamento da mesma. Se você não informar estas configurações terá um erro como “UNPREDICTABLE_GAS_LIMIT” ou “Cannot estimate gas; transaction may fail or may require manual gas limit”, indicando que é uma configuração obrigatória.
No final, apenas retorno a transação.
Isso quase deixa nossa função pronta. Quase.
Experimente chamar esta função no seu index.js, seja para comprar ou para vender, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
const api = require('./api'); const WALLET = process.env.WALLET; const PRICE_TO_BUY = 1; const PROFITABILITY = 1.1;//10% const CRAWLER_INTERVAL = 3000; const WBNB_TESTNET = "0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd"; const CAKE_MAINNET = "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82"; const CAKE_TESTNET = "0xf9f93cf501bfadb6494589cb4b4c15de49e85d0e"; let isOpened = false; setInterval(async () => { const price = await getPrice(CAKE_MAINNET); console.log(price); if (price < PRICE_TO_BUY && !isOpened) { console.log("Cheap. Time to buy."); isOpened = true; api.swapTokens(WALLET, WBNB_TESTNET, '0.1', CAKE_TESTNET)//change 0.1 WBNB for as many CAKES as possible .then(result => console.log(result)) .catch(err => console.error(err)); } else if (price > (PRICE_TO_BUY * PROFITABILITY) && isOpened) { console.log("Expensive. Time to sell."); isOpened = false; api.swapTokens(WALLET, CAKE_TESTNET, '10', WBNB_TESTNET)//change 10 CAKE for as many WBNB as possible .then(result => console.log(result)) .catch(err => console.error(err)); } }, CRAWLER_INTERVAL) |
Isso vai quase funcionar.
#3 – Aprovando a transferência
O código acima fará com que a transação seja corretamente gerada e enviada para a blockchain. Inclusive você vai receber um recibo da transação como retorno, como abaixo.
No entanto, você vai verificar que nada vai acontecer no saldo das moedas da sua carteira. Então experimente pegar o valor do campo hash e colocar no BSC Scan da Testnet e verá algo como neste exemplo, que contém a mensagem “Fail with error ‘TransferHelper: TRANSFER_FROM_FAILED'”, o que indica que internamente o contrato da DEX tentou fazer uma operação transferFrom, nativa dos tokens ERC20 (e BEP20 por consequência, pela BSC ser um fork da ETH) mas falhou.
Mas por que falhou, se o código estava todo correto?
Isso porque a função transferFrom, conforme especificação ERC-20, permite que um contrato faça transferência de fundos de uma conta para outra, no entanto isso requer uma aprovação prévia (allowance) do dono da carteira que terá seus fundos transferidos delegando essa permissão para o contrato que fará a transferência, a DEX neste caso.
Resumindo: a DEX não pode sair transferindo seus tokens sem a sua aprovação prévia e é isso o que aconteceu aqui. Tentamos fazer a transferência, mas mesmo com a chave privada em mãos, sem autorização prévia e específica para o contrato da DEX, não rola.
Para dar esta permissão vamos criar mais uma função em nosso api.js, que vou chamar de approve, já que este é o nome presente na especificação. Se olharmos olharmos a mesma veremos que a função approve do smart contract deve ter a seguinte assinatura (em Solidity).
1 2 3 |
function approve(address _spender, uint256 _value) public returns (bool success) |
O primeiro parâmetro é o endereço do contrato que vamos dar a permissão e o segundo parâmetro é a quantidade que vamos autorizar deste token. Esta função approve está presente em todos os contratos de token, e devemos chamá-la sempre antes de um transferFrom onde vamos gastar/enviar tokens daquele contrato em questão.
Assim, uma versão da função approve no api.js pode ser vista abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const ROUTER_CONTRACT="0xD99D1c33F9fC3444f8101754aBC46c52416550D1"; async function approve(tokenToSend, quantity) { const account = getWallet(); const contract = new ethers.Contract( tokenToSend, ["function approve(address _spender, uint256 _value) public returns (bool success)"], account ); return contract.approve(ROUTER_CONTRACT, quantity); } |
Primeiro obtemos a conexão com o full node através da carteira. Depois, configuramos o contrato com o token que pretendemos gastar/enviar, você vai precisar do endereço do contrato dele aqui.
O parâmetro seguinte é o ABI, que só precisamos ter a função approve, e por último o objeto de conexão à rede.
Uma vez com o contrato configurado para o token que vamos dar a permissão, nós chamamos a função approve passando o endereço do contrato de roteamento da PancakeSwap (que é quem vai gastar os tokens em nosso nome) e a quantidade que pretendemos gastar em nossa próxima transação.
Com esta função pronta, basta voltarmos à nossa função swapTokens e chamar a função approve uma linha antes da swapExactTokensForTokens que faz a transação de fato.
1 2 3 4 5 |
await approve(tokenFrom, value); const tx = await contract.swapExactTokensForTokens(value, 0, [ ... |
Isso ajusta o último ponto que estava pendente para que nossa função de swap de tokens (não-BNB) funcione corretamente.
Se rodar agora o programa, quando ele entrar na sua lógica de compra ou de venda, vai ver que a transação vai executar com sucesso.
E com isso finalizamos mais uma etapa da nossa saga de construção de um bot para PancakeSwap. Na próxima e última etapa, quero lhe mostrar como fazer swap quando envolver o token BNB, seja de compra ou venda. Clique aqui para ler.
Teve algum problema? Confira este artigo com os erros mais comuns!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.
Otimo mas não tenho capacidade para escrever um contrato e sim copiar e pegar. Enviei uma mensagem no face mas vc não respondeu. Queria ter un flashloan pronto, para copiar e pegar para ser usado lançado na blockchain pelo remix com os valores do emprestimo na AAVE sendo escolhido através do remix minimo 1000 e liberdade do contrato para escolha das compra e vendas, fazer a arbitragem, a carteira metamask será conectada quando introduza web 3 no remix automaticamente contratos 0.5.0 e 0.6.0 entre outros moeda base BNB. tenho codigos prontos mas pode que haja desvio no retorno do lucro para outra carteira e nã posso confiar. Por favor quanto poderia custar e quando poderia ter pronto. obrigado. Meu zap61992216906
Infelizmente eu não presto este tipo de serviço para terceiros, trabalho apenas com cursos de programação. Tome cuidado com códigos de terceiros que você não consegue entender como foram programados, todos os dias alguém me manda mensagem desesperado que caiu em algum golpe envolvendo robôs baixados da Internet.
Olá, primeiro gostaria de lhe agradecer por compartilhar seu conhecimento e dizer que a sua didática é ótima. fiz o passo a passo do robo em questão mas estou com alguns erros que não consegui resolver. Vc poderia me ajudar ? EE Error: expected TYPE; got {“depth”:1,”linkBack”:-13,”linkNext”:-13,”match”:-13,”type”:”ID”,”text”:”boll”,”offset”:67,”value”:-1}
at TokenString.popType (C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\fragments.js:81:19)
at ParamType.from (C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\fragments.js:639:44)
at C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\fragments.js:298:52
at Array.map ()
at consumeParams (C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\fragments.js:298:31)
at FunctionFragment.from (C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\fragments.js:1248:27)
at Fragment.from (C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\fragments.js:745:58)
at Fragment.from (C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\fragments.js:733:29)
at new Interface (C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\interface.js:253:52)
at Interface.from (C:\NodeProjects\bot_pancake\node_modules\ethers\lib.commonjs\abi\interface.js:1102:16)
C:\NodeProjects\bot_pancake\index.js:28
return contract.approve(process.env.ROUTER_CONTRACT, value);
^
TypeError: contract.approve is not a function
at approve (C:\NodeProjects\bot_pancake\index.js:28:19)
at swapTokens (C:\NodeProjects\bot_pancake\index.js:42:15)
at fetchBNBPrice (C:\NodeProjects\bot_pancake\index.js:114:13)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
TypeError: contract.approve is not a function
Isso quer dizer que ele não reconheceu a função approve no contrato em questão. Pode ser endereço do contrato errado ou ABI errado.
Tem como fazer um vídeo atualizado?
O tutorial em texto está 100% atualizado, recomendo seguir por ele. O vídeo tem apenas alguns detalhes que precisam ser atualizados (em relação à obtenção de cotação), mas não há previsão para isso acontecer no momento em virtude de outras prioridades.