Dans la première partie de ce tutoriel, publiée récemment sur ce site, nous avons effectué la configuration initiale de l’application et la première fonctionnalité, à savoir l’initialisation du programme Solana sur la blockchain. Dans cette deuxième étape, je vais mettre en œuvre le CRUD des livres via le frontend, comme convenu.
Allons-y !
#1 – Enregistrement d’un Livre
Pour pouvoir enregistrer un livre, nous avons quelques défis à relever. L’un d’eux a été résolu lors de la première étape, où nous devions découvrir le PDA de la bibliothèque, vous vous souvenez ? Le second consiste à déterminer quel sera l’ID du prochain livre à enregistrer. En effet, les PDAs des livres utilisent leur identifiant comme seed de l’adresse.
Le prochain ID est une caractéristique gérée par le compte de la bibliothèque, alors allons dans Web3Service.ts pour créer une fonction qui nous fournit cette information.
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;
}
Dans getNextBookId nous récupérons le PDA de la bibliothèque, puis nous renvoyons le compte directement depuis la blockchain, en déclenchant une erreur s’il n’existe pas. Or, ce compte est codé en binaire, un format peu pratique pour le frontend, non ? Dans ce cas, nous avons besoin d’une fonction de décodage, que nous allons créer comme ci-dessous.
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(),
};
}
Cette fonction utilise l’objet accountsCoder pour décoder les octets en suivant la logique selon laquelle la structure s’appelle Library et qu’elle contient next_id et authority.
Le prochain défi pour la création d’un nouveau livre, en considérant que chacun d’eux est enregistré dans son propre compte PDA, est de dériver son adresse en utilisant des seeds. Ce que la fonction getBookPda va faire pour nous.
function getBookPda(bookId: number) {
return getPda([
encoder.encode("book"),
getLibraryPda().toBytes(),
Uint8Array.from(new BN(bookId).toArray("le", 4)),
]);
}
Attention particulière ici à la transmission du bookId, qui est un BN (BigNumber) mais qui doit être passé sous forme de tableau d’octets en little endian (le). Comme nos bookIds sont des u32, c’est-à-dire des entiers non signés sur 32 bits, toArray doit utiliser 4 comme longueur dans le paramètre length puisqu’il y a 4 octets par identifiant.
Maintenant oui, nous pouvons créer la fonction addBook dans Web3Service.ts car nous avons résolu toutes les dépendances des autres fonctions.
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 },
]);
}
La fonction addBook attend le livre à enregistrer, le portefeuille de l’utilisateur (préalablement connecté) et la connexion.
Elle commence par récupérer le PDA de la bibliothèque et, avec celui-ci, le prochain ID du livre à enregistrer. Avec ces informations, il est possible de dériver l’adresse du compte PDA du livre et d’envoyer la transaction de création du même.
La transaction de création du livre est un appel à la fonction “add_book” contenant un livre dans l’objet des instructions. Ensuite, on passe le wallet, la connection et le tableau des comptes à utiliser dans le contexte de la transaction, conformément à ce qui est exigé par le programme.
Avec la fonction prête dans Web3Service.ts, nous pouvons passer à la création d’un composant BookForm.tsx pour le formulaire d’enregistrement/édition, en commençant par les imports.
import { useState } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import type { DecodedBookAccount } from "./Web3Service";
import { addBook } from "./Web3Service";
Et ensuite les états et l’utilisation des 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(false
Le formulaire lui-même est assez simple et rien de particulier. Il se contente de récupérer les valeurs des champs et de les stocker dans l’état 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>
)
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);
}
}
Cette fonction prend en charge plusieurs validations lorsque l’on enregistre un nouveau livre et lorsque le portefeuille est déjà connecté. Lorsque tout est correct, elle vérifie s’il s’agit d’un nouvel enregistrement et appelle la fonction correspondante. L’apparence du formulaire peut être vue ci-dessous et il devrait être fonctionnel pour être utilisé.
#2 – Recherche et Édition de Livre
Avant de mettre en œuvre l’édition, il est important de commencer par la recherche, car sans elle nous ne saurions pas si l’enregistrement et, plus tard, l’édition fonctionnent réellement.
Pour la recherche, revenons dans Web3Service.ts afin de créer une fonction qui décode les octets d’un livre en un objet lisible.
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,
};
}
Nous l’avons déjà fait auparavant, mais pour library, vous vous souvenez ? La logique ici est la même mais adaptée à la réalité de la structure Book.
Ensuite, créons la fonction getBookData, qui servira de fonction de recherche, car elle reçoit un ID de livre et recherche son compte sur la blockchain, en décodant les octets trouvés ou en renvoyant une erreur s’il n’est pas trouvé.
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);
}
Pour l’interface utilisateur, créez un composant BookSearch.tsx avec les imports suivants :
import { useState } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { getBookData } from "./Web3Service";
Et les états et l’usage des hooks suivants.
export default function BookSearch() {
const { connection } = useConnection();
const { publicKey } = useWallet();
const [bookId, setBookId] = useState("0");
const [result, setResult] = useState("");
const [error, setError] = useState("");
Avec tout ceci prêt, créer le formulaire de recherche est très simple, comme ci-dessous.
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>
);
Et la fonction appelée par le bouton de recherche peut être vue ci-après.
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");
}
}
Cela vous offrira l’apparence ci-dessous, ainsi que la fonctionnalité d’indiquer l’ID d’un livre et de vérifier s’il existe ou non, ce que vous devriez tester avant de progresser.

