Introduction au Compteur Web3
Ce document détaille la conception et l'implémentation d'une application de compteur Web3, s'appuyant sur l'écosystème Hardhat pour le développement de contrats intelligents et Ethers.js pour l'interaction frontend. Ce projet sert de base pour comprendre l'architecture des applicaitons décentralisées (DApps).
Les composants clés incluent :
- Contrat Intelligent : Un contrat Solidity simple pour gérer une valeur numérique.
- Scripts de Déploiement : Des scripts TypeScript pour déployer le contrat sur une blockchain.
- Interface Utilisateur : Une application web qui interagit avec le contrat via MetaMask et Ethers.js.
Architecture Générale de l'Interaction
┌─────────────────────────────────────────────────────────────────┐
│ Navigateur de l'Utilisateur │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Application Frontend (index.ts) │ │
│ │ - Librairie Ethers.js │ │
│ │ - Connexion et interaction avec MetaMask │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ Appels via Ethers.js
▼
┌─────────────────────────────────────────────────────────────────┐
│ Portefeuille MetaMask │
│ - Sécurise les clés privées │
│ - Signe les transactions │
│ - Transmet les transactions à la blockchain │
└─────────────────────────────────────────────────────────────────┘
│
│ Transactions signées
▼
┌─────────────────────────────────────────────────────────────────┐
│ Chaîne Locale Hardhat (localhost:8545) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Contrat Intelligent CompteurDeValeur (adresse: 0x5Fb...)│ │
│ │ - incrementerCompteur(): augmente la valeur │ │
│ │ - lireValeurActuelle(): récupère la valeur actuelle │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Détail de l'Implémentation et du Flux d'Exécution
Étape 1: Création du Contrat Intelligent CompteurDeValeur.sol
Ce contrat Solidity gère une unique valeur numérique qui peut être incrémentée et lue.
pragma solidity ^0.8.24;
import "hardhat/console.sol"; // Outil de log fourni par Hardhat
/**
* @title CompteurDeValeur
* @dev Contrat simple pour stocker et incrémenter une valeur numérique.
*/
contract CompteurDeValeur {
uint256 public currentValue; // Variable d'état stockée sur la blockchain
// Événement émis chaque fois que la valeur du compteur est mise à jour
event ValueUpdated(uint256 newValue);
/**
* @notice Incrémente la valeur actuelle du compteur de un.
* Cette fonction modifie l'état du contrat et nécessite donc du gaz.
*/
function incrementerCompteur() public {
currentValue++;
console.log("Nouvelle valeur du compteur: ", currentValue); // Log sur la console de la chaîne locale
emit ValueUpdated(currentValue); // Émet un événement pour notifier les observateurs
}
/**
* @notice Retourne la valeur actuelle du compteur.
* @dev Cette fonction est une fonction "view", elle ne modifie pas l'état et ne coûte pas de gaz.
* @return La valeur numérique actuelle du compteur.
*/
function lireValeurActuelle() public view returns (uint256) {
return currentValue;
}
}
Points clés :
uint256 public currentValue: Une variable d'état persistante sur la blockchain. Chaque modification entraîne des frais de gaz.event ValueUpdated: Permet au contrat d'émettre des notifications que les applications frontend peuvent écouter en temps réel.incrementerCompteur(): Une opération d'écriture nécessitant du gaz.lireValeurActuelle(): Une fonction de lecture (view) qui n'entraîne aucun coût de gaz.
Étape 2: Rédaction du Script de Déploiement deployer-compteur.ts
Ce script TypeScript, exécuté avec Hardhat, est responsable de la compilation et du déploiement du contrat sur un réseau Ethereum.
import "@nomicfoundation/hardhat-ethers"; // Importe le plugin ethers de Hardhat
import { ethers } from "hardhat"; // Importe la bibliothèque ethers
/**
* @notice Déploie le contrat CompteurDeValeur sur le réseau configuré.
* @returns L'instance du contrat déployé.
*/
async function deployerContrat() {
console.log("Préparation du déploiement du contrat CompteurDeValeur...");
// 1. Obtention du ContractFactory (ABI compilé + bytecode du contrat)
const CompteurFactory = await ethers.getContractFactory("CompteurDeValeur");
// 2. Déploiement de l'instance du contrat sur la blockchain
const instanceCompteur = await CompteurFactory.deploy();
// 3. Attente de la confirmation du déploiement
await instanceCompteur.waitForDeployment();
// 4. Affichage de l'adresse du contrat déployé
const adresseDeplogee = await instanceCompteur.getAddress();
console.log(`Contrat CompteurDeValeur déployé à l'adresse: ${adresseDeplogee}`);
return instanceCompteur;
}
/**
* @notice Simule une interaction initiale avec le contrat déployé.
* @param instanceContrat L'instance du contrat à interagir.
*/
async function interagirAvecContrat(instanceContrat: any) {
console.log("Simulation d'interaction: Incrémentation initiale...");
// 5. Appel de la méthode incrementerCompteur() du contrat
let txResponse = await instanceContrat.incrementerCompteur();
await txResponse.wait(); // Attendre que la transaction soit minée et confirmée
console.log(`Transaction d'incrémentation envoyée: ${txResponse.hash}`);
// 6. Lecture de la valeur actuelle après l'incrémentation
const valeurLue = await instanceContrat.lireValeurActuelle();
console.log(`Valeur actuelle du compteur sur la chaîne: ${valeurLue}`);
}
// Fonction principale pour orchestrer les actions
async function main() {
const contratDeploge = await deployerContrat();
await interagirAvecContrat(contratDeploge);
}
// Exécuter la fonction principale et gérer les erreurs
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Points clés :
ethers.getContractFactory("CompteurDeValeur"): Récupère la définition du contrat compilé (ABI et bytecode).deploy(): Envoie la transaction de déploiement du contrat au réseau.waitForDeployment(): Attend que la transaction de déploiement soit incluse dans un bloc.getAddress(): Récupère l'adresse unique où le contrat a été déployé.
Étape 3: Lancement de la Chaîne Locale Hardhat
npx hardhat node
Cette commande :
- Démarre un nœud Ethereum local émulé.
- Crée 20 comptes de test avec 10 000 ETH chacun pour le développement.
- Est accessible via
http://127.0.0.1:8545. - Idéal pour le développement et les tests en local.
Étape 4: Exécution du Script de Déploiement
npx hardhat run scripts/deployer-compteur.ts --network localhost
Cette commande déploie le contrat CompteurDeValeur sur le nœud Hardhat local. Elle exécute le script deployer-compteur.ts et affiche l'adresse du contrat (par exemple, 0x5FbDB2315678afecb367f032d93F642f64180aa3).
Étape 5: Développement de l'Interface Frontend src/index.ts
Cette application web utilise Ethers.js pour se connecter à MetaMask, interagir avec le contrat déployé et afficher les mises à jour en temps réel.
import { ethers } from "ethers";
// Importe l'ABI du contrat. Le chemin peut varier selon la configuration de votre projet.
import { abi as compteurAbi } from '../artifacts/contracts/CompteurDeValeur.sol/CompteurDeValeur.json';
/**
* @returns Le fournisseur Ethereum injecté par MetaMask.
* @throws Erreur si aucun fournisseur Ethereum n'est détecté.
*/
function obtenirFournisseurEth() {
const eth = window.ethereum;
if (!eth) {
throw new Error("Aucun fournisseur Ethereum (tel que MetaMask) détecté.");
}
return eth;
}
/**
* @notice Demande à l'utilisateur l'autorisation de connecter son portefeuille MetaMask.
* @returns Vrai si l'accès est accordé, Faux sinon.
*/
async function demanderAccesPortefeuille(): Promise<boolean> {
const ethProvider = obtenirFournisseurEth();
try {
const comptes = await ethProvider.request({ method: "eth_requestAccounts" });
return comptes && comptes.length > 0;
} catch (error) {
console.error("Erreur lors de la demande d'accès au portefeuille:", error);
return false;
}
}
/**
* @notice Vérifie si des comptes sont déjà connectés à l'application.
* @returns Vrai si au moins un compte est connecté, Faux sinon.
*/
async function verifierComptesConnectes(): Promise<boolean> {
const ethProvider = obtenirFournisseurEth();
const comptes = await ethProvider.request({ method: "eth_accounts" });
return comptes.length > 0;
}
/**
* @notice Initialise l'application décentralisée, y compris la connexion au contrat et l'interface utilisateur.
* @throws Erreur si l'accès au portefeuille n'est pas accordé.
*/
async function initialiserApplicationDapp() {
// Vérifier et demander la connexion du portefeuille si nécessaire
if (!(await verifierComptesConnectes()) && !(await demanderAccesPortefeuille())) {
throw new Error("L'accès au portefeuille Ethereum est requis pour utiliser l'application.");
}
// Création d'un Provider Ethers.js à partir du fournisseur MetaMask
const provider = new ethers.BrowserProvider(obtenirFournisseurEth());
// Adresse du contrat déployé (À METTRE À JOUR avec l'adresse réelle de votre contrat)
const ADRESSE_CONTRAT_DEPLOOYEE = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
// Obtention du Signer, nécessaire pour envoyer des transactions
const signer = await provider.getSigner();
// Instanciation du contrat intelligent pour l'interaction frontend
const instanceContratFrontend = new ethers.Contract(
ADRESSE_CONTRAT_DEPLOOYEE,
compteurAbi,
signer // Le Signer est utilisé pour les appels qui modifient l'état de la chaîne
);
// --- Configuration des éléments de l'interface utilisateur ---
const displayElement = document.createElement("p");
displayElement.id = "valeur-actuelle-compteur";
displayElement.style.fontSize = "2.5em";
displayElement.style.fontWeight = "bold";
displayElement.style.color = "#333";
displayElement.style.margin = "20px 0";
const incrementButton = document.createElement("button");
incrementButton.textContent = "Incrémenter le Compteur";
incrementButton.style.padding = "12px 25px";
incrementButton.style.fontSize = "1.1em";
incrementButton.style.backgroundColor = "#4CAF50";
incrementButton.style.color = "white";
incrementButton.style.border = "none";
incrementButton.style.borderRadius = "5px";
incrementButton.style.cursor = "pointer";
incrementButton.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
incrementButton.onmouseover = () => incrementButton.style.backgroundColor = "#45a049";
incrementButton.onmouseout = () => incrementButton.style.backgroundColor = "#4CAF50";
document.body.style.fontFamily = "Arial, sans-serif";
document.body.style.display = "flex";
document.body.style.flexDirection = "column";
document.body.style.alignItems = "center";
document.body.style.justifyContent = "center";
document.body.style.minHeight = "100vh";
document.body.style.backgroundColor = "#f4f7f6";
document.body.appendChild(displayElement);
document.body.appendChild(incrementButton);
/**
* @notice Met à jour l'affichage de la valeur du compteur en lisant le contrat.
*/
async function mettreAJourAffichageValeur() {
try {
const valeurActuelle = await instanceContratFrontend.lireValeurActuelle();
displayElement.textContent = `Compteur: ${valeurActuelle.toString()}`;
} catch (error) {
console.error("Erreur lors de la lecture de la valeur du contrat:", error);
displayElement.textContent = "Erreur de lecture du compteur.";
}
}
// Mise à jour initiale de l'affichage
await mettreAJourAffichageValeur();
// Gestion de l'événement clic du bouton d'incrémentation
incrementButton.onclick = async () => {
try {
incrementButton.disabled = true; // Désactiver le bouton pendant la transaction
displayElement.textContent = "Transaction en cours, veuillez patienter...";
const tx = await instanceContratFrontend.incrementerCompteur();
await tx.wait(); // Attendre que la transaction soit minée
console.log("Transaction d'incrémentation réussie:", tx.hash);
// La valeur de l'affichage sera mise à jour par l'écouteur d'événement
} catch (error) {
console.error("Erreur lors de l'incrémentation:", error);
displayElement.textContent = "Échec de l'incrémentation.";
} finally {
incrementButton.disabled = false; // Réactiver le bouton
}
};
// Écouter l'événement ValueUpdated pour des mises à jour en temps réel
instanceContratFrontend.on(instanceContratFrontend.filters.ValueUpdated(), (newValue: bigint) => {
console.log("Événement 'ValueUpdated' reçu. Nouvelle valeur:", newValue.toString());
displayElement.textContent = `Compteur: ${newValue.toString()}`;
});
console.log("Application DApp initialisée avec succès.");
}
// Lancer l'initialisation de l'application
initialiserApplicationDapp().catch(console.error);
</boolean></boolean>
Points clés :
window.ethereum: L'objet injecté par MetaMask qui fournit l'API Ethereum au navigateur.eth_requestAccounts: Méthode pour demander l'autorisation de l'utilisateur de connecter son portefeuille.ethers.BrowserProvider: Une abstraction d'Ethers.js qui s'interface avec le fournisseur Ethereum du navigateur.provider.getSigner(): Récupère un objet qui représente le compte actuellement connecté et qui peut signer des transactions.new ethers.Contract(): Crée une instance JavaScript du contrat intelligent, en utilisant son adresse, son ABI et un signataire (si des écritures sont nécessaires).instanceContratFrontend.on(): Permet à l'application front end d'écouter les événements émis par le contrat sur la blockchain.
Étape 6: Lancement de l'Application Frontend
pnpm dev
Cette commande démarre un serveur de développement web (souvent basé sur Webpack ou Vite), compile le code TypeScript et ouvre l'application dans votre navigateur.
Analyse des Journaux (Logs)
Journal 1: Déploiement du Contrat
Exemple de log après l'exécution de npx hardhat run scripts/deployer-compteur.ts --network localhost :
Déploiement du contrat: CompteurDeValeur
Adresse du contrat: 0x5fbdb2315678afecb367f032d93f642f64180aa3 <-- Adresse du contrat déployé
Hachage de transaction: 0x9fc31615c81250f54d3886efc4843629a70ea57395e662088c8c54fbf488c7a7 <-- Hachage de la transaction de déploiement
Expéditeur: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 <-- Adresse du compte Hardhat ayant déployé le contrat
Valeur envoyée: 0 ETH <-- Aucune valeur ETH n'a été envoyée lors du déploiement
Gaz consommé: 241290 sur 30000000 <-- Quantité de gaz utilisée pour le déploiement
Bloc #1: 0xb5d7999904289f18e36a0ffcf443220bcbbd0ecc64f7a521669ceca23395b9db <-- Bloc dans lequel la transaction a été incluse
Informations essentielles :
Adresse du contrat: L'adresse sur la blockchain à laquelle l'application frontend se connectera.Hachage de transaction: Identifiant unique de la transaction de déploiement.Expéditeur: Le compte de test Hardhat utilisé pour le déploiement.Gaz consommé: La quantité de gaz nécessaire pour stocker le bytecode du contrat sur la blockchain.
Journal 2: Appel de Fonction du Contrat
Exemple de log suite à un appel à incrementerCompteur() (par le script de déploiement ou le frontend) :
Appel de contrat: CompteurDeValeur#incrementerCompteur <-- Fonction du contrat appelée
Hachage de transaction: 0x98556283c837de2b302406131caf897a58e264b820731b1cd12e8ccc3b6ffdc4 <-- Hachage de la transaction d'appel
Expéditeur: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 <-- Adresse du compte ayant effectué l'appel
Destinataire: 0x5fbdb2315678afecb367f032d93f642f64180aa3 <-- Adresse du contrat ciblé
Valeur envoyée: 0 ETH <-- Aucune valeur ETH n'a été envoyée avec l'appel
Gaz consommé: 47383 sur 30000000 <-- Quantité de gaz utilisée pour l'exécution de la fonction
Bloc #2: 0xe804c8bb62b0a1d1316b26f779f9fb37dfb688c4d4d2d6e4f5f0ca796a6b5e04 <-- Bloc d'inclusion de la transaction
console.log:
Nouvelle valeur du compteur: 1 <-- Sortie du `console.log` du contrat Solidity
Informations essentielles :
Appel de contrat: Indique la fonction du contrat qui a été exécutée.Destinataire: L'adresse du contrat qui a reçu l'appel.Gaz consommé: Les frais de gaz pour l'exécution de la logique de la fonction (généralement moins que pour le déploiement).console.log: Les messages de débogage du contrat sont affichés ici.
Concepts Fondamentaux
L'utilisateur clique sur le bouton "Incrémenter"
│
▼
L'interface (Ethers.js) appelle instanceContratFrontend.incrementerCompteur()
│
▼
MetaMask affiche une fenêtre pour confirmer la transaction
│
▼
L'utilisateur valide la transaction dans MetaMask
│
▼
MetaMask signe la transaction avec la clé privée de l'utilisateur
│
▼
La transaction signée est diffusée sur le réseau Ethereum
│
▼
Un mineur/validateur inclut la transaction dans un bloc
│
▼
Le contrat intelligent exécute la fonction incrementerCompteur()
│
▼
La variable d'état `currentValue` du contrat est incrémentée
│
▼
L'événement `ValueUpdated` est émis par le contrat
│
▼
L'interface frontend écoute cet événement et met à jour l'affichage
Foire Aux Questions (FAQ)
Q1: Pourquoi le déploiement et l'exécution de fonctions consomment-ils des quantités de Gaz différentes ?
Le déploiement d'un contrat exige plus de gaz car il implique de stocker le bytecode complet du contrat sur la blockchain de manière permanente (coût initial de stockage: 241290 gaz). En revanche, l'appel d'une fonction de contrat (comme incrementerCompteur()) ne nécessite que l'exécution de la logique de cette fonction et la modification de quelques variables d'état, ce qui est généralement moins coûteux (par exemple, 47383 gaz).
Q2: L'adresse du contrat est-elle permanente ?
Oui, une fois qu'un contrat est déployé sur une blockchain spécifique, son adresse est fixe et permanente sur ce réseau. Elle est dérivée de l'adresse du déployeur et de son nonce de transaction. Cependant, si vous déployez le même contrat sur un réseau différent (par exemple, de la chaîne locale Hardhat à Ropsten), il obtiendra une nouvelle adresse unique.
Q3: Quel est le rôle de MetaMask ?
MetaMask est un portefeuille de cryptomonnaies et une passerelle vers les applications décentralisées. Il remplit plusieurs fonctions cruciales : il gère de manière sécurisée les clés privées de l'utilisateur, signe les transactions pour prouver leur origine, et fournit une interface pour se connecter et interagir avec différents réseaux Ethereum.
Q4: Quelle est la distinction entre une fonction 'view' et une fonction d'écriture ?
- Une fonction
view(oupure) est une fonction en lecture seule qui ne modifie pas l'état de la blockchain. Elle ne coûte pas de gaz et peut être exécutée localement par un nœud Ethereum. Exemple:lireValeurActuelle(). - Une fonction d'écriture (ou "état modifiable") modifie l'état de la blockchain. Elle doit être incluse dans une transaction, coûte du gaz et nécessite d'être minée par le réseau. Exemple:
incrementerCompteur().
Commandes de Développement Rapides
# Installer les dépendances du projet
yarn install # ou npm install / pnpm install
# Compiler les contrats Solidity
npx hardhat compile
# Démarrer la chaîne de développement Ethereum locale de Hardhat
npx hardhat node
# Déployer le contrat sur la chaîne locale Hardhat
npx hardhat run scripts/deployer-compteur.ts --network localhost
# Lancer le serveur de développement frontend
pnpm dev # ou npm run dev / yarn dev
# Exécuter les tests des contrats
npx hardhat test