ITSkillsCenter
Développement Web

Connecter un front-end React à un smart contract avec viem et wagmi

13 min de lecture
Guide principal : Développer des smart contracts sur Ethereum : Solidity, outils et web3.

Un contrat déployé ne sert à grand-chose si seuls les initiés savent l’appeler en ligne de commande. La dernière marche consiste à lui donner une interface web : un bouton pour connecter son portefeuille, un solde qui s’affiche, une action qui déclenche une transaction. Côté navigateur, deux bibliothèques se sont imposées : viem, qui parle à la chaîne, et wagmi, qui enveloppe viem dans des hooks React confortables. Nous les utilisons ici pour brancher une application React sur le registre de fidélité.

À la fin, vous aurez une petite application qui se connecte à un portefeuille, lit le solde de points du compte connecté depuis le contrat, et envoie une transaction de crédit — le tout sans jamais manipuler de clé privée dans votre code, puisque c’est le portefeuille de l’utilisateur qui signe.

🎯 Ce que vous allez apprendre

  • Mettre en place wagmi version 2 avec viem et TanStack Query dans un projet React.
  • Connecter et déconnecter un portefeuille avec le connecteur injecté.
  • Lire l’état d’un contrat avec useReadContract.
  • Envoyer une transaction avec useWriteContract et gérer l’état de chargement.
  • Définir une ABI minimale pour les seules fonctions utilisées.

🛠️ Ce que vous allez construire

Une interface React qui affiche un bouton de connexion ; une fois connectée, elle montre l’adresse du compte, son solde de points lu en direct depuis le contrat, et un bouton qui déclenche un crédit via une transaction signée par le portefeuille.

Prérequis

  • Node.js installé (version active LTS recommandée) et des bases de React.
  • Un contrat RegistreFidelite déjà déployé sur Sepolia (voir le tutoriel de déploiement) et son adresse.
  • Une extension de portefeuille dans le navigateur, configurée sur le réseau Sepolia.
  • ⏱️ Temps estimé : 60 à 90 minutes.

Étape 1 — Créer le projet React

On part d’un squelette Vite en TypeScript, léger et rapide. Dans un terminal :

npm create vite@latest mon-dapp -- --template react-ts
cd mon-dapp
npm install

Vite génère une application React minimale. Vérifiez qu’elle démarre avec npm run dev et ouvrez l’adresse locale affichée : une page d’accueil Vite doit apparaître. C’est notre base de travail.

Étape 2 — Installer wagmi, viem et TanStack Query

wagmi s’appuie sur viem pour la communication avec la chaîne et sur TanStack Query pour la gestion du cache et des états de requête. Les trois s’installent ensemble :

npm install wagmi viem @tanstack/react-query

Ces dépendances ajoutées, nous pouvons configurer la connexion à la chaîne. wagmi a besoin de savoir quels réseaux il cible et comment les joindre.

Étape 3 — Configurer wagmi

On centralise la configuration dans un fichier. On déclare Sepolia comme seul réseau, on ajoute le connecteur « injecté » (celui qui détecte l’extension de portefeuille du navigateur) et on définit le transport HTTP. Créez src/config.ts :

import { http, createConfig } from 'wagmi'
import { sepolia } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'

export const config = createConfig({
  chains: [sepolia],
  connectors: [injected()],
  transports: {
    [sepolia.id]: http(),
  },
})

L’appel http() sans argument utilise un point d’accès public par défaut pour Sepolia ; vous pouvez y passer votre URL RPC personnelle pour plus de fiabilité. Le connecteur injected() couvre les extensions de portefeuille standard. Cette configuration est l’unique source de vérité sur les réseaux supportés par l’application.

Étape 4 — Brancher les fournisseurs de contexte

Pour que les hooks wagmi fonctionnent dans toute l’application, on enveloppe l’arbre React dans deux fournisseurs : celui de wagmi, et celui de TanStack Query. Modifiez src/main.tsx :

