Smart Contracts são algoritmos que rodam em cima da blokchain da Ethereum e compatíveis. Através de programação usando a linguagem Solidity é possível fazer pagamentos usando cripto, criar protocolos DeFi, salvar dados na blockchain, transferir a propriedade de NFTs e muito mais. Explorei a linguagem Solidity e a criação de Smart Contracts neste outro tutorial aqui do blog.
No entanto, Smart Contracts são como se fossem o backend da Web3: eles não possuem qualquer interface gráfica para que os usuários possam interagir com eles, como se fossem web APIs, entende?
Se você quer que usuários comuns consigam usar dos recursos da sua aplicação é necessário que você forneça alguma maneira simples e visual deles interagirem com o mesmo, criar uma aplicação de fato e não apenas um contrato. A estas aplicações que se conectam a smart contracts nós chamamos de dapps, ou decentralized applications (aplicações descentralizadas).
Você pode criar um dapp com qualquer tecnologia, mas quando o assunto é dapps web não vai conseguir fugir de JavaScript. E ainda no espectro do JavaScript existem muitas opções de tecnologias, sendo que a que recomendo utilizar é a ReactJS, que já apresentei em mais detalhes em outros tutoriais aqui do blog.
Então neste tutorial eu vou te mostrar como você pode criar uma aplicação ReactJS muito simples que servirá de interface para um smart contract bem simples também, que eu já publiquei na blockchain da Binance (BSC) em outra oportunidade. Apesar de estar nesta blockchain, qualquer blockchain compatível com Ethereum vai funcionar da mesma forma.
Usaremos aqui a biblioteca Wagmi, uma das mais famosas para este tipo de integração. Outra alternativa é a biblioteca Ethers, que usei neste outro tutorial idêntico.
Não é imprescindível que você entenda como os smart contracts funcionam, mas ajuda bastante. Assista ao vídeo abaixo caso seja o seu primeiro contato com o assunto.

Vamos lá!
#1 – Estruturando o Projeto
O link do smart contract que vamos usar é este aqui e ele está publicado na Testnet da BNB Chain. Recomendo que dê uma olhada nele e faça alguns testes pelo painel do BSC Scan mesmo (link que forneci), a fim de entender do que se trata, embora te adianto que não tem nada de incrível, é apenas um CRUD de informações na blockchain.
Para fazer as operações de leitura você não precisará de nenhum tipo de autenticação, mas para conseguir fazer as operações de escrita você precisará pagar as taxas da rede (mesmo sendo rede de teste) e portanto estar conectado no BSC Scan com uma carteira de criptomoedas, sendo que a que recomendo é a MetaMask, que ensino a criar e configurar para testes no vídeo abaixo.

