Há algum tempo eu escrevi a primeira parte deste tutorial, sobre como fazer integração do seu site ou aplicação web com a carteira de criptomoedas MetaMask, usando apenas JavaScript.
Isso é especialmente útil para quem estiver criando um dapp (aplicação distribuída), aplicação para web 3.0 ou apenas querendo integrar a MetaMask como meio de pagamento cripto.
No entanto, após a publicação da primeira parte, a dúvida número 1 da minha audiência foi: como lidar como outros tokens que não sejam BNB? Isso porque como conectamos na BSC (Binance Smart Chain), a criptomoeda dela é o BNB e qualquer saldo, transação, etc estará cotado nativamente nesta moeda. O mesmo vale para o Ether (ETH) se estivéssemos conectados na rede Ethereum ou o Bitcoin (BTC) se estivéssemos conectados na rede de mesmo nome.
Para entender melhor como fazer e consultar transações de outros tokens, valem algumas explicações antes. Caso prefira assistir ao invés de ler, o vídeo abaixo contém o mesmo conteúdo deste tutorial.
#1 – Contratos Inteligentes
Você pode assistir ao vídeo abaixo que passa o mesmo conteúdo desta seção, ao invés de ler.
Quando o Bitcoin surgiu em 2008 para 2009 o seu foco, conforme consta no seu paper original, era ser um dinheiro eletrônico seguro e confiável P2P, ou seja, sem intermediários. O seu foco é e possivelmente sempre será ser “apenas” uma moeda digital, o que chamamos de criptomoeda 1.0.
Quando Vitalik Buterim criou o Ethereum em 2015 ele já pensava diferente. Ele viu na tecnologia blockchain a possibilidade de construir uma plataforma. Nesta plataforma, qualquer um poderia criar algoritmos que rodariam de maneira distribuída e descentralizada, incluindo novas criptomoedas, ou tokens, como passariam a ser chamadas as moedas não-nativas de uma blockchain. A esses algoritmos deu-se o nome de contratos inteligentes ou smart contracts.
Na prática, um contrato inteligente é como se fosse uma classe Java ou C#, mas que roda na blockchain, ou seja, sem um endereço central e específico, sem estar atrelado a um servidor. O programador do contrato inteligente faz deploy dele na blockchain e então o algoritmo passa a estar disponível publicamente, sendo executado por uma máquina virtual (EVM no caso da Etehereum) toda vez que requisitado e gravando os dados de suas transações na própria blockchain (o que incorre no famoso custo de gás). Isso é o que chamamos de criptomoeda 2.0 ou segunda geração.
Assim, contei toda essa história para lhe ajudar a entender que, qualquer que seja a atividade que você deseje fazer em uma blockchain 2.0 em diante que não seja consultar ou fazer transações com sua criptomoeda nativa, será feita através de contratos inteligentes. E isso responde, na teoria, a pergunta mais comum que recebi no tutorial anterior que é como consultar saldo e fazer transferência de outros tokens que o usuário possa ter na MetaMask: através de contratos inteligentes.
#2 – Contratos de Tokens
O tipo de smart contract mais comum que temos são os de novos tokens. Um contrato de token é um smart contract que atende a um padrão comum como ERC20 (blockchain ETH) ou BEP20 (blockchain BSC). Nestes padrões é dito que todo smart contract de token deve ter uma função para saldo, outra para transferência e assim por diante. Essas funções são definidas em uma interface de programação chamada de ABI (Application Binary Interface) e uma vez que o programador escreva sua ABI corretamente, todo o ecossistema de aplicações que se comunicam com ERC20, BEP20, etc vai conseguir se comunicar com o contrato dele.
No caso da MetaMask, ela é uma carteira que se comunica com a blockchain da Ethereum, o que inclui tokens no padrão ERC20 e compatíveis. Quando me refiro a compatíveis quero dizer a forks da blockchain ETH, como a própria BSC. O padrão de tokens da BSC é o BEP20, que é praticamente um copia e cola do ERC20. Então tanto a MetaMask funciona para a rede da BSC quanto qualquer programação que fizermos para uma, vai funcionar na maioria dos casos para a outra.
É por isso que uma biblioteca como a Ethers JS, claramente feita para Ethereum, funciona tão bem com a BSC.
E é por isso que as regras para consultar saldos e fazer transações na BSC é do mesmo jeito que na ETH: através de smart contracts. E é isso que nós vamos aprender no tutorial de hoje: como interagir com smart contracts de tokens usando a MetaMask.
#3 – MetaMask Service
Seguiremos aplicando os conhecimentos da biblioteca EthersJS na mesma aplicação ReactJS que criamos na primeira parte. Não que isso faça grande diferença já que estamos usando JS puro, aplicável a qualquer outra tecnologia de frontend web como Angular e VueJS.
Vamos começar reorganizando a nossa aplicação, criando um arquivo MetaMaskService.js junto dos demais para que seja o nosso service, ou seja, a camada de comunicação da aplicação com recursos externos. Bem pra organização mesmo.
Neste service nós vamos começar adicionando as funções de saldo e transferência de BNB que já conhecemos e que dispensam explicações. Apenas vou dar nomes sugestivos a elas e garantir que elas sejam bem desacopladas em relação à aplicação em si, realmente fornecendo um serviço de consulta de saldo e transferência.
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 33 |
import { ethers } from 'ethers'; async function getMetaMaskProvider() { if (!window.ethereum) throw new Error(`No MetaMask found!`); const provider = new ethers.BrowserProvider(window.ethereum); const accounts = await provider.send('eth_requestAccounts'); if(!accounts || !accounts.length) throw new Error(`No MetaMask account allowed`); return provider; } export async function getBnbBalance(address) { const provider = await getMetaMaskProvider(); const balance = await provider.getBalance(address); return ethers.formatEther(balance.toString()); } export async function transferBnb(toAddress, quantity) { const provider = await getMetaMaskProvider(); const signer = await provider.getSigner(); ethers.getAddress(toAddress);//valida endereço const tx = await signer.sendTransaction({ to: toAddress, value: ethers.parseEther(quantity) }) await tx.wait(); return tx; } |
Repare apenas que como a verificação da existência da MetaMask no navegador e o carregamento do provider são tarefas comuns a qualquer ação que vamos fazer, coloquei isso dentro de uma função específica, que chamaremos sempre que necessário, a getMetaMaskProvider.
Agora você pode ajustar o código do seu App.js para que ele use as novas funções, deixando-o bem mais organizado.
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
import { useState } from 'react'; import { getBnbBalance, transferBnb } from './MetaMaskService'; function App() { const [address, setAddress] = useState("<SUA CARTEIRA>"); const [balance, setBalance] = useState(''); const [toAddress, setToAddress] = useState(""); const [quantity, setQuantity] = useState(""); const [message, setMessage] = useState(''); async function checkBalance() { let balance = await getBnbBalance(address); setBalance(balance); setMessage(``); } async function transfer() { let result = await transferBnb(toAddress, quantity); setMessage(JSON.stringify(result)); } return ( <div> <p> My Address : <input type="text" onChange={evt => setAddress(evt.target.value)} value={address} /> </p> <p> <input type="button" value="See Balance" onClick={evt => checkBalance()} /> </p> <p> Balance: {balance} </p> <hr /> <p> To Address: <input type="text" onChange={evt => setToAddress(evt.target.value)} /> </p> <p> Qty: <input type="text" onChange={evt => setQuantity(evt.target.value)} /> </p> <p> <input type="button" value="Transfer" onClick={evt => transfer()} /> </p> <hr /> <p> {message} </p> </div > ); } export default App; |
E com isso voltamos ao mesmo tempo que já estávamos na lição passada, mas agora com um código mais evoluído e com um entendimento melhor do que faremos a seguir. Basicamente precisamos nos conectar aos contratos dos tokens que desejamos usar e mandar comandos para eles, através da MetaMask, ao invés de mandar para a BSC em si.
#4 – Token Functions
Vamos voltar ao nosso MetaMaskService e vamos adicionar duas novas funções, bem parecidas com as anteriores, mas usando contratos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
export async function getTokenBalance(address, contractAddress, decimals = 18) { const provider = await getMetaMaskProvider(); const contract = new ethers.Contract(contractAddress, CONTRACT_ABI, provider); const balance = await contract.balanceOf(address) return ethers.formatUnits(balance, decimals); } export async function transferToken(toAddress, contractAddress, quantity, decimals = 18) { const provider = await getMetaMaskProvider(); const signer = await provider.getSigner(); const contract = new ethers.Contract(contractAddress, CONTRACT_ABI, sginer); ethers.getAddress(toAddress);//valida endereço const tx = await contract.transfer(toAddress, ethers.parseUnits(quantity, decimals)); await tx.wait(); return tx; } |
A primeira função, getTokenBalance, espera o endereço da carteira e o endereço do contrato do token que você deseja ver o saldo. Um terceiro parâmetro opcional é para a formatação de casas decimais. O início da função conecta no provider, ou seja, na carteira MetaMask, como já fizemos antes. Depois, ele carrega o contrato em si, usando o endereço informado (contrato do token) e passamos ainda o ABI e o provider já conectado e autorizado.
E aqui valem duas explicações. Primeiro, sobre o ABI. Eu mencionei anteriormente o que é o ABI e aqui precisaremos informá-lo manualmente. Como vamos trabalhar com tokens ERC20/BEP20, eu peguei o ABI padrão disponível na documentação. Inclua este conteúdo em um arquivo ABI.json no mesmo nível do MetaMaskService.js:
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"}] |
E depois carregue ele no topo do MetaMaskService.js.
1 2 3 |
import CONTRACT_ABI from "./ABI.json"; |
Com a informação do endereço do contrato e do ABI, a biblioteca Ethers vai conseguir fornecer um objeto de contrato que faça as ações que esperamos como ver saldo e fazer transferências.
Voltando ao código, é exatamente isso que a primeira função faz, usa deste objeto de contrato carregado para chamar uma das funções do ABI que é a balanceOf.
Já na segunda função nós pegamos o provedor, a autorização para assinar transações e usamos tudo isso junto ao contrato para fazer a transferência passando por ele.
Agora, para que possamos usar estas funções, teremos de informar na interface da aplicação o campo para o usuário incluir o endereço do contrato.
#5 – Contrato na Interface
Ao invés de colocar um campo de texto pro usuário informar o contrato, eu vou optar por fornecer um select com várias opções comuns. Menos flexível, mas com usabilidade melhor.
Em um cenário de produção, o usuário poderia ir no site da CoinMarketCap, procurar o token que deseja manipular e pegar o endereço do contrato dele na BSC. No entanto, como estamos desenvolvendo tudo apontado para Testnet eu estou usando a Testnet do BSC Scan para encontrar os endereços dos contratos de teste, lembrando que nem todos tokens estarão por lá.
Abaixo, as alterações no App.js para termos um state pro contrato do token, um select na interface já com alguns tokens comuns (e seus endereços na Testnet) e a lógica para chamar as funções apropriadas quando o token selecionado for BNB ou outros.
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
import { useState } from 'react'; import { getTokenBalance, getBnbBalance, transferBnb, transferToken } from './MetaMaskService'; function App() { const [address, setAddress] = useState("<SUA CARTEIRA>"); const [contract, setContract] = useState("BNB"); const [balance, setBalance] = useState(''); const [toAddress, setToAddress] = useState(""); const [quantity, setQuantity] = useState(""); const [message, setMessage] = useState(''); async function checkBalance() { let balance; if (contract === "BNB") balance = await getBnbBalance(address); else balance = await getTokenBalance(address, contract); setBalance(balance); setMessage(``); } async function transfer() { let result; if (contract === "BNB") result = await transferBnb(toAddress, quantity); else result = await transferToken(toAddress, contract, quantity); setMessage(JSON.stringify(result)); } return ( <div> <p> My Address : <input type="text" onChange={evt => setAddress(evt.target.value)} value={address} /> </p> <p> <select className="form-select" onChange={evt => setContract(evt.target.value)}> <option value="BNB">BNB</option> <option value="0x53598858bC64f5f798B3AcB7F82FF2CB2aF463bf">BTC</option> <option value="0xd66c6B4F0be8CE5b39D52E0Fd1344c389929B378">ETH</option> <option value="0x64544969ed7EBf5f083679233325356EbE738930">USDC</option> </select> <input type="button" value="See Balance" onClick={evt => checkBalance()} /> </p> <p> Balance: {balance} </p> <hr /> <p> To Address: <input type="text" onChange={evt => setToAddress(evt.target.value)} /> </p> <p> Qty: <input type="text" onChange={evt => setQuantity(evt.target.value)} /> </p> <p> <input type="button" value="Transfer" onClick={evt => transfer()} /> </p> <hr /> <p> {message} </p> </div > ); } export default App; |
O resultado é que agora o usuário pode selecionar qual token ele deseja ver o saldo, bem como fazer transferências do mesmo.
Legal, não?
#6 – Consultando Transações
Uma última funcionalidade que podemos adicionar é um verificador de transações. Toda transação realizada na blockchain pode ser consultada a fim de verificarmos se ela foi bem sucedida ou não. Embora você possa fazer essa consulta manualmente nos exploradores como EtherScan, BSC Scan, etc; é de extrema utilidade que seu próprio software verifique e podemos novamente usar o provider MetaMask para isso em conjunto com a EthersJS.
Adicione mais uma função no MetaMaskService.js
1 2 3 4 5 6 7 |
export async function getTransaction(hash){ const provider = await getMetaMaskProvider(); const tx = await provider.getTransactionReceipt(hash); return tx; } |
E agora, no App.js crie um novo state, importe a nova função e chame ela imprimindo as duas informações mais importantes do resultado.
1 2 3 4 5 6 7 8 9 10 11 12 |
import { getTokenBalance, getBnbBalance, transferBnb, transferToken, getTransaction } from './MetaMaskService'; const [transaction, setTransaction] = useState(""); async function checkTransaction() { const result = await getTransaction(transaction); setMessage(` Status: ${result.status} Confirmations: ${result.confirmations}`); } |
E no HTML, adicione mais um pedaço para utilizar este state.
1 2 3 4 5 6 |
<p> Transaction: <input type="text" value={transaction} onChange={evt => setTransaction(evt.target.value)} /> <input type="button" value="Check" onClick={evt => checkTransaction()} /> </p> |
O resultado é que agora você tem um campo onde informa o hash de uma transação (feita por você ou não) e um botão que quando clicado traz todos os detalhes da transação, mas principalmente se deu tudo certo (status 1) e quantas confirmações já teve.
Com isso, agora você tem MUITO mais ferramentas para integrar suas aplicações não só com a MetaMask mas com as blockchains ETH e BSC através da MetaMask.
Quer aprender ainda mais? Que tal este outro tutorial onde ensino como implementar autenticação nos seus dapps e aplicações web3 usando a MetaMask também.
Quer aprender a se integrar com outra carteira? Ensino com Brave Wallet aqui.
E no vídeo abaixo ensino como fazer deploy do seu dapp de forma descentralizada.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.