Construindo uma interface de usuário para seu contrato
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
-
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).
-
Clone o repositório do github.
1git clone https://github.com/qbzzt/20230801-modern-ui.git -
Instale os pacotes necessários.
1cd 20230801-modern-ui2pnpm install -
Inicie a aplicação.
1pnpm dev -
Navegue até o URL mostrado pela aplicação. Na maioria dos casos, é http://localhost:5173/ (opens in a new tab).
-
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'45export 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 useContractEvent7 } 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 // greeterABIA 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]: AddressType3}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 // Holesky3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',45 // Sepolia6 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: any4}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 | undefined3}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 argumentos5 watch: true6 })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:
- O valor atual da variável de estado.
- 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:
- Envie a transação para um nó na cadeia de blocos usando
eth_estimateGas(opens in a new tab). - Aguarde uma resposta do nó.
- 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.
- Aguarde a aprovação do usuário.
- 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
valuepara manter o valor da nova saudação, porque, caso contrário, ele voltaria ao padrão, a string vazia. usePrepareContractWriteé chamado toda vez quenewGreetingmuda, o que significa que ele sempre terá onewGreetingmais recente na transação preparada.
1 <button disabled={!workingTx.write}2 onClick={workingTx.write}3 >4 Atualizar saudação5 </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:
readResultspreparedTxworkingTx
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'23const 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)78const { connectors } = getDefaultWallets({9 appName: 'My wagmi + RainbowKit App',10 chains,11 projectId: walletConnectProjectId,12})1314export const config = createConfig({15 autoConnect: true,16 connectors,17 publicClient,18 webSocketPublicClient,19})2021export { chains }Exibir tudoAdicionando 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).
-
Importe o tipo
defineChaindo viem.1import { defineChain } from 'viem' -
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 -
Adicione a nova cadeia à chamada
configureChains.1 const { chains, publicClient, webSocketPublicClient } = configureChains(2 [ holesky, sepolia, redstoneHolesky ],3 [ 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:1const contractAddrs : AddressPerBlockchainType = {2 // Holesky3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',45 // Redstone Holesky6 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3',78 // Sepolia9 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:
-
Especifique para criar uma aplicação wagmi.
1pnpm create wagmi -
Nomeie a aplicação.
-
Selecione a estrutura React.
-
Selecione a variante Vite.
-
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