Introduction
Le développement Web3 ouvre de nouvelles possibilités pour créer des applications décentralisées (DApps). Ce tutoriel complet vous guide dans la création de votre première DApp avec React et Ethers.js, en couvrant tous les aspects essentiels du développement blockchain. Si vous cherchez une expertise professionnelle, découvrez nos services de développement.
Prérequis
Avant de commencer, assurez-vous d'avoir les connaissances et outils suivants :
Requis pour ce tutoriel :
- ✓Connaissance de React et JavaScript/TypeScript
- ✓Compréhension de base de la blockchain Ethereum
- ✓Node.js et npm installés sur votre machine
- ✓Extension MetaMask installée dans votre navigateur
Configuration du Projet
Commençons par créer un nouveau projet React et installer toutes les dépendances nécessaires pour le développement Web3.
# Créer un nouveau projet React npx create-react-app my-dapp cd my-dapp # Installer les dépendances Web3 npm install ethers npm install @metamask/detect-provider # Installer les dépendances UI (optionnel) npm install @mui/material @emotion/react @emotion/styled
Connexion à MetaMask
La première étape consiste à établir une connexion avec le wallet MetaMask de l'utilisateur. Créons un hook React personnalisé pour gérer cette interaction.
Fonctionnalités du hook
- • Connexion automatique au wallet
- • Gestion des erreurs de connexion
- • État de connexion persistant
- • Accès au provider et signer
Avantages d'Ethers.js
- • API simple et intuitive
- • Support TypeScript natif
- • Documentation complète
- • Performance optimisée
// hooks/useWallet.js import { useState, useEffect } from 'react'; import { ethers } from 'ethers'; export const useWallet = () => { const [account, setAccount] = useState(null); const [provider, setProvider] = useState(null); const [signer, setSigner] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const connectWallet = async () => { if (typeof window.ethereum !== 'undefined') { try { setIsConnecting(true); // Demander l'accès au wallet await window.ethereum.request({ method: 'eth_requestAccounts' }); // Créer le provider et signer const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const address = await signer.getAddress(); setProvider(provider); setSigner(signer); setAccount(address); } catch (error) { console.error('Erreur de connexion:', error); alert('Erreur lors de la connexion au wallet'); } finally { setIsConnecting(false); } } else { alert('MetaMask n\'est pas installé! Veuillez l\'installer pour continuer.'); } }; // Vérifier si déjà connecté au chargement useEffect(() => { const checkConnection = async () => { if (typeof window.ethereum !== 'undefined') { const accounts = await window.ethereum.request({ method: 'eth_accounts' }); if (accounts.length > 0) { connectWallet(); } } }; checkConnection(); }, []); return { account, provider, signer, connectWallet, isConnecting }; };
Interaction avec un Smart Contract
Maintenant que nous pouvons nous connecter à MetaMask, créons un composant pour interagir avec un smart contract. Nous allons couvrir les opérations de lecture et d'écriture.
Concepts clés :
ABI (Application Binary Interface)
Interface qui définit comment interagir avec le smart contract.
Adresse du contrat
Identifiant unique du smart contract sur la blockchain.
Transactions vs Appels
Les lectures sont gratuites, les écritures coûtent du gaz.
// components/ContractInteraction.js import React, { useState, useEffect } from 'react'; import { ethers } from 'ethers'; const CONTRACT_ADDRESS = '0x...'; // Remplacez par l'adresse de votre contrat const CONTRACT_ABI = [ // Définition de l'ABI de votre contrat "function getValue() view returns (uint256)", "function setValue(uint256 _value) external", "function owner() view returns (address)", "event ValueChanged(uint256 indexed newValue, address indexed changedBy)" ]; export const ContractInteraction = ({ signer, account }) => { const [value, setValue] = useState(''); const [contractValue, setContractValue] = useState(''); const [owner, setOwner] = useState(''); const [loading, setLoading] = useState(false); const [txHash, setTxHash] = useState(''); // Créer l'instance du contrat const contract = new ethers.Contract( CONTRACT_ADDRESS, CONTRACT_ABI, signer ); // Lire la valeur actuelle du contrat const readValue = async () => { try { setLoading(true); const result = await contract.getValue(); setContractValue(result.toString()); const contractOwner = await contract.owner(); setOwner(contractOwner); } catch (error) { console.error('Erreur lors de la lecture:', error); alert('Erreur lors de la lecture du contrat'); } finally { setLoading(false); } }; // Écrire une nouvelle valeur dans le contrat const writeValue = async () => { if (!value || isNaN(value)) { alert('Veuillez entrer une valeur numérique valide'); return; } try { setLoading(true); setTxHash(''); // Envoyer la transaction const tx = await contract.setValue(value); setTxHash(tx.hash); // Attendre la confirmation const receipt = await tx.wait(); if (receipt.status === 1) { alert('Transaction confirmée !'); readValue(); // Relire la valeur mise à jour setValue(''); // Vider le champ } else { alert('Transaction échouée'); } } catch (error) { console.error('Erreur lors de l\'écriture:', error); if (error.code === 'ACTION_REJECTED') { alert('Transaction annulée par l\'utilisateur'); } else { alert('Erreur lors de l\'exécution de la transaction'); } } finally { setLoading(false); } }; // Effet pour charger les données initiales useEffect(() => { if (contract && signer) { readValue(); } }, [contract, signer]); return ( <div className="space-y-6"> <h3 className="text-2xl font-bold text-white">Interaction avec le Smart Contract</h3> {/* Informations du contrat */} <div className="bg-gray-800/50 rounded-lg p-4"> <h4 className="font-semibold text-white mb-2">Informations du contrat</h4> <p className="text-sm text-gray-300">Adresse: {CONTRACT_ADDRESS}</p> <p className="text-sm text-gray-300">Propriétaire: {owner}</p> <p className="text-sm text-gray-300">Valeur actuelle: {contractValue || 'Non chargée'}</p> </div> {/* Lecture de données */} <div className="bg-blue-900/20 rounded-lg p-4 border border-blue-500/30"> <h4 className="font-semibold text-white mb-3">Lire les données</h4> <button onClick={readValue} disabled={loading} className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg disabled:opacity-50" > {loading ? 'Chargement...' : 'Actualiser la valeur'} </button> </div> {/* Écriture de données */} <div className="bg-green-900/20 rounded-lg p-4 border border-green-500/30"> <h4 className="font-semibold text-white mb-3">Modifier les données</h4> <div className="flex gap-3"> <input type="number" value={value} onChange={(e) => setValue(e.target.value)} placeholder="Nouvelle valeur" className="flex-1 bg-gray-800 text-white px-3 py-2 rounded-lg border border-gray-600" /> <button onClick={writeValue} disabled={loading || !value} className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg disabled:opacity-50" > {loading ? 'En cours...' : 'Écrire la valeur'} </button> </div> {txHash && ( <p className="text-sm text-green-300 mt-2"> Transaction: {txHash} </p> )} </div> </div> ); };
Écoute des Événements
Les événements permettent à votre DApp de réagir en temps réel aux changements sur la blockchain. Implémentons l'écoute d'événements pour notre smart contract.
// Ajouter dans ContractInteraction.js useEffect(() => { if (contract && signer) { // Fonction pour gérer les événements ValueChanged const handleValueChanged = (newValue, changedBy, event) => { console.log('Événement ValueChanged détecté:', { newValue: newValue.toString(), changedBy, blockNumber: event.blockNumber, transactionHash: event.transactionHash }); // Mettre à jour l'état local setContractValue(newValue.toString()); // Afficher une notification if (changedBy.toLowerCase() !== account.toLowerCase()) { alert(`Valeur mise à jour par un autre utilisateur: ${newValue.toString()}`); } }; // Commencer à écouter l'événement contract.on('ValueChanged', handleValueChanged); // Fonction de nettoyage return () => { contract.off('ValueChanged', handleValueChanged); }; } }, [contract, signer, account]); // Également, vous pouvez récupérer les événements passés const getHistoricalEvents = async () => { try { const currentBlock = await provider.getBlockNumber(); const fromBlock = currentBlock - 1000; // 1000 blocs en arrière const events = await contract.queryFilter( 'ValueChanged', fromBlock, currentBlock ); console.log('Événements historiques:', events); } catch (error) { console.error('Erreur lors de la récupération des événements:', error); } };
Gestion des Réseaux
Une DApp robuste doit gérer différents réseaux Ethereum (mainnet, testnets, Layer 2). Implémentons un système de gestion des réseaux.
Réseaux supportés :
Production
- • Ethereum Mainnet
- • Polygon
- • Arbitrum
- • Optimism
Test
- • Goerli Testnet
- • Sepolia Testnet
- • Mumbai (Polygon)
- • Localhost
// utils/networks.js export const NETWORKS = { 1: { name: 'Ethereum Mainnet', currency: 'ETH', rpcUrl: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID', blockExplorer: 'https://etherscan.io' }, 5: { name: 'Goerli Testnet', currency: 'ETH', rpcUrl: 'https://goerli.infura.io/v3/YOUR_PROJECT_ID', blockExplorer: 'https://goerli.etherscan.io' }, 137: { name: 'Polygon Mainnet', currency: 'MATIC', rpcUrl: 'https://polygon-rpc.com', blockExplorer: 'https://polygonscan.com' }, 80001: { name: 'Polygon Mumbai', currency: 'MATIC', rpcUrl: 'https://rpc-mumbai.maticvigil.com', blockExplorer: 'https://mumbai.polygonscan.com' } }; export const switchNetwork = async (chainId) => { try { await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: `0x${chainId.toString(16)}` }], }); return true; } catch (error) { if (error.code === 4902) { // Réseau non ajouté, proposer de l'ajouter return await addNetwork(chainId); } console.error('Erreur changement réseau:', error); return false; } }; const addNetwork = async (chainId) => { const network = NETWORKS[chainId]; if (!network) return false; try { await window.ethereum.request({ method: 'wallet_addEthereumChain', params: [{ chainId: `0x${chainId.toString(16)}`, chainName: network.name, nativeCurrency: { name: network.currency, symbol: network.currency, decimals: 18 }, rpcUrls: [network.rpcUrl], blockExplorerUrls: [network.blockExplorer] }] }); return true; } catch (error) { console.error('Erreur ajout réseau:', error); return false; } };
Application Complète
Assemblons tous les composants dans notre application principale pour créer une DApp fonctionnelle.
// App.js import React, { useState, useEffect } from 'react'; import { useWallet } from './hooks/useWallet'; import { ContractInteraction } from './components/ContractInteraction'; import { NETWORKS, switchNetwork } from './utils/networks'; function App() { const { account, provider, signer, connectWallet, isConnecting } = useWallet(); const [currentNetwork, setCurrentNetwork] = useState(null); const [balance, setBalance] = useState('0'); // Obtenir les informations du réseau actuel useEffect(() => { const getNetworkInfo = async () => { if (provider) { const network = await provider.getNetwork(); setCurrentNetwork(NETWORKS[Number(network.chainId)] || { name: 'Réseau inconnu', currency: 'ETH' }); // Obtenir le solde if (account) { const balance = await provider.getBalance(account); setBalance(ethers.formatEther(balance)); } } }; getNetworkInfo(); }, [provider, account]); // Gérer les changements de compte/réseau useEffect(() => { if (typeof window.ethereum !== 'undefined') { const handleAccountsChanged = (accounts) => { if (accounts.length === 0) { // Utilisateur déconnecté window.location.reload(); } else { // Compte changé window.location.reload(); } }; const handleChainChanged = (chainId) => { // Réseau changé window.location.reload(); }; window.ethereum.on('accountsChanged', handleAccountsChanged); window.ethereum.on('chainChanged', handleChainChanged); return () => { window.ethereum.removeListener('accountsChanged', handleAccountsChanged); window.ethereum.removeListener('chainChanged', handleChainChanged); }; } }, []); return ( <div className="min-h-screen bg-gray-900 text-white p-8"> <div className="max-w-4xl mx-auto"> <header className="mb-12 text-center"> <h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-cyan-400 to-purple-400 bg-clip-text text-transparent"> Ma Première DApp Web3 </h2> <p className="text-gray-400"> Application décentralisée construite avec React et Ethers.js </p> </header> {account ? ( <div className="space-y-8"> {/* Informations du wallet */} <div className="bg-gray-800 rounded-lg p-6"> <h2 className="text-2xl font-bold mb-4">Wallet connecté</h2> <div className="grid md:grid-cols-2 gap-4 text-sm"> <div> <p className="text-gray-400">Adresse</p> <p className="font-mono break-all">{account}</p> </div> <div> <p className="text-gray-400">Solde</p> <p>{parseFloat(balance).toFixed(4)} {currentNetwork?.currency}</p> </div> <div> <p className="text-gray-400">Réseau</p> <p>{currentNetwork?.name}</p> </div> <div> <p className="text-gray-400">Status</p> <p className="text-green-400">✓ Connecté</p> </div> </div> </div> {/* Interface d'interaction avec le contrat */} <ContractInteraction signer={signer} account={account} provider={provider} /> {/* Boutons de changement de réseau */} <div className="bg-gray-800 rounded-lg p-6"> <h3 className="text-xl font-bold mb-4">Changer de réseau</h3> <div className="grid grid-cols-2 md:grid-cols-4 gap-3"> {Object.entries(NETWORKS).map(([chainId, network]) => ( <button key={chainId} onClick={() => switchNetwork(Number(chainId))} className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg text-sm transition-colors" > {network.name} </button> ))} </div> </div> </div> ) : ( <div className="text-center py-16"> <div className="bg-gray-800 rounded-lg p-8 max-w-md mx-auto"> <h2 className="text-2xl font-bold mb-4">Connexion requise</h2> <p className="text-gray-400 mb-6"> Connectez votre wallet MetaMask pour commencer à utiliser la DApp </p> <button onClick={connectWallet} disabled={isConnecting} className="bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600 text-white font-bold py-3 px-6 rounded-lg transition-all duration-300 disabled:opacity-50" > {isConnecting ? 'Connexion...' : 'Connecter MetaMask'} </button> </div> </div> )} </div> </div> ); } export default App;
Déploiement
Une fois votre DApp développée et testée, vous pouvez la déployer sur différentes plateformes pour la rendre accessible au public.
Déploiement centralisé
- • Vercel (recommandé)
- • Netlify
- • GitHub Pages
- • AWS S3 + CloudFront
Déploiement décentralisé
- • IPFS + Pinata
- • Arweave
- • Fleek
- • ENS + IPFS
# Build de production npm run build # Déploiement sur Vercel npm install -g vercel vercel --prod # Déploiement sur IPFS avec Fleek npm install -g @fleek-platform/cli fleek site deploy # Déploiement manuel sur IPFS npm install -g ipfs-deploy ipd build/
Bonnes Pratiques de Sécurité
La sécurité est cruciale en Web3. Voici les meilleures pratiques à suivre pour protéger vos utilisateurs et votre application.
Points de sécurité essentiels :
- 🔒Toujours vérifier la présence et l'état de MetaMask
- 🔒Gérer les erreurs de transaction de façon appropriée
- 🔒Afficher les états de chargement pendant les transactions
- 🔒Valider rigoureusement toutes les entrées utilisateur
- 🔒Utiliser exclusivement des testnets pour le développement
- 🔒Implémenter une gestion robuste des changements de réseau
Conclusion
Félicitations ! Vous avez maintenant créé votre première DApp Web3 fonctionnelle. Cette base solide vous permettra d'explorer des fonctionnalités plus avancées comme les NFTs, les tokens ERC-20, les protocoles DeFi, ou même des interactions cross-chain.
Le développement Web3 évolue rapidement. Continuez à vous tenir informé des dernières technologies et bonnes pratiques pour créer des applications décentralisées toujours plus performantes et sécurisées.
Besoin d'aide pour votre projet Web3 ?
Notre équipe peut vous accompagner dans le développement de votre DApp et l'intégration blockchain. Découvrez également nos autres tutoriels : Smart Contracts Solidityet Performance React Native.
Contactez-nous