Créer un portefeuille crypto avec Ethers.js : Guide complet 2025 (React + MetaMask)

Tutoriel complet pour créer un portefeuille crypto Ethereum avec Ethers.js v6 et React en 2025. Connexion MetaMask, affichage solde, envoi transactions, interaction smart contracts.

En 2025, créer un portefeuille crypto pour interagir avec Ethereum est devenu essentiel pour tout développeur Web3. Avec Ethers.js v6 (successeur moderne de l'ancien Web3.js), tu peux facilement créer des dApps (applications décentralisées) connectées à la blockchain.

⚠️ Note importante : Web3.js a été officiellement "sunset" (arrêté) le 4 mars 2025. La bibliothèque recommandée en 2025 est Ethers.js, plus moderne, légère et activement maintenue.


Pourquoi Ethers.js en 2025 ?

Ethers.js s'est imposé comme LA bibliothèque de référence pour le développement Ethereum grâce à :

  • TypeScript natif : typage complet et auto-complétion
  • Plus léger : 88 KB vs 400 KB pour Web3.js
  • API moderne : async/await, Promises
  • Documentation excellente : docs.ethers.org
  • Activement maintenue : mises à jour régulières, sécurité optimale
  • Wallet intégré : gestion sécurisée des clés privées

Ce que tu vas apprendre

Dans ce tutoriel complet, tu vas créer un portefeuille crypto React capable de :

  • 🔗 Se connecter à MetaMask
  • 💰 Afficher le solde ETH de l'utilisateur
  • 💸 Envoyer des transactions Ethereum
  • 📜 Lire des données depuis un smart contract
  • ✍️ Écrire des données dans un smart contract (exemple ERC-20)
  • 🔄 Écouter les événements blockchain en temps réel

Prêt à devenir développeur Web3 ? Let's go ! 🚀


Installation et configuration

Créer le projet React

# Créer une nouvelle app React avec Vite (rapide)
npm create vite@latest crypto-wallet -- --template react-ts
cd crypto-wallet

# Installer Ethers.js v6
npm install ethers

# Installer TailwindCSS (optionnel, pour le style)
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Lancer le serveur de dev
npm run dev

Structure du projet

crypto-wallet/
├── src/
│   ├── components/
│   │   ├── ConnectWallet.tsx
│   │   ├── Balance.tsx
│   │   ├── SendTransaction.tsx
│   │   └── ContractInteraction.tsx
│   ├── hooks/
│   │   └── useWallet.ts
│   ├── utils/
│   │   └── ethereum.ts
│   ├── App.tsx
│   └── main.tsx
├── package.json
└── vite.config.ts

Étape 1 : Connexion à MetaMask

Hook personnalisé useWallet.ts

// src/hooks/useWallet.ts
import { useState, useEffect } from 'react';
import { BrowserProvider, Eip1193Provider } from 'ethers';

// Déclarer le type pour window.ethereum
declare global {
  interface Window {
    ethereum?: Eip1193Provider;
  }
}

export const useWallet = () => {
  const [account, setAccount] = useState(null);
  const [provider, setProvider] = useState(null);
  const [chainId, setChainId] = useState(null);
  const [isConnecting, setIsConnecting] = useState(false);
  const [error, setError] = useState(null);

  // Vérifier si MetaMask est installé
  const isMetaMaskInstalled = () => {
    return typeof window.ethereum !== 'undefined';
  };

  // Connecter MetaMask
  const connectWallet = async () => {
    if (!isMetaMaskInstalled()) {
      setError('MetaMask n\'est pas installé. Installez-le depuis metamask.io');
      return;
    }

    setIsConnecting(true);
    setError(null);

    try {
      // Créer le provider Ethers.js depuis window.ethereum
      const ethersProvider = new BrowserProvider(window.ethereum!);

      // Demander l'autorisation de connexion
      const accounts = await ethersProvider.send('eth_requestAccounts', []);
      
      if (accounts.length > 0) {
        setAccount(accounts[0]);
        setProvider(ethersProvider);

        // Récupérer le chainId
        const network = await ethersProvider.getNetwork();
        setChainId(Number(network.chainId));
      }
    } catch (err: any) {
      setError(err.message || 'Erreur lors de la connexion');
      console.error(err);
    } finally {
      setIsConnecting(false);
    }
  };

  // Déconnecter
  const disconnectWallet = () => {
    setAccount(null);
    setProvider(null);
    setChainId(null);
  };

  // Écouter les changements de compte et de réseau
  useEffect(() => {
    if (!window.ethereum) return;

    const handleAccountsChanged = (accounts: string[]) => {
      if (accounts.length === 0) {
        disconnectWallet();
      } else {
        setAccount(accounts[0]);
      }
    };

    const handleChainChanged = (chainId: string) => {
      setChainId(parseInt(chainId, 16));
      // Recharger la page pour éviter les problèmes de réseau
      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 {
    account,
    provider,
    chainId,
    isConnecting,
    error,
    isMetaMaskInstalled: isMetaMaskInstalled(),
    connectWallet,
    disconnectWallet,
  };
};

Composant ConnectWallet.tsx

// src/components/ConnectWallet.tsx
import { useWallet } from '../hooks/useWallet';

export const ConnectWallet = () => {
  const { account, chainId, isConnecting, error, isMetaMaskInstalled, connectWallet, disconnectWallet } = useWallet();

  // Noms des réseaux
  const getNetworkName = (chainId: number) => {
    const networks: Record = {
      1: 'Ethereum Mainnet',
      5: 'Goerli Testnet',
      11155111: 'Sepolia Testnet',
      137: 'Polygon Mainnet',
      80001: 'Polygon Mumbai',
    };
    return networks[chainId] || `Chain ID: ${chainId}`;
  };

  if (!isMetaMaskInstalled) {
    return (
      

MetaMask n'est pas installé.

Installer MetaMask
); } if (!account) { return (
{error &&

{error}

}
); } return (

Connecté : {account.slice(0, 6)}...{account.slice(-4)}

Réseau : {chainId && getNetworkName(chainId)}

); };

Étape 2 : Afficher le solde ETH

// src/components/Balance.tsx
import { useState, useEffect } from 'react';
import { BrowserProvider, formatEther } from 'ethers';
import { useWallet } from '../hooks/useWallet';

export const Balance = () => {
  const { account, provider } = useWallet();
  const [balance, setBalance] = useState('0');
  const [loading, setLoading] = useState(false);

  const fetchBalance = async () => {
    if (!provider || !account) return;

    setLoading(true);
    try {
      const balanceWei = await provider.getBalance(account);
      // Convertir de Wei en ETH (1 ETH = 10^18 Wei)
      const balanceEth = formatEther(balanceWei);
      setBalance(parseFloat(balanceEth).toFixed(4));
    } catch (err) {
      console.error('Erreur lors de la récupération du solde:', err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchBalance();
  }, [account, provider]);

  if (!account) return null;

  return (
    

Solde

{loading ? (

Chargement...

) : (

{balance} ETH

)}
); };

Étape 3 : Envoyer une transaction ETH

// src/components/SendTransaction.tsx
import { useState } from 'react';
import { parseEther } from 'ethers';
import { useWallet } from '../hooks/useWallet';

export const SendTransaction = () => {
  const { provider, account } = useWallet();
  const [to, setTo] = useState('');
  const [amount, setAmount] = useState('');
  const [txHash, setTxHash] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const sendTransaction = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!provider || !account) return;

    setLoading(true);
    setError('');
    setTxHash('');

    try {
      // Obtenir le signer (compte connecté)
      const signer = await provider.getSigner();

      // Créer la transaction
      const tx = await signer.sendTransaction({
        to: to,
        value: parseEther(amount), // Convertir ETH en Wei
      });

      setTxHash(tx.hash);

      // Attendre la confirmation (optionnel)
      await tx.wait();
      alert('Transaction confirmée ! 🎉');

      // Réinitialiser le formulaire
      setTo('');
      setAmount('');
    } catch (err: any) {
      setError(err.message || 'Erreur lors de l\'envoi');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  if (!account) return null;

  return (
    

Envoyer ETH

setTo(e.target.value)} placeholder="0x..." className="shadow appearance-none border rounded w-full py-2 px-3" required />
setAmount(e.target.value)} placeholder="0.01" className="shadow appearance-none border rounded w-full py-2 px-3" required />
{error &&

{error}

} {txHash && (

Transaction envoyée !

Voir sur Etherscan
)}
); };

Étape 4 : Interaction avec un Smart Contract

Exemple : Lire un solde ERC-20 (USDC)

// src/components/ContractInteraction.tsx
import { useState } from 'react';
import { Contract, formatUnits } from 'ethers';
import { useWallet } from '../hooks/useWallet';

// ABI minimal ERC-20 (seulement les fonctions utilisées)
const ERC20_ABI = [
  'function balanceOf(address owner) view returns (uint256)',
  'function decimals() view returns (uint8)',
  'function symbol() view returns (string)',
];

export const ContractInteraction = () => {
  const { provider, account } = useWallet();
  const [tokenAddress, setTokenAddress] = useState('');
  const [balance, setBalance] = useState('');
  const [symbol, setSymbol] = useState('');
  const [loading, setLoading] = useState(false);

  // USDC sur Ethereum Mainnet
  const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';

  const getTokenBalance = async () => {
    if (!provider || !account) return;

    setLoading(true);
    try {
      // Créer une instance du contrat
      const contract = new Contract(
        tokenAddress || USDC_ADDRESS,
        ERC20_ABI,
        provider
      );

      // Lire les données du contrat
      const [balanceRaw, decimals, tokenSymbol] = await Promise.all([
        contract.balanceOf(account),
        contract.decimals(),
        contract.symbol(),
      ]);

      // Formater le solde selon les decimals
      const formattedBalance = formatUnits(balanceRaw, decimals);

      setBalance(parseFloat(formattedBalance).toFixed(4));
      setSymbol(tokenSymbol);
    } catch (err) {
      console.error('Erreur:', err);
      alert('Erreur lors de la lecture du contrat');
    } finally {
      setLoading(false);
    }
  };

  if (!account) return null;

  return (
    

Interaction Smart Contract

setTokenAddress(e.target.value)} placeholder="0x... (USDC par défaut)" className="shadow appearance-none border rounded w-full py-2 px-3" />
{balance && (

{balance} {symbol}

)}
); };

App.tsx - Assemblage final

// src/App.tsx
import { ConnectWallet } from './components/ConnectWallet';
import { Balance } from './components/Balance';
import { SendTransaction } from './components/SendTransaction';
import { ContractInteraction } from './components/ContractInteraction';

function App() {
  return (
    

🪙 Portefeuille Crypto Ethereum

); } export default App;

Fonctionnalités avancées

1. Écouter les événements en temps réel

// Écouter les transferts ETH entrants
const listenToTransfers = () => {
  if (!provider || !account) return;

  provider.on('block', async (blockNumber) => {
    console.log('Nouveau bloc:', blockNumber);
    // Actualiser le solde
  });

  // Nettoyer à la déconnexion
  return () => provider.removeAllListeners();
};

2. Signer un message (authentification)

const signMessage = async () => {
  if (!provider) return;

  const signer = await provider.getSigner();
  const message = 'Je me connecte à cette dApp';
  const signature = await signer.signMessage(message);

  console.log('Signature:', signature);
  // Envoyer au backend pour vérification
};

3. Estimer les frais de gas

const estimateGas = async () => {
  const gasEstimate = await provider.estimateGas({
    to: '0x...',
    value: parseEther('0.1'),
  });

  const gasPrice = await provider.getFeeData();
  const totalCost = gasEstimate * gasPrice.gasPrice!;

  console.log('Coût estimé:', formatEther(totalCost), 'ETH');
};

Sécurité : Best Practices 2025

  • Ne jamais stocker les clés privées côté client (MetaMask s'en charge)
  • Valider toutes les entrées utilisateur (adresses, montants)
  • Utiliser HTTPS en production
  • Vérifier le chainId avant chaque transaction
  • Limiter les permissions : demander uniquement ce dont tu as besoin
  • Tester sur testnet (Sepolia, Goerli) avant mainnet
  • Afficher les frais de gas avant confirmation
  • Gérer les erreurs : timeout, rejection utilisateur, gas insuffisant

Alternatives à Ethers.js

BibliothèqueAvantagesInconvénientsCas d'usage
Ethers.js v6Léger, TypeScript, actif-✅ Recommandé 2025
ViemTrès performant, moderneMoins matureApps haute performance
WagmiHooks React optimisésSpécifique ReactApps React/Next.js
Web3.js-❌ Sunset (arrêté 2025)À éviter

Ressources pour aller plus loin


Déploiement

Vercel (recommandé)

# Installer Vercel CLI
npm i -g vercel

# Build
npm run build

# Déployer
vercel --prod

Configuration environnement

# .env.local
VITE_INFURA_API_KEY=your_key_here
VITE_NETWORK=sepolia

Conclusion

Félicitations ! 🎉 Tu viens de créer un portefeuille crypto complet avec Ethers.js v6 et React.

Ce que tu as appris :

  • ✅ Connexion MetaMask avec Ethers.js v6
  • ✅ Affichage du solde ETH
  • ✅ Envoi de transactions
  • ✅ Interaction avec smart contracts (ERC-20)
  • ✅ Best practices sécurité Web3
  • ✅ Gestion des événements blockchain

Prochaines étapes :

  • 📝 Ajouter la signature de messages (authentification)
  • 🪙 Supporter plusieurs tokens ERC-20
  • 🖼️ Afficher les NFTs (ERC-721)
  • 🔄 Implémenter un swap DEX (Uniswap)
  • 💾 Sauvegarder l'historique des transactions

Le développement Web3 est un domaine en pleine expansion. Avec Ethers.js, tu as maintenant les bases solides pour créer des dApps innovantes. Continue à pratiquer, expérimente sur testnet, et rejoins la révolution décentralisée ! 🚀




⚠️ AVERTISSEMENT - Disclaimer légal

Cet article est fourni à titre informatif et éducatif uniquement. Il ne constitue en aucun cas un conseil en investissement financier, fiscal ou juridique. Les cryptomonnaies comportent des risques importants, incluant la perte totale du capital investi. Ne stockez jamais de clés privées dans le code frontend. Testez toujours sur des testnets avant de déployer en production. L'auteur et CODAURA déclinent toute responsabilité quant aux pertes financières ou dommages directs ou indirects résultant de l'utilisation des informations contenues dans cet article. Vous êtes seul responsable de vos décisions d'investissement et de développement.