Vous avez un contrat Solidity qui compile, passe ses tests et vit sur une chaîne locale anvil qui disparaît dès que vous fermez le terminal. Personne d’autre ne peut l’appeler. Pour en faire une dApp, blockchain that runs exactly as written when it is called, holding and moving funds according to its code, without an intermediary able to change the outcome.">smart contract holding reserves of two or more tokens, supplied by users, that a decentralized exchange uses to execute trades automatically at a price set by a formula.">pool diverge, compared with simply holding the same tokens in a wallet. It becomes permanent when the provider withdraws.">il manque trois choses : une adresse sur un réseau public, un code source que chacun peut vérifier, et une page web avec un bouton sur lequel un inconnu peut cliquer. Cette leçon construit les trois et montre comment déployer un smart contract sur le testnet Sepolia, où une erreur ne coûte que quelques minutes.
Le projet est un livre d’or. Toute personne munie d’un wallet signe un court message, le contrat le range avec son auteur et l’heure, et la page affiche les dix dernières entrées. L’ensemble tient en une soixantaine de lignes de Solidity et de JavaScript. Le plus difficile n’est pas le code, mais la chaîne d’outils qui l’entoure : compiler, garder une clé en sécurité, choisir un point d’accès RPC, obtenir de l’ether de test, déployer, vérifier, puis brancher une page sur le contrat. Chacune de ces étapes échoue d’une manière typique, et nous nommons l’échec à chaque fois.
Quelques règles valent pour toute la leçon. L’ether de test n’a aucune valeur : on ne l’achète jamais, et quiconque en vend ou propose un « airdrop de testnet » pratique le genre d’arnaque décrit dans notre guide pour éviter les arnaques crypto. Vous utilisez un wallet neuf, créé uniquement pour les tests. Et la clé de ce wallet ne s’approche jamais de la phrase de récupération d’un wallet qui contient de vrais fonds, pour les raisons données dans notre leçon sur la sécurité de la seed phrase.
Créer sa première dApp en trois pièces : un contrat, une page et un wallet
Une application décentralisée est un programme dont la logique s’exécute sur une blockchain et dont l’interface s’exécute dans un navigateur. Si l’idée vous est encore neuve, notre explication de ce qu’est une dApp la détaille ; ici, nous en construisons une. Le contrat détient l’état et fait respecter les règles. La page lit cet état par l’intermédiaire d’un nœud et l’affiche. Le wallet signe les transactions qui modifient l’état, si bien que la page ne voit jamais une clé privée.
C’est la leçon 4 du parcours développeur. Elle suppose que vous avez suivi la leçon pratique sur Solidity (événements, erreurs personnalisées, msg.sender, tests avec Foundry) et que vous savez où chercher une réponse après notre tour d’horizon des ressources et certifications blockchain. Nous ne réexpliquons pas le langage. Il vous faut aussi un terminal, Git, une version à support long de Node.js avec npm, et un navigateur muni d’une extension de wallet comme MetaMask. Comptez un après-midi la première fois, dont l’essentiel passe en comptes, clés et faucets plutôt qu’en code.
Nous avons retenu les outils ci-dessous parce que chacun est le plus petit qui fasse le travail. Foundry compile, teste, déploie et vérifie depuis une seule ligne de commande. viem est une bibliothèque TypeScript qui parle à n’importe quel nœud Ethereum et à n’importe quel wallet, sans framework. Vite sert et assemble la page. Les versions sont celles en vigueur le 27 octobre 2026.
| Outil | Version au 27 octobre 2026 | Rôle dans cette leçon | Source consultée |
|---|---|---|---|
| Foundry (forge, cast, anvil) | v1.8.6, publiée le 21 octobre 2026 | Compiler, tester, déployer, vérifier | versions du dépôt GitHub foundry-rs |
| Compilateur Solidity | 0.8.38 (dernière version) | Compile le contrat | binaries.soliditylang.org |
| viem | 2.57.12 (publiée le 26 octobre 2026) | Lit et écrit depuis la page | registre npm, GitHub |
| Vite | 8.3.5 | Serveur de développement et build | registre npm |
| ethers | 6.17.0 | Alternative à viem | registre npm |
| wagmi | 3.7.9 | Couche React au-dessus de viem, pour plus tard | registre npm |
Pourquoi viem plutôt qu’ethers v6 ou wagmi ? ethers v6 sait tout faire dans cette leçon et beaucoup de tutoriels l’emploient : on enveloppe le wallet dans un BrowserProvider, on en tire un signataire et on construit un objet Contract. wagmi ajoute au-dessus de viem des hooks React, un cache et des connecteurs de wallets, ce qui rapporte dans une application plus grande mais masque les étapes que nous voulons vous montrer. viem avec du JavaScript pur laisse chaque requête visible, et ce que vous apprenez se transpose ensuite à wagmi.
Pourquoi Sepolia est le testnet des dApps en 2026
Ethereum compte plusieurs réseaux de test publics, et choisir le mauvais est la première erreur classique. La page des réseaux d’ethereum.org est nette sur ce point : « Sepolia is the recommended default testnet for application development », soit « Sepolia est le testnet par défaut recommandé pour développer des applications ». Les autres noms que vous croiserez dans de vieux tutoriels ont disparu ou s’adressent à d’autres. Goerli est signalé comme obsolète. Holesky, un grand réseau conçu pour les essais de staking, est lui aussi obsolète : la Fondation Ethereum a annoncé son arrêt le 1er septembre 2025 et a renvoyé les développeurs d’applications vers Sepolia. Hoodi, son remplaçant, est destiné aux validateurs et aux prestataires de staking, pas aux contrats ni aux interfaces.
Sepolia a pour identifiant de chaîne 11155111, écrit 0xaa36a7 en hexadécimal, la forme qu’emploient les wallets. Retenez les deux nombres : le décimal apparaît dans Etherscan et dans Foundry, l’hexadécimal dans les requêtes des wallets. Les explorateurs de blocs, les faucets, les fournisseurs de RPC et les deux services de vérification que nous utilisons la prennent en charge. Au 27 octobre 2026, la liste des chaînes d’Etherscan indique Sepolia comme disponible avec l’offre gratuite de l’API, alors que certains testnets de couche 2, comme Base Sepolia, exigent une offre payante pour l’accès à l’API. C’est une raison de commencer sur Sepolia même et de passer à une couche 2 plus tard, quand les notions de notre guide sur les couches 1 et 2 compteront pour votre projet.
Pourquoi ne pas rester sur anvil ? Une chaîne locale convient parfaitement aux tests, et vous devez continuer à l’utiliser. Mais personne d’autre ne peut l’atteindre, il faut pointer à la main l’extension de wallet vers elle, et elle ne vous apprend rien des problèmes du monde réel : latence, prix du gas, nonces, explorateurs. Pourquoi pas le mainnet ? Chaque transaction y coûte de l’ether réel et ne peut pas être annulée. Un testnet se comporte comme le mainnet sur tout ce qui compte pour apprendre, sauf le prix.
Mettre en place le projet Foundry et le contrat Guestbook
Installer Foundry et créer le projet
Foundry s’installe avec son propre outil de mise à jour, foundryup. Les deux commandes ci-dessous viennent de la page d’installation de Foundry ; ouvrez un nouveau terminal entre les deux, ou rechargez le profil de votre shell, pour que foundryup soit dans votre chemin. Sous Windows, la documentation demande Git Bash ou WSL plutôt que PowerShell.
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init guestbook
cd guestbook
rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol
forge init crée un fichier foundry.toml, les dossiers src, test et script, chacun avec un fichier d’exemple Counter, et installe la bibliothèque forge-std dans lib. Nous supprimons les trois fichiers Counter pour les remplacer par les nôtres. L’arborescence finale du projet ressemble à ceci :
guestbook/
foundry.toml
.env (never committed)
.env.example (committed, values empty)
src/Guestbook.sol
script/Deploy.s.sol
web/ (the Vite page)
Le contrat : un livre d’or sans argent dedans
Enregistrez ce fichier sous le nom src/Guestbook.sol. Nous l’avons compilé avec le compilateur Solidity 0.8.38 (solc-js, optimiseur activé, 200 exécutions) : aucune erreur, aucun avertissement, et 1 549 octets de code de création. Le pragma ^0.8.28 accepte tout compilateur 0.8 à partir de la version 0.8.28.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract Guestbook {
struct Entry {
address author;
uint64 timestamp;
string message;
}
Entry[] private entries;
event MessageSigned(address indexed author, uint256 indexed id, string message);
error EmptyMessage();
error MessageTooLong(uint256 length);
function sign(string calldata message) external {
uint256 length = bytes(message).length;
if (length == 0) revert EmptyMessage();
if (length > 140) revert MessageTooLong(length);
entries.push(Entry(msg.sender, uint64(block.timestamp), message));
emit MessageSigned(msg.sender, entries.length - 1, message);
}
function count() external view returns (uint256) {
return entries.length;
}
function getEntry(uint256 id) external view returns (Entry memory) {
return entries[id];
}
}
Chaque ligne reprend ce que la leçon sur Solidity a enseigné, nous n’expliquons donc que les choix de conception. Le contrat ne détient pas d’ether et n’a aucune fonction payable : un bogue peut corrompre une liste de messages mais ne peut faire perdre d’argent à personne, ce qui est le bon niveau de risque pour un premier contrat public. Il n’a ni propriétaire ni constructeur, ce qui simplifie la vérification puisqu’il n’y a aucun argument de constructeur à encoder. La limite de 140 compte des octets, pas des caractères : une lettre accentuée ou un emoji occupe plusieurs octets, si bien qu’un message en français atteint la limite plus tôt que sa longueur visible ne le laisse croire.
Les deux erreurs personnalisées coûtent moins cher que des chaînes de revert et permettent à la page d’indiquer précisément pourquoi une transaction a échoué. L’événement MessageSigned indexe l’auteur et le numéro de l’entrée, ce qui permet à un explorateur ou à un indexeur de filtrer sur l’un ou l’autre. Le stockage est la partie coûteuse de la conception : chaque message est écrit dans l’état de la chaîne, ce qui explique le plafond de longueur et pourquoi un vrai produit garderait les longs textes hors chaîne.
Compiler et tester avant que rien ne quitte votre machine
Lancez forge build pour compiler, puis écrivez au moins un test dans test/ qui signe un message et vérifie count(), et un autre qui attend l’erreur EmptyMessage. La leçon sur Solidity a montré comment faire ; l’important ici est l’habitude. Chaque étape à partir de maintenant touche un réseau public et prend plus de temps à refaire : un bogue attrapé par forge test sur votre ordinateur coûte quelques secondes, et le même bogue découvert après le déploiement coûte un nouveau déploiement, une nouvelle vérification et une nouvelle adresse dans votre page.

