Maîtrisez le Développement Web3 : Créer des DApps avec Solidity et React
Maîtrisez le Développement Web3 : Créer des DApps avec Solidity et React

Intégration de l'Interface Utilisateur React avec les Smart Contracts

Bienvenue dans cette leçon du cours "Maîtrisez le Développement Web3 : Créer des DApps avec Solidity et React". Aujourd'hui, nous allons aborder un aspect crucial du développement de DApps : l'intégration harmonieuse entre votre interface utilisateur (UI) développée avec React et les smart contracts déployés sur la blockchain. C'est ici que la magie opère, transformant un simple contrat intelligent en une application décentralisée interactive et utilisable.

Introduction : Le Pont entre le Frontend et la Blockchain

Une DApp (Application Décentralisée) est bien plus qu'un simple smart contract. C'est une application complète qui offre une expérience utilisateur riche et intuitive, tout en tirant parti de la sécurité, de la transparence et de l'immuabilité de la blockchain. Pour que cette vision devienne réalité, votre interface utilisateur React doit être capable de "parler" à votre smart contract.

Dans cette leçon, nous allons explorer les mécanismes et les outils qui permettent à votre application React de :

  • Se connecter à un fournisseur de blockchain (comme Metamask).
  • Lire des données depuis un smart contract.
  • Envoyer des transactions pour modifier l'état du smart contract.
  • Réagir aux événements émis par le smart contract pour des mises à jour en temps réel.

Préparez-vous à transformer vos smart contracts dormants en applications dynamiques !

Prérequis

