Pular para o conteúdo principal

Construindo uma interface de usuário para seu contrato

TypeScript
react
vite
wagmi
front-end
Iniciante
Ori Pomerantz
1 de novembro de 2023
15 minutos de leitura

Você encontrou um recurso que precisamos no ecossistema Ethereum. Você escreveu os contratos inteligentes para implementá-lo, e talvez até algum código relacionado que é executado fora da cadeia. Isso é ótimo! Infelizmente, sem uma interface de usuário, você não terá nenhum usuário, e da última vez que você escreveu um site, as pessoas usavam modems de discagem e o JavaScript era novo.

Este artigo é para você. Presumo que você saiba programar, e talvez um pouco de JavaScript e HTML, mas que suas habilidades de interface de usuário estejam enferrujadas e desatualizadas. Juntos, vamos analisar uma aplicação moderna simples para que você veja como isso é feito hoje em dia.

Por que isso é importante

Na teoria, você poderia simplesmente fazer com que as pessoas usassem o Etherscan (opens in a new tab) ou o Blockscout (opens in a new tab) para interagir com seus contratos. Isso será ótimo para os Ethereans experientes. Mas estamos tentando servir a mais um bilhão de pessoas (opens in a new tab). Isso não acontecerá sem uma ótima experiência do usuário, e uma interface de usuário amigável é uma grande parte disso.

Aplicação Greeter

Há muita teoria por trás do funcionamento de uma UI moderna, e muitos sites bons (opens in a new tab) que explicam isso (opens in a new tab). Em vez de repetir o bom trabalho feito por esses sites, vou presumir que você prefere aprender fazendo e começar com uma aplicação com a qual possa interagir. Você ainda precisa da teoria para fazer as coisas, e chegaremos lá — vamos apenas passar de arquivo fonte por arquivo fonte e discutir as coisas à medida que as encontrarmos.

Instalação

  1. Se necessário, adicione a cadeia de blocos Holesky (opens in a new tab) à sua carteira e obtenha ETH de teste (opens in a new tab).

  2. Clone o repositório do github.

    git clone https://github.com/qbzzt/20230801-modern-ui.git
    
  3. Instale os pacotes necessários.

    cd 20230801-modern-ui
    pnpm install
    
  4. Inicie a aplicação.

    pnpm dev
    
  5. Navegue até o URL mostrado pela aplicação. Na maioria dos casos, é http://localhost:5173/ (opens in a new tab).

  6. Você pode ver o código-fonte do contrato, uma versão ligeiramente modificada do Greeter da Hardhat, em um explorador de blockchain (opens in a new tab).

Passo a passo dos arquivos

index.html

Este arquivo é um boilerplate HTML padrão, exceto por esta linha, que importa o arquivo de script.

<script type="module" src="/src/main.tsx"></script>

src/main.tsx

A extensão do arquivo nos diz que este arquivo é um componente React (opens in a new tab) escrito em TypeScript (opens in a new tab), uma extensão do JavaScript que suporta verificação de tipo (opens in a new tab). O TypeScript é compilado para JavaScript, então podemos usá-lo para execução do lado do cliente.

import '@rainbow-me/rainbowkit/styles.css'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import { WagmiConfig } from 'wagmi'
import { chains, config } from './wagmi'

Importe o código da biblioteca que precisamos.

import { App } from './App'

Importe o componente React que implementa a aplicação (veja abaixo).