Ainda falando do smart contract, no link que passei antes, se você for na aba Contract, seção Code, encontrará o código fonte do mesmo e outra informação que precisamos para nossa integração, o ABI. ABI é uma sigla para Application Binary Interface (Interface de Aplicação Binária), algo como uma especificação de como este contrato deve ser invocado, caso queira consumir suas funções. Copie este ABI para um arquivo abi.json (o qual eu reproduzo abaixo), vamos precisar dele mais tarde.
|
1 2 3 |
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"components":[{"internalType":"string","name":"title","type":"string"},{"internalType":"uint16","name":"year","type":"uint16"}],"internalType":"struct BookDatabase.Book","name":"book","type":"tuple"}],"name":"addBook","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"","type":"uint32"}],"name":"books","outputs":[{"internalType":"string","name":"title","type":"string"},{"internalType":"uint16","name":"year","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"a","type":"string"},{"internalType":"string","name":"b","type":"string"}],"name":"compareStrings","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"count","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"id","type":"uint32"},{"components":[{"internalType":"string","name":"title","type":"string"},{"internalType":"uint16","name":"year","type":"uint16"}],"internalType":"struct BookDatabase.Book","name":"newBook","type":"tuple"}],"name":"editBook","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"id","type":"uint32"}],"name":"getBook","outputs":[{"components":[{"internalType":"string","name":"title","type":"string"},{"internalType":"uint16","name":"year","type":"uint16"}],"internalType":"struct BookDatabase.Book","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"id","type":"uint32"}],"name":"removeBook","outputs":[],"stateMutability":"nonpayable","type":"function"}] |
Agora sim, temos todo o necessário para começar a programar, então vamos criar nosso projeto. O Wagmi pode ser instalado em qualquer projeto React, mas é muito mais fácil quando criamos um projeto do zero usar o utilitário deles com o comando abaixo.
|
1 2 3 |
npm create wagmi@latest |
O nome do meu projeto será wagmi-react e o template será React com Vite. Todo o restante do setup de dependências, TypeScript e estrutura inicial fica por conta do utilitário. A biblioteca Wagmi fornece uma série de hooks React que facilitam enormemente nossa vida quando o assunto é criar o frontend de dapps web3. Ela usa o @wagmi/connectors para se conectar às carteiras mais populares do mercado e o Viem como biblioteca para comunicação com a blockchain, tudo isso com o mais alto nível de suporte a TypeScript e os recursos de fetch/caching/refetch/etc do React Query, se tornando a solução mais profissional atualmente para front web3.
O Wagmi adota uma abordagem baseada em componentes por padrão. Então a primeira coisa que recomendo é a criação de um componente com o código inicial de conexão que ele criou pra gente. Crie um arquivo Connection.tsx e mova pra ele TODO o conteúdo do App.tsx, apenas trocando o nome da função App() para Connection(). Já no App.tsx, subtitua pelo seguinte código, que apenas usa o referido componente.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import Connection from "./Connection" function App() { return ( <> <Connection /> </> ) } export default App |
Como por padrão o wagmi.ts vem configurado para Ethereum, ajuste para que aponte para BNB Chain e BNB Chain Testnet, como abaixo.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { createConfig, http } from 'wagmi' import { bsc, bscTestnet } from 'wagmi/chains' export const config = createConfig({ chains: [bsc, bscTestnet], transports: { [bsc.id]: http(), [bscTestnet.id]: http() }, }) declare module 'wagmi' { interface Register { config: typeof config } } |
Quando terminar esse setup, pode seguir as instruções do terminal para colocar o projeto para rodar, que já contará com o context provider do Wagmi no seu main.tsx configurado e uma interface com a funcionalidade de conectar na MetaMask. É interessante gastar alguns minutos dando uma olhada em tudo que foi criado para entender principalmente as configurações (na imagem abaixo o chain ID é da Sepolia, o seu deve aparecer o ID da BNB Chain).