Pour tirer le meilleur parti de cette leçon, il est recommandé d'avoir :

  • Une connaissance de base de React (composants, hooks, gestion d'état).
  • Une compréhension fondamentale de ce qu'est un smart contract et de son déploiement.
  • Node.js et npm/yarn installés sur votre machine.
  • L'extension de navigateur Metamask installée et configurée avec un réseau de test (ex: Sepolia).

Fondations de l'Intégration : Les Outils Essentiels

L'interaction entre votre frontend React et la blockchain ne se fait pas directement. Nous avons besoin d'intermédiaires, de bibliothèques qui facilitent cette communication.

Le Rôle des Bibliothèques Web3 : ethers.js et web3.js

Ces bibliothèques sont les traducteurs universels entre votre code JavaScript et le protocole Ethereum. Elles encapsulent les appels RPC (Remote Procedure Call) nécessaires pour interagir avec un nœud Ethereum.

  • web3.js: Historiquement la première bibliothèque majeure, elle offre une suite complète d'outils pour interagir avec Ethereum.
  • ethers.js: Plus moderne, plus légère, souvent préférée aujourd'hui pour les projets React en raison de son approche plus idiomatique en JavaScript moderne, de sa meilleure gestion des erreurs et de sa prise en charge native de TypeScript. Nous nous concentrerons sur ethers.js dans cette leçon.

Ces bibliothèques permettent de :

  • Se connecter à un fournisseur de blockchain (ex: Metamask).
  • Accéder aux comptes des utilisateurs.
  • Interagir avec les smart contracts (lire des données, envoyer des transactions).
  • Écouter les événements de la blockchain.

Fournisseurs (Providers) et Signataires (Signers)

Comprendre ces deux concepts est fondamental pour interagir avec la blockchain.

  • Fournisseur (Provider) : C'est une connexion en lecture seule à un nœud Ethereum. Il permet de lire des informations de la blockchain (solde d'un compte, état d'un smart contract, blocs récents, etc.).
    • Exemples : JsonRpcProvider (pour se connecter à un nœud RPC public ou local comme Hardhat/Ganache), Web3Provider (pour se connecter à un injecteur de portefeuille comme Metamask).
  • Signataire (Signer) : C'est une entité qui peut signer des transactions et donc modifier l'état de la blockchain. Un Signer est toujours associé à un compte (une adresse avec une clé privée) et nécessite l'approbation de l'utilisateur pour envoyer une transaction.
    • Exemple : Le compte actuellement sélectionné dans Metamask est un Signer que ethers.js peut utiliser via un Web3Provider.

Pour lire des données (méthodes view ou pure de Solidity), vous n'avez besoin que d'un Provider. Pour envoyer des transactions (méthodes payable ou qui modifient l'état), vous avez besoin d'un Signer.

Adresses de Contrat et ABI

Pour qu'un Provider ou Signer puisse interagir avec un smart contract spécifique, il a besoin de deux informations cruciales :

  • Adresse du Contrat : L'adresse unique à laquelle votre smart contract a été déployé sur la blockchain. C'est son identifiant numérique.
  • ABI (Application Binary Interface) : C'est une description JSON de l'interface de votre smart contract. Elle indique à ethers.js (ou web3.js) quelles fonctions le contrat expose, quels arguments elles attendent, et quels types elles retournent. Sans l'ABI, ethers.js ne saurait pas comment formater les appels pour interagir avec les fonctions de votre contrat. L'ABI est générée automatiquement lors de la compilation de votre smart contract (par Hardhat, Truffle, Foundry, etc.).

Mise en Place de l'Environnement React

Commençons par créer un projet React et installer les dépendances nécessaires.

  1. Créer un projet React (si vous n'en avez pas déjà un) :

    npx create-react-app my-dapp
    cd my-dapp
    

    Ou avec Vite, qui est plus rapide :

    npm create vite@latest my-dapp -- --template react
    cd my-dapp
    npm install
    
  2. Installer ethers.js :

    npm install ethers
    # ou
    yarn add ethers
    

Maintenant que notre environnement est prêt, nous pouvons commencer l'intégration.

Connexion au Smart Contract

L'objectif est de permettre à l'utilisateur de connecter son portefeuille (ex: Metamask) et d'instancier notre smart contract dans notre application React.

Structure du Contrat Exemple (Solidity)

Imaginons un smart contract Solidity simple que nous voulons interagir avec. Ce contrat stocke un message et permet de le lire et de le modifier.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleStorage {
    string public message;

    event MessageChanged(string oldMessage, string newMessage, address indexed caller);

    constructor(string memory _initialMessage) {
        message = _initialMessage;
    }

    function getMessage() public view returns (string memory) {
        return message;
    }

    function setMessage(string memory _newMessage) public {
        string memory oldMsg = message;
        message = _newMessage;
        emit MessageChanged(oldMsg, _newMessage, msg.sender);
    }
}

Note : Après la compilation et le déploiement de ce contrat, vous obtiendrez son ABI (dans un fichier .json, souvent sous artifacts/contracts/SimpleStorage.json) et son adresse de déploiement.

Code React : Connexion et Instanciation

Nous allons modifier src/App.js pour gérer la connexion Metamask et l'instanciation du contrat.

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
// Importez l'ABI de votre contrat. L'ABI est un fichier JSON.
// Assurez-vous que ce chemin est correct pour votre projet (ex: depuis les artifacts de Hardhat)
import SimpleStorageABI from './contracts/SimpleStorage.json'; // Adaptez le chemin

function App() {
  const [provider, setProvider] = useState(null);
  const [signer, setSigner] = useState(null);
  const [contract, setContract] = useState(null);
  const [account, setAccount] = useState(null);
  const [contractMessage, setContractMessage] = useState('');
  const [newMessage, setNewMessage] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // Remplacez par l'adresse de votre contrat déployé
  const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // Exemple, CHANGEZ CETTE ADRESSE !

  useEffect(() => {
    // Initialiser la connexion Metamask au chargement de la page
    const initConnection = async () => {
      if (window.ethereum) {
        try {
          // Demande de connexion aux comptes
          const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
          setAccount(accounts[0]);

          // Création du Provider (lecture seule)
          const newProvider = new ethers.BrowserProvider(window.ethereum); // Utilisez BrowserProvider pour Metamask
          setProvider(newProvider);

          // Création du Signer (écriture)
          const newSigner = await newProvider.getSigner();
          setSigner(newSigner);

          // Instanciation du contrat
          const newContract = new ethers.Contract(contractAddress, SimpleStorageABI.abi, newSigner);
          setContract(newContract);

          console.log('Connecté à Metamask et contrat initialisé.');
        } catch (err) {
          console.error("Erreur de connexion à Metamask :", err);
          setError("Veuillez connecter votre portefeuille Metamask.");
        }
      } else {
        setError("Metamask ou un portefeuille compatible Ethereum n'est pas détecté.");
      }
    };

    initConnection();

    // Écouter les changements de compte ou de réseau dans Metamask
    if (window.ethereum) {
      window.ethereum.on('accountsChanged', (accounts) => {
        setAccount(accounts[0]);
        // Recharger le provider et le signer pour s'assurer qu'ils sont à jour
        window.location.reload(); 
      });

      window.ethereum.on('chainChanged', (chainId) => {
        // Recharger la page si le réseau change pour s'assurer de la bonne configuration
        window.location.reload();
      });
    }

    // Nettoyage des listeners lors du démontage du composant
    return () => {
      if (window.ethereum) {
        window.ethereum.removeListener('accountsChanged', () => {});
        window.ethereum.removeListener('chainChanged', () => {});
      }
    };
  }, []); // Le tableau vide signifie que cet effet s'exécute une seule fois au montage

  // Fonction pour lire le message du contrat
  const fetchMessage = async () => {
    if (contract) {
      setLoading(true);
      setError(null);
      try {
        const msg = await contract.getMessage();
        setContractMessage(msg);
      } catch (err) {
        console.error("Erreur lors de la lecture du message :", err);
        setError("Impossible de lire le message du contrat.");
      } finally {
        setLoading(false);
      }
    }
  };

  // Fonction pour modifier le message du contrat
  const updateMessage = async () => {
    if (contract && newMessage) {
      setLoading(true);
      setError(null);
      try {
        // Envoi de la transaction
        const tx = await contract.setMessage(newMessage);
        console.log("Transaction envoyée :", tx.hash);
        alert(`Transaction envoyée ! Hash: ${tx.hash}`);

        // Attendre la confirmation de la transaction
        await tx.wait();
        console.log("Transaction confirmée !");
        alert("Transaction confirmée ! Le message a été mis à jour.");
        setNewMessage(''); // Vider le champ d'entrée
        fetchMessage(); // Recharger le message après la mise à jour
      } catch (err) {
        console.error("Erreur lors de la mise à jour du message :", err);
        setError("Erreur lors de l'envoi de la transaction. Vérifiez votre portefeuille.");
      } finally {
        setLoading(false);
      }
    }
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
      <h1>Intégration React & Smart Contract</h1>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {account ? (
        <>
          <p>Compte connecté : <strong>{account}</strong></p>
          {contract ? (
            <div>
              <h2>Interagir avec SimpleStorage</h2>
              <p>Message actuel du contrat : {loading ? 'Chargement...' : contractMessage}</p>
              <button onClick={fetchMessage} disabled={loading}>
                {loading ? 'Chargement...' : 'Rafraîchir Message'}
              </button>

              <div style={{ marginTop: '20px' }}>
                <h3>Modifier le message</h3>
                <input
                  type="text"
                  value={newMessage}
                  onChange={(e) => setNewMessage(e.target.value)}
                  placeholder="Nouveau message"
                  style={{ marginRight: '10px', padding: '8px' }}
                  disabled={loading}
                />
                <button onClick={updateMessage} disabled={loading || !newMessage}>
                  {loading ? 'Envoi...' : 'Envoyer Nouveau Message'}
                </button>
              </div>
            </div>
          ) : (
            <p>Initialisation du contrat en cours ou échec...</p>
          )}
        </>
      ) : (
        <p>Veuillez connecter votre portefeuille Metamask pour commencer.</p>
      )}
    </div>
  );
}

export default App;

Explications du Code :

  1. import { ethers } from 'ethers';: Importe la bibliothèque ethers.js.
  2. import SimpleStorageABI from './contracts/SimpleStorage.json';: Importe l'ABI de votre smart contract. L'ABI est essentielle pour qu'ethers.js sache comment interagir avec les fonctions de votre contrat. Assurez-vous que le chemin est correct.
  3. useState Hooks: Utilisés pour gérer l'état de l'application :
    • provider: L'instance du fournisseur ethers.
    • signer: L'instance du signataire ethers (pour les transactions).
    • contract: L'instance du smart contract ethers.
    • account: L'adresse du compte Metamask connecté.
    • contractMessage: Le message actuellement lu depuis le contrat.
    • newMessage: Le message que l'utilisateur veut écrire.
    • loading: Indique si une opération est en cours (pour désactiver les boutons).
    • error: Pour afficher les messages d'erreur à l'utilisateur.
  4. contractAddress: C'est l'adresse à laquelle votre contrat SimpleStorage est déployé. C'est une valeur à changer après votre déploiement sur un réseau de test.
  5. useEffect pour l'Initialisation:
    • Ce hook s'exécute une fois au montage du composant ([] en deuxième argument).
    • Il vérifie window.ethereum, qui est injecté par Metamask.
    • await window.ethereum.request({ method: 'eth_requestAccounts' }); : Demande à Metamask de connecter les comptes de l'utilisateur.
    • new ethers.BrowserProvider(window.ethereum) : Crée un Provider pour interagir avec Metamask.
    • await newProvider.getSigner() : Obtient le Signer (le compte Metamask actuellement sélectionné) à partir du Provider. C'est le Signer qui pourra autoriser les transactions.
    • new ethers.Contract(contractAddress, SimpleStorageABI.abi, newSigner) : C'est l'étape clé ! Elle crée une instance du contrat ethers.
      • Le premier argument est l'adresse du contrat.
      • Le deuxième est l'ABI du contrat.
      • Le troisième est le signer (ou provider si vous ne faites que des lectures). Pour notre application, nous passons le signer afin de pouvoir à la fois lire et écrire.
    • Gestion des événements Metamask (accountsChanged, chainChanged): Il est crucial d'écouter ces événements pour réagir aux changements de compte ou de réseau de l'utilisateur dans Metamask, souvent en rechargeant la page pour s'assurer que l'état de l'application React est synchronisé avec Metamask.
  6. fetchMessage():
    • Appelle contract.getMessage() pour lire l'état actuel du message. Comme c'est une fonction view ou pure en Solidity, elle n'entraîne pas de coût en gaz et ne nécessite pas de transaction. L'appel se fait directement via le provider (ou le signer qui contient un provider).
  7. updateMessage():
    • Appelle contract.setMessage(newMessage) pour envoyer une transaction qui modifiera l'état du contrat.
    • await tx.wait() : Très important ! Après l'envoi d'une transaction, il faut attendre sa confirmation sur la blockchain. tx.wait() renvoie une promesse qui se résout une fois que la transaction est incluse dans un bloc et a un certain nombre de confirmations. C'est seulement après cette attente que vous pouvez être sûr que la modification a eu lieu.

Interagir avec le Smart Contract : Lecture et Écriture

Maintenant que notre application est connectée au smart contract, explorons plus en détail les interactions de lecture et d'écriture.

Appels en Lecture (Read-only calls)

Les fonctions de votre smart contract marquées view ou pure en Solidity sont des appels de lecture. Elles ne modifient pas l'état de la blockchain et ne coûtent donc pas de gaz. Elles sont exécutées localement par le nœud auquel votre Provider est connecté.

  • Syntaxe avec ethers.js: contract.yourViewFunction()
  • Exemple : contract.getMessage() dans notre fetchMessage fonction.

Transactions (Write calls)

Les fonctions qui modifient l'état du smart contract (celles sans view ou pure) nécessitent l'envoi d'une transaction. Cela implique un coût en gaz et doit être signé par l'utilisateur via son portefeuille (Metamask).

  • Syntaxe avec ethers.js: contract.yourWriteFunction(arg1, arg2, ...)
  • Processus:
    1. Votre application React appelle la fonction du contrat via l'instance contract (qui est liée au Signer).
    2. Metamask intercepte l'appel et affiche une fenêtre de confirmation à l'utilisateur.
    3. Si l'utilisateur confirme, Metamask signe la transaction et l'envoie au réseau.
    4. Votre code React reçoit un objet TransactionResponse qui contient le hash de la transaction.
    5. Vous pouvez ensuite utiliser tx.wait() pour attendre que la transaction soit minée et confirmée sur la blockchain.
  • Exemple : contract.setMessage(newMessage) dans notre updateMessage fonction.

Gestion des Événements (Events)

Les événements sont un mécanisme puissant dans Solidity pour "logguer" des informations sur la blockchain. Ces logs sont immuables et peuvent être écoutés par votre application React pour des mises à jour en temps réel sans avoir à sonder constamment le smart contract.

Pourquoi utiliser les événements ?

  • Mises à jour en temps réel : Votre UI peut réagir immédiatement aux changements sur la blockchain.
  • Preuve auditable : Les événements sont stockés sur la blockchain, fournissant un historique des actions.
  • Efficacité : Plus performant que de sonder constamment l'état du contrat.

Écouter les Événements avec ethers.js

// ... dans le useEffect d'App.js, après l'instanciation du contrat
useEffect(() => {
  // ... code de connexion existant

  const initConnection = async () => {
    // ... code de connexion existant pour provider, signer, contract
    if (newContract) {
      // Écouter l'événement MessageChanged
      newContract.on("MessageChanged", (oldMessage, newMessage, caller) => {
        console.log(`Événement 'MessageChanged' reçu :`);
        console.log(`  Ancien message : ${oldMessage}`);
        console.log(`  Nouveau message : ${newMessage}`);
        console.log(`  Appelé par : ${caller}`);
        setContractMessage(newMessage); // Mettre à jour l'UI avec le nouveau message
        alert(`Le message du contrat a été mis à jour par ${caller} : "${newMessage}"`);
      });

      console.log('Écoute des événements MessageChanged activée.');
    }
  };

  initConnection();

  // Fonction de nettoyage des listeners pour éviter les fuites de mémoire
  return () => {
    if (contract) {
      contract.off("MessageChanged"); // Désactiver le listener lors du démontage
    }
    // ... nettoyage des listeners Metamask existants
  };
}, [contract]); // Dépend du contrat pour s'assurer qu'il est instancié

Explications :

  1. contract.on("MessageChanged", (oldMessage, newMessage, caller) => { ... });:
    • contract.on() est la méthode d'ethers.js pour écouter les événements.
    • Le premier argument est le nom de l'événement tel que défini dans Solidity (MessageChanged).
    • Le deuxième argument est une fonction de rappel qui sera exécutée chaque fois que l'événement est émis. Les arguments de cette fonction correspondent aux arguments de l'événement Solidity, dans le même ordre.
    • Dans notre exemple, lorsque MessageChanged est émis, nous mettons à jour l'état contractMessage pour refléter le nouveau message et affichons une alerte à l'utilisateur.
  2. Nettoyage du Listener (contract.off("MessageChanged")): Il est crucial de désactiver les listeners d'événements lorsque le composant est démonté pour éviter les fuites de mémoire. Cela se fait dans la fonction de retour du useEffect.

Considérations Avancées

  • Gestion des États de Chargement et des Erreurs : Comme montré dans l'exemple, l'utilisation de useState pour loading et error est fondamentale pour une expérience utilisateur robuste. Informez l'utilisateur de l'état des opérations et des problèmes rencontrés.
  • Pattern try-catch-finally : Entourez toujours vos appels blockchain asynchrones (await) avec des blocs try-catch pour gérer gracieusement les erreurs (par exemple, l'utilisateur annule la transaction, ou la transaction échoue).
  • Utilisation de Hooks Personnalisés : Pour des DApps plus complexes, vous pouvez abstraire la logique d'interaction avec les smart contracts dans des hooks React personnalisés (ex: useContract, useContractRead, useContractWrite). Cela améliore la réutilisabilité et la lisibilité de votre code. Des bibliothèques comme wagmi ou thirdweb fournissent des hooks très puissants pour cela.
  • Context API ou Redux : Pour les DApps de grande envergure, la gestion de l'état global du Web3 (provider, signer, contract) peut bénéficier d'une solution de gestion d'état comme le Context API de React ou Redux, évitant le prop-drilling.

Conclusion

L'intégration de votre interface utilisateur React avec les smart contracts est le cœur même du développement de DApps. En maîtrisant l'utilisation de bibliothèques comme ethers.js, en comprenant le rôle des Providers et Signers, et en exploitant l'ABI de vos contrats, vous pouvez construire des applications décentralisées complètes et interactives.

Nous avons couvert les étapes essentielles :

  • La configuration de l'environnement React.
  • La connexion à un portefeuille comme Metamask.
  • L'instanciation de votre smart contract en JavaScript.
  • L'exécution d'appels en lecture (pas de gaz).
  • L'envoi de transactions en écriture (nécessite du gaz et confirmation utilisateur).
  • L'écoute des événements pour des mises à jour en temps réel.

Le développement Web3 est un domaine en constante évolution. N'hésitez pas à explorer davantage les bibliothèques et frameworks dédiés comme wagmi, thirdweb ou web3modal qui simplifient encore plus l'intégration et offrent des fonctionnalités avancées (gestion de plusieurs chaînes, connexion simplifiée de plusieurs portefeuilles, etc.).

Continuez à expérimenter, à déployer et à construire ! La prochaine étape logique est d'approfondir la gestion des erreurs, l'optimisation des performances et la sécurité de vos DApps.