ReactDOM.createRoot(document.getElementById('root')!).render(

Crie o componente React raiz. O parâmetro para render é JSX (opens in a new tab), uma linguagem de extensão que usa tanto HTML quanto JavaScript/TypeScript. O ponto de exclamação aqui diz ao componente TypeScript: "você não sabe que document.getElementById('root') será um parâmetro válido para ReactDOM.createRoot, mas não se preocupe - eu sou o desenvolvedor e estou lhe dizendo que haverá".

  <React.StrictMode>

A aplicação está dentro de um componente React.StrictMode (opens in a new tab). Este componente diz à biblioteca React para inserir verificações de depuração adicionais, o que é útil durante o desenvolvimento.

    <WagmiConfig config={config}>

A aplicação também está dentro de um componente WagmiConfig (opens in a new tab). A biblioteca wagmi (we are going to make it) (opens in a new tab) conecta as definições da UI do React com a biblioteca viem (opens in a new tab) para escrever um aplicativo descentralizado da Ethereum.

      <RainbowKitProvider chains={chains}>

E, finalmente, um componente RainbowKitProvider (opens in a new tab). Este componente lida com o login e a comunicação entre a carteira e a aplicação.

        <App />

Agora podemos ter o componente para a aplicação, que realmente implementa a UI. O /> no final do componente diz ao React que este componente não tem nenhuma definição dentro dele, conforme o padrão XML.

      </RainbowKitProvider>
    </WagmiConfig>
  </React.StrictMode>,
)

Claro, temos que fechar os outros componentes.

src/App.tsx

import { ConnectButton } from '@rainbow-me/rainbowkit'
import { useAccount } from 'wagmi'
import { Greeter } from './components/Greeter'

export function App() {

Esta é a maneira padrão de criar um componente React - definir uma função que é chamada toda vez que precisa ser renderizada. Esta função normalmente tem algum código TypeScript ou JavaScript no início, seguido por uma declaração return que retorna o código JSX.

  const { isConnected } = useAccount()

Aqui usamos useAccount (opens in a new tab) para verificar se estamos conectados a uma cadeia de blocos através de uma carteira ou não.

Por convenção, em React, funções chamadas use... são hooks (opens in a new tab) que retornam algum tipo de dado. Quando você usa esses hooks, não apenas seu componente obtém os dados, mas quando esses dados mudam, o componente é renderizado novamente com as informações atualizadas.

  return (
    <>

O JSX de um componente React tem que retornar um componente. Quando temos múltiplos componentes e não temos nada que os envolva "naturalmente", usamos um componente vazio (<> ... </>) para transformá-los em um único componente.

      <h1>Greeter</h1>
      <ConnectButton />

Obtemos o componente ConnectButton (opens in a new tab) do RainbowKit. Quando não estamos conectados, ele nos dá um botão Conectar Carteira que abre um modal que explica sobre carteiras e permite que você escolha qual usar. Quando estamos conectados, ele exibe a cadeia de blocos que usamos, o endereço da nossa conta e nosso saldo de ETH. Podemos usar essas exibições para trocar de rede ou para desconectar.

      {isConnected && (

Quando precisamos inserir JavaScript real (ou TypeScript que será compilado para JavaScript) em um JSX, usamos chaves ({}).

A sintaxe a && b é uma abreviação de a ? b : a (opens in a new tab). Ou seja, se a for verdadeiro, ele avalia para b, caso contrário, avalia para a (que pode ser falso, 0, etc.). Esta é uma maneira fácil de dizer ao React que um componente só deve ser exibido se uma determinada condição for satisfeita.

Neste caso, só queremos mostrar Greeter ao usuário se o usuário estiver conectado a uma cadeia de blocos.

          <Greeter />
      )}
    </>
  )
}

src/components/Greeter.tsx

Este arquivo contém a maior parte da funcionalidade da UI. Ele inclui definições que normalmente estariam em vários arquivos, mas como este é um tutorial, o programa é otimizado para ser fácil de entender na primeira vez, em vez de desempenho ou facilidade de manutenção.

import { useState, ChangeEventHandler } from 'react'
import {  useNetwork,
          useReadContract,
          usePrepareContractWrite,
          useContractWrite,
          useContractEvent
        } from 'wagmi'

Usamos estas funções de biblioteca. Novamente, elas são explicadas abaixo onde são usadas.

import { AddressType } from 'abitype'