import React from 'react'
import ReactDOM from 'react-dom/client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from './config'
import App from './App'

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')!).render(
  <WagmiProvider config={config}>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </WagmiProvider>
)

L’ordre compte : WagmiProvider à l’extérieur, QueryClientProvider à l’intérieur. Sans ces fournisseurs, tout hook wagmi lèverait une erreur indiquant qu’il est utilisé hors contexte. L’application est maintenant prête à dialoguer avec la chaîne.

Étape 5 — Déclarer l’ABI minimale

Pour appeler un contrat, le front-end a besoin de son ABI : la description des fonctions. On ne déclare que celles qu’on utilise, ce qui garde le fichier lisible. Créez src/abi.ts :

export const abiRegistre = [
  {
    type: 'function',
    name: 'soldeDe',
    stateMutability: 'view',
    inputs: [{ name: 'client', type: 'address' }],
    outputs: [{ name: '', type: 'uint256' }],
  },
  {
    type: 'function',
    name: 'crediter',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'client', type: 'address' },
      { name: 'montant', type: 'uint256' },
    ],
    outputs: [],
  },
] as const

Le as const final est important : il fige les types littéraux, ce qui permet à wagmi et TypeScript de vérifier vos appels (noms de fonctions, types d’arguments) au moment de la compilation. Une faute de frappe sur functionName deviendra une erreur de type, pas un bug à l’exécution.

Étape 6 — Connecter le portefeuille, lire et écrire

Place au composant principal. Il gère trois moments : non connecté (on propose la connexion), connecté (on affiche l’adresse et le solde), et l’action de crédit. Remplacez src/App.tsx par ce contenu, en insérant l’adresse de votre contrat :

import {
  useAccount, useConnect, useDisconnect,
  useReadContract, useWriteContract,
} from 'wagmi'
import { abiRegistre } from './abi'

const ADRESSE_CONTRAT = '0xVotreContrat' as const

function App() {
  const { address } = useAccount()
  const { connect, connectors } = useConnect()
  const { disconnect } = useDisconnect()
  const { writeContract, isPending } = useWriteContract()

  const { data: solde, refetch } = useReadContract({
    address: ADRESSE_CONTRAT,
    abi: abiRegistre,
    functionName: 'soldeDe',
    args: address ? [address] : undefined,
    query: { enabled: Boolean(address) },
  })

  if (!address) {
    return (
      <button onClick={() => connect({ connector: connectors[0] })}>
        Connecter le portefeuille
      </button>
    )
  }

  return (
    <div>
      <p>Compte : {address}</p>
      <p>Votre solde : {solde?.toString() ?? '...'} points</p>
      <button
        disabled={isPending}
        onClick={() =>
          writeContract({
            address: ADRESSE_CONTRAT,
            abi: abiRegistre,
            functionName: 'crediter',
            args: [address, 10n],
          })
        }
      >
        {isPending ? 'En cours...' : 'Crediter 10 points'}
      </button>
      <button onClick={() => disconnect()}>Deconnecter</button>
    </div>
  )
}

export default App

Décortiquons les hooks. useAccount donne l’adresse connectée et l’état de connexion. useConnect fournit la fonction de connexion et la liste des connecteurs (ici, l’injecté). useReadContract lit soldeDe : on lui passe l’adresse du contrat, l’ABI, le nom de la fonction et les arguments ; l’option query.enabled et l’args conditionnel empêchent l’appel tant qu’aucune adresse n’est disponible. L’arrêt anticipé sur !address garantit aussi que, dans le rendu connecté, TypeScript considère l’adresse comme définie — ce qui rend les appels suivants correctement typés sans assertion. useWriteContract renvoie writeContract, qui ouvre le portefeuille pour signer la transaction, et isPending pour afficher un état de chargement. Le suffixe 10n est un entier bigint, type approprié pour les nombres de la chaîne. Le suffixe as const sur l’adresse fixe son type littéral, ce qui satisfait le typage strict attendu par wagmi pour une adresse (sinon une simple chaîne déclenche une erreur TypeScript).