Un wallet de test, une clé chiffrée et un point d’accès RPC
Créer un wallet qui n’existe que pour les testnets
Dans votre extension de wallet, créez un nouveau compte ou, mieux, un wallet distinct avec sa propre seed phrase, et nommez-le de façon à ne jamais le confondre avec autre chose (« Sepolia deployer » convient). Cette clé va résider sur un ordinateur portable et être saisie dans des outils de développement : considérez-la comme déjà à moitié exposée et n’envoyez jamais d’actifs réels à son adresse. C’est la clé, pas l’application de wallet, qui contrôle une adresse.
Donnez ensuite cette clé à Foundry dans un keystore chiffré plutôt que dans un fichier en clair. La commande ci-dessous demande la clé privée sans l’afficher, ainsi qu’un mot de passe pour la chiffrer ; le compte est alors connu de toutes les commandes Foundry sous le nom deployer. La documentation de déploiement de Foundry présente le keystore comme l’option recommandée et classe l’option brute --private-key parmi celles qui ne le sont pas. Deux autres voies évitent complètement le keystore : --ledger signe avec un wallet matériel et --browser envoie la transaction au wallet de votre navigateur pour approbation.
cast wallet import deployer --interactive
cast wallet list
N’utilisez jamais les comptes anvil par défaut sur un réseau public. Leurs clés sont publiées dans chaque copie de Foundry, et des robots surveillent les fonds envoyés vers ces adresses.
Un point d’accès RPC et un fichier .env propre
Vos outils atteignent Sepolia par un point d’accès RPC, l’URL d’un nœud qui accepte les requêtes JSON-RPC. Alchemy, Infura, QuickNode, Chainstack et d’autres fournissent un point d’accès Sepolia gratuit après l’ouverture d’un compte, et l’URL contient généralement votre clé d’API. Il existe aussi des points d’accès publics sans compte, mais leur débit est limité et ils changent souvent. La définition de Sepolia intégrée à viem embarque bien un RPC public par défaut ; ne vous y fiez pas au-delà d’un premier essai.
Placez le point d’accès et, plus tard, votre clé Etherscan dans un fichier .env à la racine du projet, et gardez ce fichier hors de Git. Foundry lit un fichier .env dans le dossier du projet, et sa page de bonnes pratiques demande de ne pas versionner les fichiers .env et de commiter à la place un .env.example aux valeurs vides, pour que les autres sachent quelles variables définir. Votre .env tient en deux lignes :
SEPOLIA_RPC_URL=https://your-provider.example/v2/your-key
ETHERSCAN_API_KEY=your-etherscan-key
Ajoutez une ligne .env à .gitignore avant votre premier commit, et lancez git status pour vérifier que le fichier n’apparaît pas. Remarquez ce qui n’y figure pas : aucune clé privée, puisque le keystore la garde. Une clé poussée par erreur dans un dépôt public doit être considérée comme volée, même sur un testnet, car la même habitude coûtera de l’argent réel plus tard.
Donnez ensuite un nom court au point d’accès dans foundry.toml, pour que chaque commande puisse dire --rpc-url sepolia :
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
Un détail qui piège beaucoup de monde : dans Foundry, --chain ne choisit pas un réseau. C’est l’URL du RPC qui décide où partent les transactions. Passez toujours --rpc-url.
Obtenir des ETH Sepolia avec un faucet Sepolia
Le déploiement a besoin de gas, et le gas de Sepolia se paie en ether de test que l’on obtient gratuitement auprès d’un faucet. Ethereum.org recense dix faucets Sepolia, dont ceux d’Alchemy, de Google Cloud Web3, d’Infura, de QuickNode, de Chainstack et le PoW Faucet. Leurs règles diffèrent, et elles évoluent. Le 27 octobre 2026, la page du faucet d’Alchemy indiquait qu’aucun compte n’était nécessaire, tandis que celui de Google Cloud demande de se connecter avec un compte Google. La dotation de base de QuickNode ne demande pas de compte, mais le wallet qui la réclame doit détenir un petit solde sur le mainnet, ce que le faucet justifie par la prévention des abus et du spam. Le PoW Faucet ne demande que votre adresse et un moment de calcul de minage effectué dans le navigateur.
Les montants et les conditions changent d’un mois à l’autre, et les pages des faucets eux-mêmes sont la seule source fiable : nous n’en imprimons donc aucun. Collez l’adresse de votre wallet de test, jamais sa clé, et attendez la transaction. Un premier déploiement d’un contrat de cette taille n’exige qu’une petite fraction de ce que distribue un faucet typique. Si un site vous demande votre seed phrase ou un paiement pour « débloquer » de l’ether de test, fermez-le.
Déployer un smart contract sur Sepolia avec forge create ou forge script
Foundry offre deux façons de mettre le Guestbook sur Sepolia. forge create déploie un contrat avec une seule commande. forge script exécute un script Solidity qui peut déployer plusieurs contrats et les appeler, consigne tout ce qu’il a fait et peut être rejoué. Commencez par la première, puis passez à la seconde pour tout ce que vous répéterez.
Une seule commande : forge create avec –broadcast
source .env
forge create src/Guestbook.sol:Guestbook \
--rpc-url $SEPOLIA_RPC_URL \
--account deployer \
--broadcast
Foundry demande le mot de passe du keystore, signe, envoie la transaction et affiche trois lignes qui comptent : l’adresse du déployeur, l’adresse du nouveau contrat et le hash de la transaction. Copiez l’adresse du contrat ; la page en a besoin.
Regardez --broadcast. Dans les versions actuelles de Foundry, sans cette option, forge create effectue un essai à blanc : il montre ce qui se passerait et n’envoie rien. Beaucoup de tutoriels écrits avant ce changement omettent l’option, et leurs lecteurs se demandent pourquoi aucun contrat n’apparaît sur l’explorateur. Les guides plus anciens passent aussi --private-key sur la ligne de commande, ce qui laisse la clé dans l’historique de votre shell.
Un déploiement reproductible : forge script
Enregistrez ce script sous le nom script/Deploy.s.sol. vm.startBroadcast() sans argument utilise le signataire donné sur la ligne de commande, ici le compte du keystore.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Script} from "forge-std/Script.sol";
import {Guestbook} from "../src/Guestbook.sol";
contract Deploy is Script {
function run() external returns (Guestbook gb) {
vm.startBroadcast(); // le signataire vient de --account sur la ligne de commande
gb = new Guestbook();
vm.stopBroadcast();
}
}
Lancez-le de trois façons. La première commande ne fait que simuler ; la deuxième envoie pour de bon ; la troisième, à n’utiliser que si le réseau a lâché en cours de route, recharge l’exécution enregistrée et envoie ce qui manque encore.
forge script script/Deploy.s.sol --rpc-url sepolia --account deployer
forge script script/Deploy.s.sol --rpc-url sepolia --account deployer --broadcast
forge script script/Deploy.s.sol --rpc-url sepolia --account deployer --broadcast --resume
La simulation exécute votre script sur l’état réel de Sepolia et estime le gas sans rien envoyer. L’exécution réelle écrit dans broadcast/Deploy.s.sol/11155111/run-latest.json un enregistrement de chaque transaction, avec son hash, l’adresse du contrat et le reçu. Beaucoup d’équipes commitent ces enregistrements pour les réseaux publics, parce qu’ils constituent l’historique de ce qui a été déployé et où. Le dossier cache/ est une autre affaire : la documentation de Foundry précise que ses copies conservent l’URL du RPC de chaque transaction, qui peut contenir votre clé d’API, donc ajoutez cache/ à .gitignore.
Vérifier le contrat depuis le terminal avant de construire une page
Ouvrez le hash de la transaction sur sepolia.etherscan.io : vous devez voir « Contract Creation », votre déployeur comme expéditeur et un statut de réussite. Parlez ensuite au contrat avec cast, le client en ligne de commande de Foundry. Le premier appel est gratuit et n’exige aucune clé, le second envoie une vraie transaction depuis votre wallet de test.
export GUESTBOOK=0xYourContractAddress
cast call $GUESTBOOK "count()(uint256)" --rpc-url sepolia
cast send $GUESTBOOK "sign(string)" "hello from the terminal" --rpc-url sepolia --account deployer
cast call $GUESTBOOK "count()(uint256)" --rpc-url sepolia
Si le second count renvoie 1, vous avez réussi à déployer un smart contract sur Sepolia qui fonctionne, et tout problème ultérieur se trouvera dans la page, pas dans Solidity.
Vérifier un smart contract sur Etherscan et Sourcify
Juste après le déploiement, Etherscan n’affiche que du bytecode. Vérifier un smart contract consiste à remettre à l’explorateur votre code source et vos paramètres de compilation : il les recompile et, si le résultat correspond à ce qui est sur la chaîne, il publie le source à côté de l’adresse. Les lecteurs peuvent alors contrôler ce que fait le contrat, l’explorateur affiche des onglets « Read » et « Write » construits à partir de l’ABI, et les wallets et autres outils peuvent décoder vos appels de fonctions. Pour un projet de portfolio, c’est la différence entre « faites-moi confiance » et « lisez-le ».
Etherscan avec une clé d’API V2
La vérification sur Etherscan exige une clé d’API, gratuite avec un compte Etherscan. Depuis son API V2, une seule clé fonctionne pour toutes les chaînes EVM prises en charge par Etherscan : les requêtes vont vers un point d’accès unique et un paramètre chainid choisit le réseau, comme l’explique la documentation de l’API Etherscan (en anglais). La documentation parle de plus de 60 chaînes ; son API de liste des chaînes en renvoyait 57 le 27 octobre 2026. Cette même documentation accorde à l’offre gratuite 3 appels par seconde et jusqu’à 100 000 appels par jour sur certaines chaînes, dont Sepolia, soit bien plus que la vérification n’en demande.
forge verify-contract --chain sepolia $GUESTBOOK src/Guestbook.sol:Guestbook --watch
La clé vient de ETHERSCAN_API_KEY dans votre environnement, ou de --etherscan-api-key. --watch attend et rapporte le résultat au lieu de vous laisser rafraîchir la page. Vous pouvez aussi vérifier pendant le déploiement : ajoutez --verify à forge create ou à l’exécution avec diffusion de forge script. Un contrat avec des arguments de constructeur exige de les passer avec --constructor-args $(cast abi-encode "constructor(...)" ...) ; le nôtre n’en a aucun, ce qui est l’une des raisons pour lesquelles nous l’avons conçu ainsi.
Sourcify : une vérification ouverte, sans clé
Sourcify est un service et une base de données de vérification open source. Il conserve le source et les métadonnées de compilation des contrats vérifiés dans un dépôt public, de sorte que la vérification ne dépend pas d’une seule entreprise. Sa documentation conseille aux utilisateurs de Foundry et de Hardhat de vérifier avec la ligne de commande de leur framework, et l’exemple de Foundry ne demande aucune clé :
forge verify-contract $GUESTBOOK src/Guestbook.sol:Guestbook --chain sepolia --verifier sourcify
Sourcify distingue deux niveaux, renommés dans sa documentation actuelle. Une « exact match » (anciennement « perfect match ») signifie que le bytecode déployé est identique octet pour octet, hash des métadonnées compris, donc que les fichiers source sont exactement ceux qui ont servi à compiler ; ajouter un commentaire ou renommer une variable la brise. Une simple « match » (anciennement « partial match ») signifie que le code se compile vers le même contrat mais que les métadonnées diffèrent. Aplatir les sources exclut aussi la correspondance exacte : laissez donc Foundry soumettre les fichiers tels quels. Un troisième vérificateur, Blockscout, utilisé par beaucoup de petites chaînes, s’obtient avec --verifier blockscout.
Quand la vérification échoue, la cause est presque toujours un écart entre les paramètres du déploiement et ceux de la vérification : autre version du compilateur, optimiseur activé d’un côté et pas de l’autre, nombre d’exécutions différent, ou arguments de constructeur manquants. Gardez ces paramètres dans foundry.toml plutôt que sur la ligne de commande, et les deux étapes liront les mêmes valeurs. Un contrat tout frais peut aussi être « introuvable » pendant une minute, le temps que l’explorateur l’indexe ; --watch patiente à votre place.
Construire l’interface avec Vite et viem
Créez la page à côté du contrat, puis installez viem. La page a besoin de deux valeurs au moment du build, que Vite lit dans web/.env parce que leurs noms commencent par VITE_.
npm create vite@latest web -- --template vanilla
cd web
npm install viem
Créez ensuite web/.env avec l’adresse affichée au déploiement et une URL de RPC en lecture seule :
VITE_CONTRACT_ADDRESS=0xYourContractAddress
VITE_RPC_URL=https://your-provider.example/v2/your-public-key
Tout ce qui se trouve dans un bundle front-end est public, y compris cette URL de RPC. Utilisez une clé distincte que votre fournisseur restreint à votre domaine, ou un point d’accès public, et ne mettez jamais la clé Etherscan ni rien de secret dans web/.env. Remplacez le corps de web/index.html par ce squelette : une zone pour les boutons de wallets, une ligne d’état, un formulaire et une liste.
<h1>Guestbook (Sepolia)</h1>
<div id="wallets"></div>
<p id="status">No wallet connected.</p>
<form id="form"><input id="msg" maxlength="140" required><button>Sign</button></form>
<ul id="entries"></ul>
<script type="module" src="/main.js"></script>
Le JavaScript ci-dessous va dans web/main.js, en cinq parties, dans cet ordre. Ensemble, elles forment un seul fichier d’environ 80 lignes. Nous avons assemblé les cinq parties et vérifié que le fichier s’analyse comme un module JavaScript ; une version antérieure du même code se construisait sans erreur avec Vite 8.3.5 et viem 2.57.12. Les messages affichés par la page restent en anglais dans l’exemple, pour que le code reste identique à l’original ; traduisez-les si votre page s’adresse à un public francophone.
Lire le contrat sans wallet
import { createPublicClient, createWalletClient, custom, http, parseAbi } from 'viem'
import { sepolia } from 'viem/chains'
const CONTRACT = import.meta.env.VITE_CONTRACT_ADDRESS // défini dans .env : 0x... issu du déploiement
const abi = parseAbi([
'function sign(string message)',
'function count() view returns (uint256)',
'function getEntry(uint256 id) view returns ((address author, uint64 timestamp, string message))',
'event MessageSigned(address indexed author, uint256 indexed id, string message)',
'error EmptyMessage()',
'error MessageTooLong(uint256 length)',
])
// Lire n'exige aucun wallet : un RPC public suffit.
const publicClient = createPublicClient({ chain: sepolia, transport: http(import.meta.env.VITE_RPC_URL) })
parseAbi transforme des signatures lisibles en l’ABI dont viem a besoin et vous donne la vérification de types dans un éditeur. Vous pourriez plutôt importer l’ABI complète depuis out/Guestbook.sol/Guestbook.json de Foundry ; pour six entrées, les écrire est plus clair. Ne sautez pas les deux lignes error : sans elles, viem ne peut pas nommer l’erreur personnalisée quand une transaction échoue. Le client public parle à votre point d’accès RPC et n’a pas besoin de wallet : un visiteur sans aucune extension peut donc lire le livre d’or. Cela compte, car la plupart des visiteurs ne font que lire, et une page qui exige un wallet avant de rien montrer les perd.
Trouver les wallets avec EIP-6963
Pendant des années, les extensions de wallet se sont exposées aux pages par un seul objet global, window.ethereum. Avec deux extensions installées, elles se le disputaient et la page obtenait celle qui s’était chargée en dernier. EIP-6963, un standard au statut Final nommé Multi Injected Provider Discovery, règle le problème avec deux événements du navigateur. La page émet eip6963:requestProvider ; chaque wallet répond par eip6963:announceProvider, qui transporte un objet info (un identifiant unique, un nom, une icône et un identifiant en domaine inversé) et son fournisseur. La page les liste et l’utilisateur choisit.
// EIP-6963 : les wallets s'annoncent ; la page écoute, puis demande.
const providers = new Map()
window.addEventListener('eip6963:announceProvider', (event) => {
providers.set(event.detail.info.uuid, event.detail)
render()
})
window.dispatchEvent(new Event('eip6963:requestProvider'))
let walletClient, account
function render() {
const box = document.getElementById('wallets')
box.replaceChildren(...[...providers.values()].map(({ info, provider }) => {
const b = document.createElement('button')
b.textContent = 'Connect ' + info.name
b.onclick = () => connect(provider)
return b
}))
}
L’ordre compte : enregistrez d’abord l’écouteur, puis envoyez la demande, sans quoi les wallets répondent avant que personne n’écoute et la page n’affiche aucun bouton. Les wallets s’annoncent aussi d’eux-mêmes à leur chargement, si bien qu’un écouteur enregistré tôt attrape aussi les retardataires.
Connecter un wallet avec EIP-1193
Le fournisseur que remet chaque wallet suit EIP-1193, l’interface JavaScript standard entre une page et un wallet : une fonction request({ method, params }) et des événements. eth_requestAccounts ouvre l’invite de connexion du wallet et renvoie les adresses que l’utilisateur autorise. wallet_switchEthereumChain demande au wallet de passer sur Sepolia, avec l’identifiant de chaîne en hexadécimal. Si l’utilisateur refuse, le wallet renvoie l’erreur 4001, que EIP-1193 définit comme « User Rejected Request ».
async function connect(provider) {
// EIP-1193 : provider.request({ method, params })
const [address] = await provider.request({ method: 'eth_requestAccounts' })
try {
await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0xaa36a7' }] })
} catch (e) {
if (e.code === 4001) return setStatus('Network switch refused.')
throw e // chaîne inconnue : proposer wallet_addEthereumChain à la place
}
account = address
walletClient = createWalletClient({ account, chain: sepolia, transport: custom(provider) })
provider.on('accountsChanged', () => location.reload())
provider.on('chainChanged', () => location.reload())
setStatus('Connected: ' + address)
}
function setStatus(t) { document.getElementById('status').textContent = t }
Un wallet qui ne connaît pas la chaîne demandée renvoie une erreur au lieu de basculer ; la réponse habituelle est d’appeler wallet_addEthereumChain avec le nom de la chaîne, son URL de RPC et sa devise, puis de réessayer. Dans MetaMask, Sepolia figure déjà dans la liste des réseaux : sa page d’assistance indique d’activer l’option « Show test networks » pour la voir, sans avoir à l’ajouter à la main. D’autres wallets peuvent ne pas la connaître. Les deux gestionnaires d’événements rechargent la page quand l’utilisateur change de compte ou de réseau dans le wallet : c’est rustique, mais cela garantit que la page ne signe jamais depuis une adresse ou sur une chaîne qu’elle n’attendait pas. Une vraie application mettrait plutôt son état à jour.
Écrire dans le contrat et afficher le résultat
document.getElementById('form').addEventListener('submit', async (e) => {
e.preventDefault()
if (walletClient === undefined) return setStatus('Connect a wallet first.')
try {
const hash = await walletClient.writeContract({
address: CONTRACT, abi, functionName: 'sign', args: [document.getElementById('msg').value],
})
setStatus('Sent: ' + hash)
await publicClient.waitForTransactionReceipt({ hash })
setStatus('Confirmed.')
load()
} catch (err) {
setStatus(err.shortMessage ?? err.message)
}
})
writeContract construit la transaction, l’envoie au wallet pour signature et renvoie le hash une fois que le wallet l’a diffusée. Ce hash prouve seulement que la transaction a été envoyée. waitForTransactionReceipt attend qu’elle soit incluse dans un bloc, en général en un ou deux slots (ethereum.org décrit l’Ethereum en preuve d’enjeu comme divisé en slots de 12 secondes, et Sepolia suit le même consensus), et la page ne recharge la liste qu’à ce moment. Si le contrat annule la transaction, viem décode l’erreur personnalisée grâce aux deux entrées error de l’ABI et place un résumé lisible, qui nomme EmptyMessage ou MessageTooLong, dans shortMessage.