A biblioteca abitype (opens in a new tab) nos fornece definições TypeScript para vários tipos de dados do Ethereum, como AddressType (opens in a new tab).

let greeterABI = [
  .
  .
  .
] as const   // greeterABI

A ABI para o contrato Greeter. Se você estiver desenvolvendo os contratos e a UI ao mesmo tempo, normalmente os colocaria no mesmo repositório e usaria a ABI gerada pelo compilador Solidity como um arquivo em sua aplicação. No entanto, isso não é necessário aqui porque o contrato já está desenvolvido e não vai mudar.

type AddressPerBlockchainType = {
  [key: number]: AddressType
}

O TypeScript é fortemente tipado. Usamos essa definição para especificar o endereço em que o contrato Greeter é implantado em diferentes cadeias. A chave é um número (o chainId), e o valor é um AddressType (um endereço).

const contractAddrs: AddressPerBlockchainType = {
  // Holesky
  17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',

  // Sepolia
  11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
}

O endereço do contrato nas duas redes suportadas: Holesky (opens in a new tab) e Sepolia (opens in a new tab).

Nota: Existe na verdade uma terceira definição, para Redstone Holesky, que será explicada abaixo.

type ShowObjectAttrsType = {
  name: string,
  object: any
}

Este tipo é usado como um parâmetro para o componente ShowObject (explicado mais adiante). Ele inclui o nome do objeto e seu valor, que são exibidos para fins de depuração.

type ShowGreetingAttrsType = {
  greeting: string | undefined
}

A qualquer momento, podemos saber qual é a saudação (porque a lemos da cadeia de blocos) ou não saber (porque ainda não a recebemos). Portanto, é útil ter um tipo que pode ser uma string ou nada.

