
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.
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.
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.
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.
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.
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.
xport 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(falseO 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.
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.
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.
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.
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:
import { useState } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { getBookData } from "./Web3Service";E os seguintes states e uso de hooks.
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.
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.
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.
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.
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.
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.
import { useState } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { deleteBook } from "./Web3Service";Depois os seus states e uso de hooks.
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):
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.
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!








