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
useWriteContractet 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
RegistreFidelitedé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
- Déployer un smart contract sur le testnet Sepolia — obtenir l’adresse du contrat à brancher.
- Créer un token ERC-20 avec OpenZeppelin — afficher aussi un solde de jetons.
Pour aller plus loin
- 🔝 Retour au guide principal : Développer des smart contracts sur Ethereum
- Documentation officielle wagmi : wagmi.sh/react/getting-started
- Documentation officielle viem : viem.sh
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.