Componente Greeter
const Greeter = () => {

Finalmente, chegamos à definição do componente.

  const { chain } = useNetwork()

Informações sobre a cadeia que estamos usando, cortesia de wagmi (opens in a new tab). Como isso é um hook (use...), toda vez que essa informação muda, o componente é redesenhado.

  const greeterAddr = chain && contractAddrs[chain.id]

O endereço do contrato Greeter, que varia por cadeia (e que é undefined se não tivermos informações da cadeia ou estivermos em uma cadeia sem esse contrato).

  const readResults = useReadContract({
    address: greeterAddr,
    abi: greeterABI,
    functionName: "greet" , // Sem argumentos
    watch: true
  })

O hook useReadContract (opens in a new tab) lê informações de um contrato. Você pode ver exatamente quais informações ele retorna expandindo readResults na UI. Neste caso, queremos que ele continue verificando para sermos informados quando a saudação mudar.

Nota: Poderíamos escutar os eventos setGreeting (opens in a new tab) para saber quando a saudação muda e atualizar dessa forma. No entanto, embora possa ser mais eficiente, não se aplicará em todos os casos. Quando o usuário muda para uma cadeia diferente, a saudação também muda, mas essa mudança não é acompanhada por um evento. Poderíamos ter uma parte do código escutando eventos e outra para identificar mudanças de cadeia, mas isso seria mais complicado do que apenas definir o parâmetro watch (opens in a new tab).

  const [ newGreeting, setNewGreeting ] = useState("")

O hook useState (opens in a new tab) do React nos permite especificar uma variável de estado, cujo valor persiste de uma renderização do componente para outra. O valor inicial é o parâmetro, neste caso, a string vazia.

O hook useState retorna uma lista com dois valores:

  1. O valor atual da variável de estado.
  2. Uma função para modificar a variável de estado quando necessário. Como este é um hook, toda vez que ele é chamado, o componente é renderizado novamente.

Neste caso, estamos usando uma variável de estado para a nova saudação que o usuário deseja definir.

  const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>
    setNewGreeting(evt.target.value)

Este é o manipulador de eventos para quando o campo de entrada da nova saudação muda. O tipo, ChangeEventHandler<HTMLInputElement> (opens in a new tab), especifica que este é um manipulador para uma mudança de valor de um elemento de entrada HTML. A parte <HTMLInputElement> é usada porque este é um tipo genérico (opens in a new tab).

  const preparedTx = usePrepareContractWrite({
    address: greeterAddr,
    abi: greeterABI,
    functionName: 'setGreeting',
    args: [ newGreeting ]
  })
  const workingTx = useContractWrite(preparedTx.config)

Este é o processo para enviar uma transação de blockchain da perspectiva do cliente:

  1. Envie a transação para um nó na cadeia de blocos usando eth_estimateGas (opens in a new tab).
  2. Aguarde uma resposta do nó.
  3. Quando a resposta for recebida, peça ao usuário para assinar a transação através da carteira. Esta etapa tem que acontecer depois que a resposta do nó for recebida, porque o custo do gás da transação é mostrado ao usuário antes que ele a assine.
  4. Aguarde a aprovação do usuário.
  5. Envie a transação novamente, desta vez usando eth_sendRawTransaction (opens in a new tab).

A etapa 2 provavelmente levará um tempo perceptível, durante o qual os usuários se perguntariam se seu comando foi realmente recebido pela interface do usuário e por que ainda não lhes foi pedido para assinar a transação. Isso gera uma má experiência do usuário (UX).

A solução é usar hooks de preparação (opens in a new tab). Toda vez que um parâmetro muda, envie imediatamente ao nó a solicitação eth_estimateGas. Então, quando o usuário realmente quer enviar a transação (neste caso, pressionando Atualizar saudação), o custo do gás é conhecido e o usuário pode ver a página da carteira imediatamente.

  return (

Agora podemos finalmente criar o HTML real para retornar.

    <>
      <h2>Greeter</h2>
      {
        !readResults.isError && !readResults.isLoading &&
          <ShowGreeting greeting={readResults.data} />
      }
      <hr />

Crie um componente ShowGreeting (explicado abaixo), mas somente se a saudação for lida com sucesso da cadeia de blocos.

      <input type="text"
        value={newGreeting}
        onChange={greetingChange}
      />

Este é o campo de texto de entrada onde o usuário pode definir uma nova saudação. Toda vez que o usuário pressiona uma tecla, chamamos greetingChange, que chama setNewGreeting. Como setNewGreeting vem do hook useState, ele faz com que o componente Greeter seja renderizado novamente. Isso significa que:

  • Precisamos especificar value para manter o valor da nova saudação, porque, caso contrário, ele voltaria ao padrão, a string vazia.
  • usePrepareContractWrite é chamado toda vez que newGreeting muda, o que significa que ele sempre terá o newGreeting mais recente na transação preparada.
      <button disabled={!workingTx.write}
              onClick={workingTx.write}
      >
        Atualizar saudação
      </button>

Se não houver workingTx.write, ainda estamos aguardando as informações necessárias para enviar a atualização da saudação, então o botão está desabilitado. Se houver um valor workingTx.write, essa é a função a ser chamada para enviar a transação.

      <hr />
      <ShowObject name="readResults" object={readResults} />
      <ShowObject name="preparedTx" object={preparedTx} />
      <ShowObject name="workingTx" object={workingTx} />
    </>
  )
}

Finalmente, para ajudar você a ver o que estamos fazendo, mostramos os três objetos que usamos:

  • readResults
  • preparedTx
  • workingTx
Componente ShowGreeting

Este componente mostra

const ShowGreeting = (attrs : ShowGreetingAttrsType) => {

A função de um componente recebe um parâmetro com todos os atributos do componente.

  return <b>{attrs.greeting}</b>
}
Componente ShowObject

Para fins informativos, usamos o componente ShowObject para mostrar os objetos importantes (readResults para ler a saudação e preparedTx e workingTx para as transações que criamos).

