Veja como configurar o frontend do seu dapp para que os saldos de tokens e as transferências de fundos sejam atualizados nas carteiras Ethereum de seus usuários.
Esse artigo é uma tradução de Lorenzo Sicilia feita por @bananlabs. Você pode encontrar o artigo original aqui
Ethereum nos permite desenvolver aplicações descentralizadas (dapps). A principal diferença entre uma aplicação típica e um dapp é que você não precisa implantar um backend — ao menos enquanto você se beneficia das vantagens de outros contratos inteligentes implementados na rede principal da Ethereum.
Por causa disto, o frontend desempenha um papel importante. Ele está encarregado de empacotar e desempacotar os dados dos contratos inteligentes, assim como gerenciando as interações com a carteira (seja ela de software ou hardware) e, como de costume, gerenciando a experiência de usuário (UX). Não somente isso, um dapp, pela forma que ele é projetado, utiliza as chamadas de JSON-RPC e assim é possível abrir uma conexão de socket para receber atualizações.
Como você pode perceber, há algumas poucas coisas para orquestrar, mas não se preocupe, o ecossistema tem amadurecido muito nos últimos meses.
Pré-requisitos
Ao longo deste tutorial eu presumo que você já possui o seguinte:
Uma carteira para se conectar a um nó Geth
A abordagem mais simples é instalar a MetaMask, desta forma, você vai poder usar a infraestrutura da Infura imediatamente.
Alguma quantidade de Ether em sua conta
Quando estamos desenvolvendo com Ethereum eu fortemente recomendo você mudar para uma rede de teste e usar o Ether de teste. Se você precisa de fundos com a finalidade de teste você pode usar um faucet: e.g. https://faucet.rinkeby.io/
Um conhecimento básico sobre React
Eu vou te guiar através do passo a passo, mas eu presumo que você saiba como o React funciona (inclusive hooks). Se algo parece não familiar consulte a documentação do React.
Um Playground Funcional de React
Eu escrevi este tutorial com Typescript mas só algumas poucas coisas foram digitadas, assim, com pequenas modificações você pode utilizá-lo com Javascript também. Eu utilizei Parcel.js mas se sinta livre para utilizar também o Create React App ou qualquer outro empacotador de aplicação web.
Conecte-se à Rede Principal da Ethereum
Uma vez que você já esteja com a MetaMask pronta, então usaremos web3-react para manejar a interação com a rede. Isso te dará um hook bem prático que é useWeb3React
, o qual tem muitas funcionalidades para brincar com a Ethereum.
yarn add @web3-react/core @web3-react/injected-connector
Code language: CSS (css)
Deste modo, você vai precisar de um Provedor. Um Provedor abstrai a conexão com à rede Ethereum pela emissão de requisições e enviando transações de mudança-de-estado assinadas.
Usaremos o Web3Provider do Ether.js.
Parece que ele já tem algumas poucas bibliotecas, mas quando se interage com Ethereum é necessário traduzir os tipos de dados do Javascript para os de Solidity.
E também é requerido assinar as transações antes de executar alguma ação. Ether.js oferece estas funcionalidades elegantemente.
yarn add @ethersproject/providers
Code language: CSS (css)
Observação: o pacote Ether.js acima é a v5 atualmente em beta.
Dito isso estamos pronto para escrever um pequeno “Hello World” para verificarmos se temos tudo o que precisamos:
import React from 'react'
import { Web3ReactProvider } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core'
import { InjectedConnector } from '@web3-react/injected-connector'
export const injectedConnector = new InjectedConnector({
supportedChainIds: [
1, // Mainet
3, // Ropsten
4, // Rinkeby
5, // Goerli
42, // Kovan
],
})
function getLibrary(provider: any): Web3Provider {
const library = new Web3Provider(provider)
library.pollingInterval = 12000
return library
}
export const Wallet = () => {
const { chainId, account, activate, active } = useWeb3React<Web3Provider>()
const onClick = () => {
activate(injectedConnector)
}
return (
<div>
<div>ChainId: {chainId}</div>
<div>Account: {account}</div>
{active ? (
<div>✅ </div>
) : (
<button type="button" onClick={onClick}>
Connect
</button>
)}
</div>
)
}
export const App = () => {
return (
<Web3ReactProvider getLibrary={getLibrary}>
<Wallet />
</Web3ReactProvider>
)
}
Code language: JavaScript (javascript)
Se você fez o seu trabalho de casa certo você deve ter algo como isso:
Aqui está o que fizemos até agora: GIT - passo 1
Como Buscar os Dados da Rede Principal
Eu usarei SWR para gerenciar a busca de dados.
Isto é o que quero alcançar.
const { data: balance } = useSWR(["getBalance", account, "latest"])
Code language: JavaScript (javascript)
Bem legal 🙂
Vamos desvendar o truque! SWR significa Stale-While-Revalidate, uma estratégia de invalidação de cache HTTP popularizada pela RFC 5861.
SWR primeiro retorna os dados vindos do cache (stale), então reenvia a requisição de busca (revalidate), e finalmente vem com os dados atualizados novamente. A SWR aceita uma chave e por trás dos panos ela vai manejar a resolução.
Para fazer isso a SWR permite passar um buscador (fetcher
) capaz de resolver a chave ao retornar uma promise. O hello world da SWR é baseado em requisições da API REST com um buscador (fetcher
) baseado em fetch
API ou Axios
.
O que é brilhante sobre a SWR é que o único requisito é criar um buscador (fetcher
) que deve retornar uma promise.
Assim aqui está minha primeira implementação de um buscador (fetcher
) para a Ethereum:
const fetcher = (library) => (...args) => {
const [method, ...params] = args
console.log(method, params)
return library[method](...params)
}
Code language: JavaScript (javascript)
Como você pode ver, ela é uma função parcialmente aplicada. Desta forma, posso injetar a biblioteca (myWeb3Provider
) enquanto configuro o buscador. Após isso, toda vez que uma chave muda, a função pode ser resolvida retornando a promise requerida.
Agora posso criar meu componente <Balance/>
export const Balance = () => {
const { account, library } = useWeb3React<Web3Provider>()
const { data: balance } = useSWR(['getBalance', account, 'latest'], {
fetcher: fetcher(library),
})
if(!balance) {
return <div>...</div>
}
return <div>Balance: {balance.toString()}</div>
}
Code language: JavaScript (javascript)
O objeto balance retornado é do tipo BigNumber
.
Como você pode ver o número não está formatado e é extremamente grande. Isto é porque Solidity usa um tipo inteiro com o máximo de 256 bits.
Para exibir este número em um formato humanamente legível, a solução é utilizar uma das funcionalidades mencionadas anteriormente do Ether.js: formatEther (balance)
yarn install @ethersproject/units
Code language: CSS (css)
Agora eu posso refazer meu componente <Balance/>
para manipular e formatar o BitInt
em uma forma humanamente legível
export const Balance = () => {
const { account, library } = useWeb3React<Web3Provider>()
const { data: balance } = useSWR(['getBalance', account, 'latest'], {
fetcher: fetcher(library),
})
if(!balance) {
return <div>...</div>
}
return <div>Ξ {parseFloat(formatEther(balance)).toPrecision(4)}</div>
}
Code language: JavaScript (javascript)
O que fizemos até o momento: GIT passo 2
Como Atualizar os Dados em Tempo Real
SWR expõe uma função mutate
para atualizar seu cache interno.
const { data: balance, mutate } = useSWR(['getBalance', account, 'latest'], {
fetcher: fetcher(library),
})
const onClick = () => {
mutate(new BigNumber(10), false)
}
Code language: JavaScript (javascript)
A função mutate
está automaticamente ligada à chave (e.g. [‘getBalance’, account. ‘latest’]
) da qual ela foi gerada. Ela aceita dois parâmetros. Os novos dados e se uma validação deve ser acionada. Se ela for acionada, SWR vai usar automaticamente o buscador para atualizar o cache 💥
Como foi antecipado, os eventos em Solidity oferecem uma pequena abstração no topo da funcionalidade de logging da EVM. As aplicações podem se inscrever e ouvir a estes eventos pela interface RPC do cliente Ethereum.
Ether.js tem uma API simples para se inscrever em um evento:
const { account, library } = useWeb3React<Web3Provider>()
library.on("blockNumber", (blockNumber) => {
console.log({blockNumber})
})
Code language: JavaScript (javascript)
Agora vamos combinar ambas abordagens no componente novo <Balance/>
export const Balance = () => {
const { account, library } = useWeb3React<Web3Provider>()
const { data: balance, mutate } = useSWR(['getBalance', account, 'latest'], {
fetcher: fetcher(library),
})
useEffect(() => {
// listen for changes on an Ethereum address
console.log(`listening for blocks...`)
library.on('block', () => {
console.log('update balance...')
mutate(undefined, true)
})
// remove listener when the component is unmounted
return () => {
library.removeAllListeners('block')
}
// trigger the effect only on component mount
}, [])
if (!balance) {
return <div>...</div>
}
return <div>Ξ {parseFloat(formatEther(balance)).toPrecision(4)}</div>
}
Code language: JavaScript (javascript)
Inicialmente, a SWR buscará o saldo da conta, e assim toda vez que ela receber uma evento de bloco ela usará o mutate para acionar uma nova busca.
Observação: Nós utilizamos o mutate (undefined, true)
porque não podemos recuperar do evento atual o saldo real, apenas acionamos uma nova busca do saldo.
Abaixo está uma rápida demonstração com duas carteiras Ethereum que estão transacionando alguns ETH.
O que fizemos até o momento: GIT passo 3
Como Interagir Com Um Contrato Inteligente
Até agora, ilustramos o básico do uso de SWR e como fazer uma chamada básica através de um Web3Provider
. Vamos agora descobrir como interagir com um contrato inteligente.
O Ether.js lida com a interação do contrato inteligente usando a ABI (Application Binary Interface) gerada pelo compilador da Solidity.
> A ABI (Application Binary Interface) do contrato é a maneira padrão de interagir com contratos no ecossistema Ethereum, tanto de fora da blockchain quanto para interação contrato a contrato.
Por exemplo, dado o contrato inteligente simples abaixo:
pragma solidity ^0.5.0;
contract Test {
constructor() public { b = hex"12345678901234567890123456789012"; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
function foo(uint a) public { emit Event(a, b); }
bytes32 b;
}
Code language: JavaScript (javascript)
Esta é a ABI gerada:
[
{
"type": "event",
"inputs": [
{ "name": "a", "type": "uint256", "indexed": true },
{ "name": "b", "type": "bytes32", "indexed": false }
],
"name": "Event"
},
{
"type": "event",
"inputs": [
{ "name": "a", "type": "uint256", "indexed": true },
{ "name": "b", "type": "bytes32", "indexed": false }
],
"name": "Event2"
},
{
"type": "function",
"inputs": [{ "name": "a", "type": "uint256" }],
"name": "foo",
"outputs": []
}
]
Code language: JSON / JSON with Comments (json)
Para usar as ABIs, podemos simplesmente copiá-las diretamente em seu código e importá-las onde for necessário. Nesta demonstração, usaremos uma ABI ERC20 padrão porque queremos recuperar os saldos de dois tokens: DAI e MKR.
A próxima etapa é criar o componente <TokenBalance/>
export const TokenBalance = ({ symbol, address, decimals }) => {
const { account, library } = useWeb3React<Web3Provider>()
const { data: balance, mutate } = useSWR([address, 'balanceOf', account], {
fetcher: fetcher(library, ERC20ABI),
})
useEffect(() => {
// listen for changes on an Ethereum address
console.log(`listening for Transfer...`)
const contract = new Contract(address, ERC20ABI, library.getSigner())
const fromMe = contract.filters.Transfer(account, null)
library.on(fromMe, (from, to, amount, event) => {
console.log('Transfer|sent', { from, to, amount, event })
mutate(undefined, true)
})
const toMe = contract.filters.Transfer(null, account)
library.on(toMe, (from, to, amount, event) => {
console.log('Transfer|received', { from, to, amount, event })
mutate(undefined, true)
})
// remove listener when the component is unmounted
return () => {
library.removeAllListeners(toMe)
library.removeAllListeners(fromMe)
}
// trigger the effect only on component mount
}, [])
if (!balance) {
return <div>...</div>
}
return (
<div>
{parseFloat(formatUnits(balance, decimals)).toPrecision(4)} {symbol}
</div>
)
}
Code language: JavaScript (javascript)
Vamos ampliar. Existem duas diferenças principais:
Definição de chave
A chave, usada por useSWR([address, 'balanceOf', account])
, precisa começar com um endereço Ethereum em vez de um método. Por causa disso, o buscador pode reconhecer o que queremos alcançar e usar a ABI.
Vamos refatorar o buscador (fetcher
) de acordo:
const fetcher = (library: Web3Provider, abi?: any) => (...args) => {
const [arg1, arg2, ...params] = args
// it's a contract
if (isAddress(arg1)) {
const address = arg1
const method = arg2
const contract = new Contract(address, abi, library.getSigner())
return contract[method](...params)
}
// it's a eth call
const method = arg1
return library[method](arg2, ...params)
}
Code language: JavaScript (javascript)
Agora temos um buscador de uso geral capaz de interagir com as chamadas JSON-RPC do Ethereum. 🙌
Filtros de Log
O outro aspecto em <TokenBalance/>
é como escutar os eventos ERC20. O Ether.js fornece uma maneira prática de configurar um filtro com base nos tópicos e no nome do evento. Mais informações sobre o que é um tópico podem ser encontradas na documentação da Solidity.
const contract = new Contract(address, ERC20ABI, library.getSigner())
const fromMe = contract.filters.Transfer(account, null)
Code language: JavaScript (javascript)
Depois de criar uma instância de contrato com a ABI, você pode passar o filtro para a instância da biblioteca.
Advertência:
Você pode ficar tentado a usar o valor no evento ERC20 diretamente para aumentar ou diminuir o saldo.
Tenha consciência do dragão. Ao configurar o buscador (fetcher
), você passou uma clojure como callback para a funçãoon
, que continha o valor do saldo no momento.
Isso pode ser corrigido usando umuseRef
, mas para simplificar vamos revalidar o cache para garantir que os saldos sejam atualizados:mutate(undefined, true)
Agora temos todas as peças necessárias. A última parte é um pouco de cola.
Configurei algumas constantes para ter uma boa maneira de mapear meu componente TokenBalance para uma lista de tokens dependendo da rede em que estamos trabalhando:
export const Networks = {
MainNet: 1,
Rinkeby: 4,
Ropsten: 3,
Kovan: 42,
}
export interface IERC20 {
symbol: string
address: string
decimals: number
name: string
}
export const TOKENS_BY_NETWORK: {
} = {
{
address: "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa",
symbol: "DAI",
name: "Dai",
decimals: 18,
},
{
address: "0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85",
symbol: "MKR",
name: "Maker",
decimals: 18,
},
],
}
Code language: JavaScript (javascript)
Uma vez que temos as constantes, é fácil mapear os tokens configurados para meu componente <TokenList/>
:
export const TokenList = ({ chainId }) => {
return (
<>
{TOKENS_BY_NETWORK[chainId].map((token) => (
<TokenBalance key={token.address} {...token} />
))}
</>
)
}
Code language: JavaScript (javascript)
Tudo pronto! Agora temos uma carteira Ethereum que carrega saldos de ether e token. E se o usuário enviar ou receber fundos, a interface do usuário da carteira será atualizada.
O que fizemos até o momento: GIT passo 4
Refatoração
Vamos mover cada componente em um arquivo separado e tornar o buscador (fetcher
) disponível globalmente usando o provedor SWRConfig.
<SWRConfig value={{ fetcher: fetcher(library, ERC20ABI) }}>
<EthBalance />
<TokenList chainId={chainId} />
<SWRConfig/>
Code language: HTML, XML (xml)
Com o SWRConfig podemos configurar algumas opções como sempre disponíveis, para que possamos ter um uso mais conveniente do SWR.
Aqui está tudo depois da refatoração: GIT passo - 5
Concluindo
SWR e Ether.js são duas boas bibliotecas para trabalhar se você quiser otimizar sua estratégia de busca de dados com o dapp Ethereum.
Principais Vantagens
- Abordagem declarativa
- Dados sempre atualizados por web sockets ou opções de SWR
- Evite reinventar a roda para gerenciamento de estado com React context personalizado
Se você usa vários contratos inteligentes em seu dapp e gostou deste tutorial, generalizei o buscador web3 em um pequeno utilitário: swr-eth (Estrelas são apreciadas 👻)
E, finalmente, aqui está o repositório GIT completo: (https://github.com/aboutlo/swr-eth-tutorial).
Oldest comments (0)