Agora que terminamos o setup podemos começar a programar.
#2 – Gerenciando Conexão
O exemplo inicial do Wagmi é de conexão e desconexão de carteira. Antes de fazer funcionalidades autorais, vou propor alguns ajustes pontuais no Connection.tsx, para ele ter duas aparências distintas (conectado/não conectado) e menos informações na página também. Essas alterações vão servir como gancho também para eu explicar alguns conceitos importantes.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import { useConnect, useConnection, useConnectors, useDisconnect } from 'wagmi' function Connection() { const connection = useConnection() const { connect, status, error } = useConnect() const connectors = useConnectors() const { disconnect } = useDisconnect() return ( <> <div> <h2>Connection</h2> { connection.status === 'connected' ? ( <div> <strong>Address:</strong> {JSON.stringify(connection.addresses)} <button type="button" onClick={() => disconnect()}> Disconnect </button> </div> ) : <div> {connectors.map((connector) => ( <button key={connector.uid} onClick={() => connect({ connector })} type="button" > {connector.name} </button> ))} </div> } <div>{error?.message}</div> </div> </> ) } export default Connection |
O Wagmi é todo baseado em React Hooks, então temos nesse exemplo:
- useConnection: hook que traz um objeto com informações da conexão com a carteira, como o status (“connected”);
- useConnect: hook que traz um objeto com função para conectar, status e error de conexão;
- useConnectors: hook que traz o array de conectores configurados no seu projeto (wagmi.ts);
- useDisconnect: hook que traz objeto com função para desconectar;
Dito isso, neste componente acima eu me baseio no connection.status para exibir endereço e botão de desconectar ou então o botão de conexão. Em ambos os casos, fica sempre exposto uma possível mensagem de erro. Sobre as ações de conexão e desconexão em si, a disconnect() não exige parâmetro algum, já a connect precisa que seja passado o connector para que a solicitação de permissão seja feita corretamente para a carteira.
Recomendo que teste tanto a conexão quanto a desconexão, para entender o fluxo e garantir que esteja funcionando.
#3 – Operação de Leitura
Em nosso projeto React vamos implementar a interface de apenas duas operações, a pesquisa de um cadastro e a inserção de um novo. Com base nestas duas operações, uma de leitura e outra de escrita, você terá a base necessária para avançar em outras operações e até mesmo usando outros contratos.
Para a operação de leitura devemos criar um novo componente Search.tsx que espere receber o ID de um livro cadastrado no respectivo contrato e um botão para fazer a pesquisa por ele. Vamos começar pelos imports, que explicarei conforme usarmos eles a seguir.
|
1 2 3 4 5 6 7 8 |
import { useState } from 'react'; import { readContract } from '@wagmi/core'; import { useConfig } from 'wagmi'; import { Abi } from 'viem'; export default function Search(props: { address: `0x${string}`, abi: Abi }) { |
Repare que já adicionei todos os imports que vamos usar, incluindo o type Abi do Viem. Na assinatura do function component Search, defini que vamos receber address e abi por parâmetro. Agora vamos declarar as variáveis e states dentro da função Search do componente.
|
1 2 3 4 5 6 7 8 |
const config = useConfig(); const [bookId, setBookId] = useState(''); const [bookData, setBookData] = useState<any>(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); |
O useConfig é um hook do Wagmi que traz o objeto com as configurações do nosso wagmi.ts. Já os states servem para:
- bookId: id do livro a ser pesquisado;
- bookData: dados do livro encontrado;
- isLoading: se existe uma consulta em andamento;
- error: erros na consulta, se houverem;
Agora vamos criar o código para renderizar o formulário de pesquisa e seções para os demais states.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
return ( <div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxWidth: 400 }}> <h2>Search</h2> <input type="number" placeholder="Book ID:" value={bookId} onChange={(e) => setBookId(e.target.value)} /> <button onClick={btnSearchClick} disabled={isLoading}> {isLoading ? 'Searching...' : 'Search Book'} </button> {error && <div style={{ color: 'red' }}>Error: {error}</div>} {bookData && ( <div style={{ border: '1px solid #ccc', padding: 8 }}> <h3>Book Data:</h3> <pre>{JSON.stringify(bookData, null, 2)}</pre> </div> )} </div> ) |
Esse código não tem muito o que explicar visto que é bem tradicional de ReactJS. Além do que, o formulário de pesquisa tem apenas um campo, que guarda o id digitado no state, e um botão, que dispara a função de pesquisa a seguir.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
async function btnSearchClick() { if (!bookId) { setError('Informe um ID válido.'); return; } setError(null); setIsLoading(true); try { const result = await readContract(config, { address: props.address, abi: props.abi, functionName: 'getBook', args: [BigInt(bookId)], }) setBookData(result); } catch (err: any) { setError(err.message); } setIsLoading(false); } |
Após a validação inicial e definição dos states de status, usamos a função readContract do Wagmi para fazer uma call para o smart contract BookDatabase na BNB Chain. A função espera o objeto de configuração do Wagmi e mais alguns parâmetros próprios:
- address: endereço do contrato na blockchain, que veio nos props do componente;
- abi: Application Binary Interface do contrato, que veio nos props do componente;
- functionName: nome da função no contrato-alvo;
- args: array de parâmetros da função no contrato, em ordem;
Para usar este componente, declare ele no App.tsx como abaixo, onde carregamos o ABI do arquivo correspondente e informei o endereço do contrato na blockchain. Ambos parâmetros eu passo como propriedades do componente Search que os espera.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { Abi } from "viem"; import Connection from "./Connection"; import Search from "./Search"; import Form from "./Form"; import ABI from './abi.json'; const CONTRACT_ADDRESS = '0x31d20731446bdd56b114d9699d392f3a67171f85'; function App() { return ( <> <Connection /> <Search abi={ABI as Abi} address={CONTRACT_ADDRESS} /> </> ) } export default App |
Como resultado, temos a imagem abaixo onde pesquisa o livro com ID 1.