Point d’étape — Lancez npm run dev. Le bouton de connexion doit ouvrir votre portefeuille ; une fois connecté sur Sepolia, votre adresse et votre solde de points s’affichent. Si le solde reste à zéro, c’est normal tant que le compte n’a pas été crédité.

Étape 7 — Rafraîchir après une transaction

Après un crédit réussi, le solde affiché ne se met pas à jour tout seul : il a été lu avant la transaction. wagmi expose refetch (récupéré plus haut) pour relire la valeur. Le plus propre est d’attendre la confirmation de la transaction avant de relire, grâce au hook useWaitForTransactionReceipt couplé au hash renvoyé par l’écriture. Pour une première version, on peut déclencher refetch dans le rappel de succès de writeContract :

writeContract(
  {
    address: ADRESSE_CONTRAT,
    abi: abiRegistre,
    functionName: 'crediter',
    args: [address, 10n],
  },
  { onSuccess: () => refetch() }
)

Notez toutefois que onSuccess se déclenche quand la transaction est envoyée, pas forcément confirmée : pour un solde parfaitement à jour, attendez le reçu avec useWaitForTransactionReceipt puis appelez refetch. C’est la différence entre « la transaction part » et « la transaction est inscrite » que nous avons vue côté chaîne.

Une remarque sur le contrôle d’accès

La fonction crediter est réservée au propriétaire du contrat. Si le portefeuille connecté n’est pas ce propriétaire, la transaction sera rejetée par le contrat — l’interface ne contourne aucune règle. C’est un point essentiel : les vérifications de sécurité vivent dans le contrat, jamais dans le front-end, qui peut toujours être modifié par l’utilisateur. Une interface honnête se contente de refléter ce que le contrat autorise. Pour tester le bouton de crédit avec succès, connectez le compte propriétaire ; sinon, l’application reste un excellent lecteur de soldes pour n’importe quel compte.

Le portefeuille, ce signataire que vous ne remplacez pas

Comprendre le rôle du portefeuille évite bien des confusions. Quand l’utilisateur connecte son extension, votre application n’obtient pas sa clé privée : elle obtient son adresse publique et un canal pour demander des signatures. Au moment d’une écriture, c’est l’extension qui affiche une fenêtre de confirmation, qui signe avec la clé restée chez l’utilisateur, et qui diffuse la transaction. Votre code ne fait que préparer la requête. Cette séparation est une garantie de sécurité fondamentale : même une application malveillante ne peut pas dépenser à la place de l’utilisateur sans son approbation explicite, transaction par transaction.

Techniquement, le connecteur injecté dialogue avec l’extension via une interface standardisée exposée par le navigateur. C’est pourquoi un même code fonctionne avec différentes extensions compatibles : elles parlent toutes le même protocole. wagmi masque ces détails derrière useConnect, mais savoir ce qui se passe dessous aide à diagnostiquer les cas où aucun connecteur n’apparaît — presque toujours parce qu’aucune extension n’est installée ou que la page n’a pas été rechargée après installation.

Pourquoi le front-end n’est jamais la source de vérité

Il est tentant, en venant du développement web classique, de placer des règles dans l’interface : masquer un bouton, bloquer une action. Sur une application décentralisée, ce réflexe est trompeur. Tout ce qui tourne dans le navigateur est modifiable par l’utilisateur : il peut éditer le code, appeler le contrat directement, ignorer votre interface. La seule barrière qui tienne est celle inscrite dans le contrat — les require, les onlyOwner, les vérifications de solde. Le front-end est une commodité d’usage, pas un gardien. Concevoir avec cette idée en tête change la manière de répartir la logique : la validation critique descend dans le contrat, l’interface se contente d’afficher, de guider et de prévenir.

