Na primeira parte deste tutorial, que saiu recentemente aqui no site, a gente fez o setup inicial da aplicação e a primeira funcionalidade que era de inicialização do programa Solana na blockchain. Nesta segunda etapa, eu vou implementar o CRUD de livros através do frontend, conforme combinado.
Vamos lá!
#1 – Cadastrando um Livro
Para conseguir cadastrar um livro nós temos alguns desafios antes. Um deles nós solucionamos na primeira etapa, onde tínhamos de descobrir o PDA da library, lembra? O segundo é descobrir qual o ID do próximo livro a ser cadastrado. Isso porque os PDAs de livros usam do seu id como seed do endereço.
O próximo ID é uma característica gerenciada pela account de library, então vamos no Web3Service.ts criar uma função que traz essa informação pra gente.
|
1 2 3 4 5 6 7 8 9 10 |
async function getNextBookId(connection: Connection) { const libraryPda = getLibraryPda(); const account = await connection.getAccountInfo(libraryPda); if (!account) throw new Error("Library account not found, the program might not be initialized"); const libraryData = decodeLibraryAccountData(account.data); return libraryData.nextId; } |
Na getNextBookId nós pegamos o PDA da library, depois retornamos a account diretamente da blockchain, disparando um erro caso ela não exista. Só que essa account vem codificada em binário, um formato não muito útil para o frontend, certo? Neste caso precisamos de uma função de decodificação, que criaremos como abaixo.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function decodeLibraryAccountData(data: Uint8Array): DecodedLibraryAccount { const decoded = accountsCoder.decode("Library", Buffer.from(data)) as { next_id: number | BN; authority: PublicKey; }; return { nextId: BN.isBN(decoded.next_id) ? decoded.next_id.toNumber() : decoded.next_id, authority: decoded.authority.toBase58(), }; } |
Essa função usa o objeto accountsCoder para decodificar os bytes seguindo a lógica que a struct se chama Library e que dentro dela tem next_id e authority.
O próximo desafio para criação de um novo livro, considerando que cada um deles é salvo em seu próprio PDA Account, é derivar seu endereço usando as seeds. Coisa que a função getBookPda vai fazer pra gente.
|
1 2 3 4 5 6 7 8 9 |
function getBookPda(bookId: number) { return getPda([ encoder.encode("book"), getLibraryPda().toBytes(), Uint8Array.from(new BN(bookId).toArray("le", 4)), ]); } |
Atenção especial aqui à passagem do bookId, que é um BN (bignumber) mas que deve ser passado como array de bytes little endian (le). Como nossos bookIds são u32, ou seja usigned integers de 32-bit, o toArray deve usar 4 no parâmetro length pois são 4 bytes por ID.
Agora sim podemos criar a função addBook no Web3Service.ts pois resolvemos todas as dependências de outras funções.
|
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 |
export async function addBook(book: DecodedBookAccount, wallet: WalletContextState, connection: Connection) { if (!wallet.publicKey) throw new Error("Wallet not connected"); const libraryPda = getLibraryPda(); const nextId = await getNextBookId(connection); console.log("Next book ID:", nextId); const bookPda = getBookPda(nextId); console.log("Book PDA:", bookPda.toBase58()); return sendTransaction("add_book", { book: { title: book.title, author: book.author, year: book.year, }, }, wallet, connection, [ { pubkey: libraryPda, isSigner: false, isWritable: true }, { pubkey: bookPda, isSigner: false, isWritable: true }, { pubkey: wallet.publicKey, isSigner: true, isWritable: true }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, ]); } |
A função addBook espera o livro a ser cadastrado, a carteira do usuário (previamente conectada) e a conexão.
Ela começa pegando o PDA da library e com ele o próximo ID do livro a ser cadastrado. Com essas informações é possível derivar o endereço da PDA Account de livro e com isso enviar a transação de criação do mesmo.
A transação de criação de livro é uma chamada à função “add_book” contendo um book no objeto de instruções. Depois passa-se a wallet, a connection e o array de accounts a serem usadas no contexto da transação, conform exigido pelo programa.
Com a função pronta no Web3Service.ts, podemos partir para a criação de um componente BookForm.tsx para o form de cadastro/edição, a começar pelos imports.
|
1 2 3 4 5 6 |
import { useState } from "react"; import { useConnection, useWallet } from "@solana/wallet-adapter-react"; import type { DecodedBookAccount } from "./Web3Service"; import { addBook } from "./Web3Service"; |
E depois os states e uso de hooks.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
export default function BookForm() { const { connection } = useConnection(); const wallet = useWallet(); const [book, setBook] = useState<DecodedBookAccount>({ id: 0, title: "", author: "", year: 0, }); const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); |
O formulário em si é bem simples e não tem nada demais. Apenas vai pegando os valores dos inputs e jogando no state de book.
|
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 |
return ( <div style={{ margin: 20 }}> <h2>Book Form</h2> <p> <label>ID: <input type="number" value={book.id} onChange={(evt) => setBook({ ...book, id: parseInt(evt.target.value) })} disabled={isLoading} /></label> </p> <p> <label>Title: <input type="text" value={book.title} onChange={(evt) => setBook({ ...book, title: evt.target.value })} disabled={isLoading} /></label> </p> <p> <label>Author: <input type="text" value={book.author} onChange={(evt) => setBook({ ...book, author: evt.target.value })} disabled={isLoading} /></label> </p> <p> <label>Year: <input type="number" value={book.year} onChange={(evt) => setBook({ ...book, year: parseInt(evt.target.value) })} disabled={isLoading} /></label> </p> <p> <button type="button" onClick={onSaveClick} disabled={isLoading}> {isLoading ? "Saving..." : "Save"} </button> </p> <p style={{ color: error.includes("successfully") ? "green" : "red" }}> {error} </p> <hr /> </div> ) |
Quando o botão é clicado, dispara uma função onSaveClick como abaixo.
|
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 |
async function onSaveClick() { if (!book.id && (!book.title || !book.author || book.year <= 0)) { setError("Please fill in all fields with valid values"); return; } if (!wallet.publicKey) { setError("Wallet not connected"); return; } setError(""); setIsLoading(true); console.log("Saving book:", book); try { if (book.id === 0) { await addBook(book, wallet, connection); setError("Book added successfully!"); } setBook({ id: 0, title: "", author: "", year: 0, }); } catch (err) { setError("Save failed: " + (err as Error).message); } finally { setIsLoading(false); } } |
Essa função se encarrega de algumas validações em caso de cadastro de novo livro e de carteira já conectada. Tudo estando certo, ela verifica se é um novo cadastro e chama a função correspondente. A aparência do form pode ser vista abaixo e ele já deve estar funcional para ser usado.