E com isso concluímos a primeira funcionalidade do nosso dapp.
#3 – Operação de Escrita
A próxima funcionalidade que vamos adicionar no nosso dapp é de cadastrar novos dados na blockchain usando aquele mesmo smart contract de antes. Para isso teremos um processo relativamente semelhante. Vamos começar criando o arquivo do outro componente, Form.tsx, iniciando-o como abaixo.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { useState } from 'react'; import { writeContract } from '@wagmi/core'; import { useConfig } from 'wagmi'; import { Abi } from 'viem'; type Book = { title: string; year: number; } export default function Form(props: { address: `0x${string}`, abi: Abi }) { |
Começamos com importações muito parecidas com as do componente anterior, apenas substituindo a função readContract por uma writeContract, que serve para enviar transações para um contrato. Também definimos o type dos livros a serem cadastrados e criamos a função do componente (Form) esperando os parâmetros necessários em props (address e abi).
Agora, vamos criar os states e carregar as configurações.
|
1 2 3 4 5 6 7 8 |
const config = useConfig(); const [book, setBook] = useState<Book>({ title: "", year: 0 }); const [tx, setTx] = useState<any>(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); |
Temos um state para os dados do livro a ser cadastrado (book) e um para o retorno da transação, que nada mais é do que o hash da mesma (tx). Demais states já foram vistos no componente anterior. Agora vamos fazer o formulário de cadastro de livro.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
return ( <div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxWidth: 400 }}> <h2>New Book</h2> <input type="text" placeholder="Title:" value={book.title} onChange={(e) => setBook(prevState => ({ ...prevState, title: e.target.value }))} /> <input type="number" placeholder="Year:" value={book.year} onChange={(e) => setBook(prevState => ({ ...prevState, year: Number(e.target.value) }))} /> <button onClick={btnSaveClick} disabled={isLoading}> {isLoading ? 'Saving...' : 'Save Book'} </button> {error && <div style={{ color: 'red' }}>Error: {error}</div>} {tx && ( <div style={{ border: '1px solid #ccc', padding: 8 }}> <h3>Tx Hash:</h3> <pre>{tx}</pre> </div> )} </div> ) |
Aqui eu posicionei dois inputs para os campos do livro, com ambos setando o state de book mantendo os demais campos e alterando somente o que foi digitado. Por fim, temos o botão que dispara a função de salvamento (a seguir) e a exibição do hash da mesma.
Agora a programação do salvamento se torna bem simples pois já deixamos quase tudo pronto nos códigos anteriores, basta resetar os states e chamar a writeContract passando os parâmetros apropriados, sendo que a função agora é a addBook no contrato, que espera um único parâmetro com o objeto do livro a ser salvo.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
async function btnSaveClick() { setError(null); setIsLoading(true); try { const result = await writeContract(config, { address: props.address, abi: props.abi, functionName: 'addBook', args: [book], }) setTx(result); } catch (err: any) { setError(err.message); } setIsLoading(false); } |
Com isso, ao clicar no botão de salvar ativamos a carteira MetaMask do usuário que vai pedir o consentimento do mesmo no navegador, pois toda transação envolve custos, pagos com o saldo do usuário na moeda da rede (ETH na Ethereum, BNB na BSC, etc). Uma vez que o usuário autorize a transação, podemos fazer a transação que desejamos invocar, addBook neste caso. Como retorno você terá o hash da transação que é a única informação que nos importa no momento.
E com isso agora você sabe como implementar chamadas de leitura e de escrita em qualquer smart contract de blockchains compatíveis com a EVM (Ethereum Virtual Machine). Caso queira aprender a criar smart contracts, este tutorial pode te ajudar.
Um abraço e até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.