const ShowObject = (attrs: ShowObjectAttrsType ) => {
  const keys = Object.keys(attrs.object)
  const funs = keys.filter(k => typeof attrs.object[k] == "function")
  return <>
    <details>

Não queremos poluir a UI com todas as informações, então para tornar possível visualizá-las ou fechá-las, usamos uma tag details (opens in a new tab).

      <summary>{attrs.name}</summary>
      <pre>
        {JSON.stringify(attrs.object, null, 2)}

A maioria dos campos é exibida usando JSON.stringify (opens in a new tab).

      </pre>
      { funs.length > 0 &&
        <>
          Functions:
          <ul>

A exceção são as funções, que não fazem parte do padrão JSON (opens in a new tab), então elas têm que ser exibidas separadamente.

          {funs.map((f, i) =>

Dentro do JSX, o código dentro de chaves { } é interpretado como JavaScript. Então, o código dentro dos parênteses ( ), é interpretado novamente como JSX.

           (<li key={i}>{f}</li>)
                )}

O React requer que as tags na Árvore DOM (opens in a new tab) tenham identificadores distintos. Isso significa que os filhos da mesma tag (neste caso, a lista não ordenada (opens in a new tab)), precisam de atributos key diferentes.

          </ul>
        </>
      }
    </details>
  </>
}

Finalize as várias tags HTML.

O export final
export { Greeter }

O componente Greeter é o que precisamos exportar para a aplicação.

src/wagmi.ts

Finalmente, várias definições relacionadas ao WAGMI estão em src/wagmi.ts. Não vou explicar tudo aqui, porque a maior parte é boilerplate que você provavelmente não precisará mudar.

O código aqui não é exatamente o mesmo que no github (opens in a new tab) porque mais adiante no artigo adicionamos outra cadeia (Redstone Holesky (opens in a new tab)).

import { getDefaultWallets } from '@rainbow-me/rainbowkit'
import { configureChains, createConfig } from 'wagmi'
import { holesky, sepolia } from 'wagmi/chains'

Importe as blockchains que a aplicação suporta. Você pode ver a lista de cadeias suportadas no github do viem (opens in a new tab).

import { publicProvider } from 'wagmi/providers/public'

const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575'

Para poder usar o WalletConnect (opens in a new tab), você precisa de um ID de projeto para sua aplicação. Você pode obtê-lo em cloud.walletconnect.com (opens in a new tab).

Adicionando outra blockchain

Hoje em dia há muitas soluções de escalabilidade L2, e você pode querer suportar algumas que o viem ainda não suporta. Para fazer isso, você modifica src/wagmi.ts. Estas instruções explicam como adicionar a Redstone Holesky (opens in a new tab).

  1. Importe o tipo defineChain do viem.

    import { defineChain } from 'viem'
    
  2. Adicione a definição de rede.

  3. Adicione a nova cadeia à chamada configureChains.

     const { chains, publicClient, webSocketPublicClient } = configureChains(
       [ holesky, sepolia, redstoneHolesky ],
       [ publicProvider(), ],
     )
    
  4. Certifique-se de que a aplicação saiba o endereço para seus contratos na nova rede. Neste caso, modificamos src/components/Greeter.tsx:

Conclusão

Claro, você não se importa realmente em fornecer uma interface de usuário para o Greeter. Você quer criar uma interface de usuário para seus próprios contratos. Para criar sua própria aplicação, siga estes passos:

  1. Especifique para criar uma aplicação wagmi.

    pnpm create wagmi
    
  2. Nomeie a aplicação.

  3. Selecione a estrutura React.

  4. Selecione a variante Vite.

  5. Você pode adicionar o kit Rainbow (opens in a new tab).

Agora vá e torne seus contratos utilizáveis para o mundo inteiro.

Veja aqui mais do meu trabalho (opens in a new tab).

Última atualização da página: 3 de março de 2026

Este tutorial foi útil?