#2 – Pesquisa e Edição de Livro
Antes de fazer a implementação para edição acho importante implementarmos a pesquisa primeiro, já que sem ela não temos como saber se de fato o cadastro e, mais tarde, a edição, estão funcionando.
Para a pesquisa, vamos voltar no Web3Service.ts para criar uma função que decodifica os bytes de um livro para um objeto legível.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function decodeBookAccountData(data: Uint8Array): DecodedBookAccount { const decoded = accountsCoder.decode("Book", Buffer.from(data)) as { id: number | BN; title: string; author: string; year: number | BN; }; return { id: BN.isBN(decoded.id) ? decoded.id.toNumber() : decoded.id, title: decoded.title, author: decoded.author, year: BN.isBN(decoded.year) ? decoded.year.toNumber() : decoded.year, }; } |
Já fizemos isso antes, mas para library, lembra? A lógica aqui é a mesma mas ajustando à realidade da struct de livro.
Na sequência, vamos criar a função getBookData, que servirá como função de pesquisa, pois recebe um ID de livro e procura a account dele na blockchain, decodificando os bytes encontrados ou um erro caso não encontre.
|
1 2 3 4 5 6 7 8 9 10 |
export async function getBookData(bookId: number, connection: Connection) { const bookPda = getBookPda(Number(bookId)); const account = await connection.getAccountInfo(bookPda); if (!account) throw new Error("Book not found"); return decodeBookAccountData(account.data); } |
Agora para a UI, crie um componente BookSearch.tsx com os seguintes imports:
|
1 2 3 4 5 |
import { useState } from "react"; import { useConnection, useWallet } from "@solana/wallet-adapter-react"; import { getBookData } from "./Web3Service"; |
E os seguintes states e uso de hooks.
|
1 2 3 4 5 6 7 8 9 |
export default function BookSearch() { const { connection } = useConnection(); const { publicKey } = useWallet(); const [bookId, setBookId] = useState("0"); const [result, setResult] = useState(""); const [error, setError] = useState(""); |
Com isso tudo preparado, criar o form de pesquisa é bem simples, como abaixo.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
return ( <div style={{ margin: 20 }}> <h2>Book Search</h2> <p> <label>Book ID: <input type="number" value={bookId} onChange={(evt) => setBookId(evt.target.value)} /></label> </p> <p>{result}</p> <p> <button type="button" onClick={onSearchClick}>Search</button> </p> <p style={{ color: "red" }}> {error} </p> <hr /> </div> ); |
E a função que o botão de pesquisar chama, pode ser vista a seguir.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
async function onSearchClick() { try { if (!publicKey) { setError("Wallet not connected"); return; } const book = await getBookData(Number(bookId), connection); setResult(JSON.stringify(book, null, 2)); setError(""); } catch (err) { console.error(err); setError("Book not found"); } } |
Isso te dará uma aparência como abaixo, bem como a funcionalidade de informar um ID de livro e ver se ele existe ou não, o que você deve testar antes de prosseguir.

Terminada a funcionalidade de pesquisar, agora podemos ver se os livros estão sendo cadastrados corretamente. Eles estando, é hora de fazermos a edição, primeiramente adicionando uma nova função no Web3Service.ts para esta finalidade, que usa diversas outras funções que já criamos anteriormente.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
export async function editBook(book: DecodedBookAccount, wallet: WalletContextState, connection: Connection) { if (!wallet.publicKey) throw new Error("Wallet not connected"); const libraryPda = getLibraryPda(); const bookPda = getBookPda(book.id); return sendTransaction("edit_book", { id: Number(book.id), new_data: { title: book.title, author: book.author, year: book.year, }, }, wallet, connection, [ { pubkey: libraryPda, isSigner: false, isWritable: true }, { pubkey: bookPda, isSigner: false, isWritable: true }, ]); } |
Com a função pronta, basta alguns ajustes no componente BookForm.tsx para que ele funcione para edições também. A primeira é importar a função editBook do Web3Service.ts no topo do componente e a outra é ajustar a função de click do botão para, em caso do id ser preenchido, chamar a edição ao invés de criação, como abaixo.
|
1 2 3 4 5 6 7 8 9 |
if (book.id === 0) { await addBook(book, wallet, connection); setError("Book added successfully!"); } else { await editBook(book, wallet, connection); setError("Book updated successfully!"); } |
Com isso, matamos agora mais duas funcionalidades do CRUD, faltando apenas a última, de exclusão.
#3 – Exclusão de Livro
A última funcionalidade que vamos adicionar no nosso dapp é de excluir livros previamente cadastrados. Para isso vamos criar uma nova função no Web3Service.ts chamada deleteBook, como abaixo.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
export async function deleteBook(bookId: number, wallet: WalletContextState, connection: Connection) { if (!wallet.publicKey) throw new Error("Wallet not connected"); const libraryPda = getLibraryPda(); const bookPda = getBookPda(bookId); return sendTransaction("delete_book", {}, wallet, connection, [ { pubkey: libraryPda, isSigner: false, isWritable: true }, { pubkey: bookPda, isSigner: false, isWritable: true }, { pubkey: wallet.publicKey, isSigner: true, isWritable: true }, ]); } |
Esta função não tem absolutamente nenhuma novidade frente à anteriores, então vamos para a criação do componente BookDelete.tsx, a começar pelos seus imports.
|
1 2 3 4 5 |
import { useState } from "react"; import { useConnection, useWallet } from "@solana/wallet-adapter-react"; import { deleteBook } from "./Web3Service"; |
Depois os seus states e uso de hooks.
|
1 2 3 4 5 6 7 8 9 |
export default function BookDelete() { const { connection } = useConnection(); const wallet = useWallet(); const [bookId, setBookId] = useState("0"); const [error, setError] = useState(""); |
Na sequência sua UI (que é quase idêntica à de pesquisa):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
return ( <div style={{ margin: 20 }}> <h2>Book Delete</h2> <p> <label>Book ID: <input type="number" value={bookId} onChange={(evt) => setBookId(evt.target.value)} /></label> </p> <p> <button type="button" onClick={onDeleteClick}>Delete</button> </p> <p style={{ color: error.includes("successfully") ? "green" : "red" }}> {error} </p> <hr /> </div> ); |
E por último a função de click do botão de exclusão.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function onDeleteClick() { try { if (!wallet.publicKey) { setError("Wallet not connected"); return; } await deleteBook(Number(bookId), wallet, connection); setError("Book deleted successfully!"); } catch (err) { console.error(err); setError("Book not found"); } } |
Nenhuma novidade nestas etapas. Importe este componente no App.tsx, posicione-o e terá algo como abaixo.

Agora você pode testar todas as operações básicas do CRUD com perfeição, finalizando a nossa implementação de um primeiro dapp web3 React + Solana.
Um abraço e até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.


