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.

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

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

    1pnpm 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.

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

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

Importe o código da biblioteca que precisamos.

1import { App } from './App'

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

1ReactDOM.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á".

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

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

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

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

1 </RainbowKitProvider>
2 </WagmiConfig>
3 </React.StrictMode>,
4)

Claro, temos que fechar os outros componentes.

src/App.tsx

1import { ConnectButton } from '@rainbow-me/rainbowkit'
2import { useAccount } from 'wagmi'
3import { Greeter } from './components/Greeter'
4
5export 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.

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

1 return (
2 <>

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.

1 <h1>Greeter</h1>
2 <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.

1 {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.

1 <Greeter />
2 )}
3 </>
4 )
5}

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.

1import { useState, ChangeEventHandler } from 'react'
2import { useNetwork,
3 useReadContract,
4 usePrepareContractWrite,
5 useContractWrite,
6 useContractEvent
7 } from 'wagmi'

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

1import { 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).

1let greeterABI = [
2 .
3 .
4 .
5] 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.

1type AddressPerBlockchainType = {
2 [key: number]: AddressType
3}

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

1const contractAddrs: AddressPerBlockchainType = {
2 // Holesky
3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
4
5 // Sepolia
6 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
7}

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.

1type ShowObjectAttrsType = {
2 name: string,
3 object: any
4}

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.

1type ShowGreetingAttrsType = {
2 greeting: string | undefined
3}

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
1const Greeter = () => {

Finalmente, chegamos à definição do componente.

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

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

1 const readResults = useReadContract({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: "greet" , // Sem argumentos
5 watch: true
6 })

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

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

1 const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>
2 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).

1 const preparedTx = usePrepareContractWrite({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: 'setGreeting',
5 args: [ newGreeting ]
6 })
7 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.

1 return (

Agora podemos finalmente criar o HTML real para retornar.

1 <>
2 <h2>Greeter</h2>
3 {
4 !readResults.isError && !readResults.isLoading &&
5 <ShowGreeting greeting={readResults.data} />
6 }
7 <hr />

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

1 <input type="text"
2 value={newGreeting}
3 onChange={greetingChange}
4 />

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.
1 <button disabled={!workingTx.write}
2 onClick={workingTx.write}
3 >
4 Atualizar saudação
5 </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.

1 <hr />
2 <ShowObject name="readResults" object={readResults} />
3 <ShowObject name="preparedTx" object={preparedTx} />
4 <ShowObject name="workingTx" object={workingTx} />
5 </>
6 )
7}

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

1const ShowGreeting = (attrs : ShowGreetingAttrsType) => {

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

1 return <b>{attrs.greeting}</b>
2}
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).

1const ShowObject = (attrs: ShowObjectAttrsType ) => {
2 const keys = Object.keys(attrs.object)
3 const funs = keys.filter(k => typeof attrs.object[k] == "function")
4 return <>
5 <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).

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

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

1 </pre>
2 { funs.length > 0 &&
3 <>
4 Functions:
5 <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.

1 {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.

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

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.

1 </ul>
2 </>
3 }
4 </details>
5 </>
6}

Finalize as várias tags HTML.

O export final
1export { 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)).

1import { getDefaultWallets } from '@rainbow-me/rainbowkit'
2import { configureChains, createConfig } from 'wagmi'
3import { 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).

1import { publicProvider } from 'wagmi/providers/public'
2
3const 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).

1const { chains, publicClient, webSocketPublicClient } = configureChains(
2 [ holesky, sepolia ],
3 [
4 publicProvider(),
5 ],
6)
7
8const { connectors } = getDefaultWallets({
9 appName: 'My wagmi + RainbowKit App',
10 chains,
11 projectId: walletConnectProjectId,
12})
13
14export const config = createConfig({
15 autoConnect: true,
16 connectors,
17 publicClient,
18 webSocketPublicClient,
19})
20
21export { chains }
Exibir tudo

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.

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

    1const redstoneHolesky = defineChain({
    2 id: 17_001,
    3 name: 'Redstone Holesky',
    4 network: 'redstone-holesky',
    5 nativeCurrency: {
    6 decimals: 18,
    7 name: 'Ether',
    8 symbol: 'ETH',
    9 },
    10 rpcUrls: {
    11 default: {
    12 http: ['https://rpc.holesky.redstone.xyz'],
    13 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    14 },
    15 public: {
    16 http: ['https://rpc.holesky.redstone.xyz'],
    17 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    18 },
    19 },
    20 blockExplorers: {
    21 default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' },
    22 },
    23})
    Exibir tudo
  3. Adicione a nova cadeia à chamada configureChains.

    1 const { chains, publicClient, webSocketPublicClient } = configureChains(
    2 [ holesky, sepolia, redstoneHolesky ],
    3 [ publicProvider(), ],
    4 )
  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:

    1const contractAddrs : AddressPerBlockchainType = {
    2 // Holesky
    3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
    4
    5 // Redstone Holesky
    6 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3',
    7
    8 // Sepolia
    9 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
    10}
    Exibir tudo

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.

    1pnpm 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?