Cela éclaire aussi la gestion des états dans wagmi. Chaque hook de lecture expose, au-delà des données, des indicateurs comme isLoading, isError et error ; chaque hook d’écriture expose isPending et le hash de transaction. Une bonne interface utilise ces signaux pour informer honnêtement l’utilisateur : « lecture en cours », « transaction envoyée, en attente de confirmation », « échec, voici pourquoi ». Afficher l’état réel d’une opération asynchrone qui se joue sur un réseau public n’est pas un détail cosmétique — c’est ce qui distingue une application déroutante d’une application dans laquelle l’utilisateur a confiance.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
useConfig must be used within WagmiProvider Fournisseurs absents ou mal ordonnés Envelopper l’app dans WagmiProvider puis QueryClientProvider
Le solde ne s’affiche jamais Mauvaise adresse de contrat ou réseau du portefeuille différent Vérifier l’adresse et basculer le portefeuille sur Sepolia
La transaction de crédit échoue Compte connecté non propriétaire Se connecter avec le compte propriétaire du contrat
Type error sur functionName ABI sans as const Ajouter as const à la fin de l’ABI
Aucun connecteur proposé Pas d’extension de portefeuille installée Installer une extension compatible et recharger la page

✅ Récapitulatif

Vous avez relié une application React à un contrat déployé : configuration wagmi avec viem et TanStack Query, fournisseurs de contexte, ABI typée, connexion de portefeuille, lecture du solde avec useReadContract et envoi d’une transaction avec useWriteContract. Vous savez aussi pourquoi le contrôle d’accès reste côté contrat et comment rafraîchir l’affichage après une écriture. C’est le chaînon qui transforme un contrat en produit utilisable.

🧾 Aide-mémoire

Élément Rôle
createConfig Déclarer réseaux, connecteurs et transports
WagmiProvider / QueryClientProvider Fournir le contexte aux hooks
useAccount Adresse et état de connexion
useConnect / useDisconnect Connexion / déconnexion du portefeuille
useReadContract Lecture d’une fonction view
useWriteContract Envoi d’une transaction
useWaitForTransactionReceipt Attendre la confirmation

💪 À vous de jouer

Ajoutez un champ de saisie pour choisir le montant à créditer plutôt que la valeur fixe de 10, et un indicateur visuel pendant l’attente de confirmation de la transaction. Indice : un état React local pour le montant, et useWaitForTransactionReceipt pour l’attente.

Voir une piste

Stockez le montant saisi dans un useState, convertissez-le en BigInt avant de le passer en argument, et désactivez le bouton tant que la valeur est vide ou nulle. Récupérez le hash renvoyé par writeContract, passez-le à useWaitForTransactionReceipt, et affichez « Confirmé » quand le reçu arrive avant d’appeler refetch.

Tutoriels frères

Pour aller plus loin

FAQ

Quelle différence entre viem et wagmi ?
viem est la bibliothèque bas niveau qui communique avec la chaîne ; wagmi l’enveloppe dans des hooks React (connexion, lecture, écriture) et gère le cache via TanStack Query. On utilise wagmi dans React, et viem directement pour des scripts ou du code hors React.

Faut-il une URL RPC dans le front-end ?
Pour de simples lectures, le transport http() par défaut suffit souvent. Mais un point d’accès public partagé peut être lent ou limité ; passer votre propre URL RPC à http() rend l’application plus fiable, surtout quand le trafic augmente. La clé reste côté configuration, jamais exposée comme un secret sensible puisqu’elle ne sert qu’à lire.

Le front-end manipule-t-il ma clé privée ?
Non. Le portefeuille de l’utilisateur signe les transactions ; l’application ne voit jamais la clé. C’est tout l’intérêt du connecteur injecté.

Pourquoi TanStack Query est-il requis ?
wagmi version 2 délègue la gestion du cache, des rechargements et des états de requête à TanStack Query. C’est pour cela qu’on enveloppe l’application dans son fournisseur en plus de celui de wagmi.

Partager
Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité