WEB3DEV

Cover image for Como Buscar e Atualizar Dados do Ethereum com React e SWR
Banana Labs
Banana Labs

Posted on

Como Buscar e Atualizar Dados do Ethereum com React e SWR

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Se você fez o seu trabalho de casa certo você deve ter algo como isso:

localhost exibição de página

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

O objeto balance retornado é do tipo BigNumber.

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

ChainID

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Demo 2

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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ção on, que continha o valor do saldo no momento.
Isso pode ser corrigido usando um useRef, 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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).

Latest comments (0)