Afficher les entrées en sécurité
async function load() {
const n = await publicClient.readContract({ address: CONTRACT, abi, functionName: 'count' })
const list = document.getElementById('entries')
list.replaceChildren()
for (let i = Number(n) - 1; i >= Math.max(0, Number(n) - 10); i--) {
const x = await publicClient.readContract({ address: CONTRACT, abi, functionName: 'getEntry', args: [BigInt(i)] })
const li = document.createElement('li')
li.textContent = x.author.slice(0, 8) + '…: ' + x.message // textContent, jamais innerHTML
list.append(li)
}
}
if (CONTRACT) load()
La boucle lit jusqu’à dix entrées, les plus récentes d’abord, à raison d’un appel chacune. C’est acceptable pour une démonstration et lent pour un vrai produit, qui lirait les événements MessageSigned ou passerait par un indexeur. Regardez textContent. Les messages sont écrits par des inconnus et conservés pour toujours sur une chaîne publique. Les insérer avec innerHTML permettrait à n’importe qui de signer un message contenant un script qui s’exécuterait dans le navigateur de chaque visiteur, une faille de type cross-site scripting que vous ne pourriez jamais effacer de la chaîne. Traitez toute chaîne de caractères venue de la chaîne comme une entrée non fiable.
Lancer la dApp et contrôler chaque maillon de la chaîne
Démarrez le serveur de développement avec npm run dev depuis web/ et ouvrez l’adresse locale qu’il affiche. Parcourez ensuite cette liste dans l’ordre ; chaque étape teste un maillon.
- La page se charge et, au bout d’une seconde, liste l’entrée que vous avez signée avec cast. Cela prouve que l’URL du RPC et l’adresse du contrat sont bonnes, sans aucun wallet.
- Un bouton « Connect » apparaît pour chaque wallet installé. Si aucun n’apparaît, le wallet est absent, désactivé pour ce site, ou ne prend pas en charge EIP-6963.
- Un clic ouvre le wallet et, après votre accord, la ligne d’état affiche votre adresse de test et le wallet est sur Sepolia.
- Saisir un message et appuyer sur Sign ouvre dans le wallet une confirmation qui nomme le contrat et indique des frais estimés en ETH Sepolia.
- L’état passe à « Sent », puis à « Confirmed », et votre message apparaît en haut de la liste.
- Sur sepolia.etherscan.io, la page du contrat montre votre transaction et, dans les Logs de cette transaction, un événement MessageSigned portant votre adresse.
npm run build produit un dossier statique dist/ que vous pouvez héberger n’importe où. L’adresse du contrat et l’URL du RPC sont figées au moment du build : reconstruisez donc après tout nouveau déploiement.
Quand le déploiement ou la page échoue
La plupart des échecs d’une première dApp viennent d’une poignée de causes. Ce tableau relie le message que vous voyez à sa cause habituelle.
| Ce que vous voyez | Cause habituelle | Remède |
|---|---|---|
| « insufficient funds » au déploiement | Le wallet de test ne détient aucun ETH Sepolia, ou vous déployez depuis un autre compte | Vérifiez le solde avec cast balance, passez par un faucet, contrôlez –account |
| La commande se termine mais rien n’apparaît sur l’explorateur | –broadcast manque : c’était un essai à blanc | Relancez avec –broadcast |
| Transaction envoyée sur le mauvais réseau | URL de RPC d’une autre chaîne ; –chain ne choisit pas le réseau | Passez –rpc-url sepolia et vérifiez l’identifiant de chaîne |
| « execution reverted » avec EmptyMessage ou MessageTooLong | Message vide, ou de plus de 140 octets | Raccourcissez le message ; les lettres accentuées et les emojis comptent pour plusieurs octets |
| Vérification « bytecode does not match » | Version du compilateur, optimiseur ou nombre d’exécutions différents du déploiement | Gardez les paramètres dans foundry.toml ; passez les mêmes valeurs |
| La page affiche une liste vide | Mauvaise adresse de contrat dans web/.env, ou build antérieur à cette adresse | Corrigez l’adresse, relancez npm run dev ou reconstruisez |
| Aucun bouton de wallet | Écouteur ajouté après la demande, ou wallet incompatible avec EIP-6963 | Enregistrez l’écouteur d’abord ; essayez un autre wallet |
| Une transaction reste en attente | Une transaction précédente du même compte est bloquée avec des frais trop bas | Patientez, ou remplacez-la depuis le wallet avec le même nonce et des frais plus élevés |
Ce qui change sur le mainnet, en principe
Sur le mainnet d’Ethereum, les mêmes commandes fonctionnent avec une autre URL de RPC, et chaque transaction se paie en ether réel. Déployer coûte le gas consommé par la transaction de création multiplié par le prix du gas à cet instant ; le gas consommé dépend surtout de la taille du code stocké, et le prix varie d’une minute à l’autre, donc nous ne donnons aucun chiffre. La simulation de forge script ou cast estimate donne les unités de gas ; multipliez-les par le prix que votre wallet affiche au moment du déploiement. Notre entrée de glossaire sur les frais de gas explique comment la base fee et le pourboire se combinent. Les réseaux de couche 2 exécutent les mêmes contrats pour une fraction des frais, ce qui explique pourquoi la plupart des nouvelles applications y démarrent.
L’argent change aussi le modèle de sécurité. Un contrat qui détient ou déplace des fonds demande un audit externe avant le lancement, la clé du déployeur a sa place sur un wallet matériel (--ledger) et les droits d’administration sur un multisig plutôt que sur une seule adresse. Notre livre d’or a évité tout cela en ne détenant rien, ce qui est précisément la raison pour laquelle il convient à un premier déploiement.
Reste le cadre des règles. Un livre d’or sur testnet ne soulève aucune question juridique, et l’ether de test n’ayant aucune valeur, il n’en soulève pas non plus sur le plan fiscal. Une dApp qui reçoit des dépôts, émet un jeton vendu au public ou propose du trading à effet de levier peut en revanche relever des règles financières, quoi que dise le code sur la décentralisation : notre guide du règlement MiCA présente le cadre européen des crypto-actifs. Si vous recevez un jour des jetons en paiement d’un travail ou si vous en vendez, notre guide de la fiscalité des cryptos en France explique la déclaration. Ceci décrit un cadre, pas un conseil juridique : un projet qui touche l’argent d’autrui demande un juriste avant le mainnet.
Mettre le déploiement vérifié dans votre portfolio
Un contrat vérifié sur Sepolia, un dépôt public avec ses tests et une page en ligne valent mieux, dans le portfolio d’un développeur junior, qu’un certificat. Un recruteur peut ouvrir l’explorateur, lire le source, voir les tests et signer le livre d’or en moins de cinq minutes. Ajoutez un court README qui liste les commandes, l’adresse et ce que vous changeriez pour le mainnet.
À partir de là, étendez le projet pas à pas : lire les événements au lieu de boucler, ajouter un propriétaire qui peut mettre le contrat en pause et tester que lui seul le peut, passer la page à wagmi et React, ou déployer le même contrat sur un testnet de couche 2. Notre plan pour devenir développeur blockchain situe ce projet dans le parcours d’ensemble, et notre panorama des métiers et salaires de la blockchain montre quels postes le demandent. Une leçon ultérieure du parcours carrières examine les métiers de la DeFi.