Après avoir terminé la fonctionnalité de recherche, nous pouvons maintenant vérifier si les livres s’enregistrent correctement. S’ils le font, il est temps d’effectuer l’édition, en commençant par ajouter une nouvelle fonction dans Web3Service.ts pour cette finalité, qui utilise plusieurs autres fonctions que nous avons déjà créées précédemment.
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 },
]);
}
Avec la fonction prête, il suffit d’apporter quelques ajustements au composant BookForm.tsx pour qu’il fonctionne aussi pour les éditions. La première consiste à importer la fonction editBook depuis Web3Service.ts en haut du composant et l’autre consiste à ajuster la fonction de clic du bouton pour, si l’ID est renseigné, appeler l’édition au lieu de la création, comme ci-dessous.
if (book.id === 0) {
await addBook(book, wallet, connection);
setError("Book added successfully!");
} else {
await editBook(book, wallet, connection);
setError("Book updated successfully!");
}
Avec cela, nous venons d’ajouter deux fonctionnalités supplémentaires du CRUD; il ne reste plus que la dernière, la suppression.
#3 – Suppression d’un Livre
La dernière fonctionnalité que nous allons ajouter à notre dApp est la suppression des livres préalablement enregistrés. Pour cela, nous allons créer une nouvelle fonction dans Web3Service.ts nommée deleteBook, comme ci-dessous.
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 },
]);
}
Cette fonction n’apporte absolument rien de nouveau par rapport aux précédentes, alors passons à la création du composant BookDelete.tsx, en commençant par ses imports.
import { useState } from "react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { deleteBook } from "./Web3Service";
Puis ses états et l’usage des hooks.
export default function BookDelete() {
const { connection } = useConnection();
const wallet = useWallet();
const [bookId, setBookId] = useState("0");
const [error, setError] = useState("");
Ensuite, son interface utilisateur (presque identique à celle de la recherche) :
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>
);
Et enfin, la fonction onclick du bouton de suppression.
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");
}
}
Aucune nouveauté dans ces étapes. Importez ce composant dans App.tsx, placez-le et vous obtiendrez quelque chose comme ci-dessous.

Maintenant vous pouvez tester toutes les opérations de base du CRUD de manière parfaite, en finalisant notre première dApp web3 React + Solana.
Amicalement et à la prochaine !
Auteur
Fabien Delpont
Fabien Delpont, développeur et créateur du site Python Doctor.

Python débutant
- Présentation Python
- Installer Python
- Interpréteur python
- IDE Editeurs python
- Calculs et variables
- Les listes
- Les tuples
- Les dictionnaires
- Les fonctions
- Les fonctions natives
- Conditions if elif else
- Boucle for / while
- Les modules/packages
- Les exceptions
- Les compréhensions de liste
- Programmation orientée objet
- Les décorateurs
- Les itérateurs/générateurs
- Expressions régulières
- Lire / Editer un fichier
- PEP 8 / bonnes pratiques
- Black formateur de code
Python avancé
- Différences python 2 / 3
- Encodage python
- Pip installe vos librairies
- Virtualenv
- Debug
- Python Path
- Gestion fichiers et dossiers
- Interface graphique tKinter
- Graphiques scientifiques
- Programmation asynchrone
- XML et python
- BeautifulSoup / HTML parser
- Créer un exécutable
- Dossier partagé / samba
- FTP / SFTP
- Fabric SSH
- Envoyer un mail SMTP
- Réseau / socket
- Base de données
- Créer un serveur web
- Websocket & Crossbar
- Générateur de site statique
- Numba
- Tests unitaires
- Deep Learning IA
- Deep Learning & PyTorch
- Block Chain
Django
- Présentation Django
- Installer Django
- Initialiser un projet Django
- Créer une application Django
- ORM Django
- Login Django
- Les champs des modèles
- Interface admin Django
- Queryset
- Many to many relation
- Les vues
- Les formulaires
- CSRF Token
- Les middlewares
- Templates Django
- Context Processor
- Signaux Django
- Xadmin
- Django select2
- Crispy Forms
- AngularJS et Django
- Uploader un fichier en ajax
- Django Rest Framework
- Django déploiement



