Passer au contenu principal

Construire une interface utilisateur pour votre contrat

TypeScript
react
vite
wagmi
frontend
Débutant
Ori Pomerantz
1 novembre 2023
16 minutes de lecture

Vous avez trouvé une fonctionnalité dont nous avons besoin dans l'écosystème Ethereum. Vous avez écrit les contrats intelligents pour la mettre en œuvre, et peut-être même du code connexe qui s'exécute hors chaîne. C'est génial ! Malheureusement, sans interface utilisateur, vous n'aurez aucun utilisateur, et la dernière fois que vous avez écrit un site web, les gens utilisaient des modems commutés et JavaScript était nouveau.

Cet article est pour vous. Je suppose que vous connaissez la programmation, et peut-être un peu de JavaScript et de HTML, mais que vos compétences en matière d'interface utilisateur sont rouillées et obsolètes. Ensemble, nous allons passer en revue une application moderne simple pour que vous puissiez voir comment on fait de nos jours.

Pourquoi est-ce important

En théorie, vous pourriez simplement demander aux gens d'utiliser Etherscan (opens in a new tab) ou Blockscout (opens in a new tab) pour interagir avec vos contrats. Ce sera formidable pour les Éthériens expérimentés. Mais nous essayons de servir un autre milliard de personnes (opens in a new tab). Cela ne se produira pas sans une excellente expérience utilisateur, et une interface utilisateur conviviale en est une grande partie.

Application Greeter

Il y a beaucoup de théorie derrière le fonctionnement d'une interface utilisateur moderne, et beaucoup de bons sites (opens in a new tab) qui l'expliquent (opens in a new tab). Au lieu de répéter l'excellent travail effectué par ces sites, je vais supposer que vous préférez apprendre par la pratique et commencer par une application avec laquelle vous pouvez jouer. Vous avez encore besoin de la théorie pour faire avancer les choses, et nous y viendrons - nous allons simplement parcourir les fichiers sources un par un, et discuter des choses au fur et à mesure que nous les abordons.

Installation

  1. Si nécessaire, ajoutez la blockchain Holesky (opens in a new tab) à votre portefeuille et obtenez des ETH de test (opens in a new tab).

  2. Clonez le dépôt GitHub.

    1git clone https://github.com/qbzzt/20230801-modern-ui.git
  3. Installez les paquets nécessaires.

    1cd 20230801-modern-ui
    2pnpm install
  4. Démarrez l'application.

    1pnpm dev
  5. Naviguez vers l'URL affichée par l'application. Dans la plupart des cas, c'est http://localhost:5173/ (opens in a new tab).

  6. Vous pouvez voir le code source du contrat, une version légèrement modifiée du Greeter de Hardhat, sur un explorateur de blockchain (opens in a new tab).

Présentation des fichiers

index.html

Ce fichier est un modèle HTML standard, à l'exception de cette ligne, qui importe le fichier de script.

1<script type="module" src="/src/main.tsx"></script>

src/main.tsx

L'extension du fichier nous indique que ce fichier est un composant React (opens in a new tab) écrit en TypeScript (opens in a new tab), une extension de JavaScript qui prend en charge la vérification de type (opens in a new tab). TypeScript est compilé en JavaScript, nous pouvons donc l'utiliser pour l'exécution côté client.

1import '@rainbow-me/rainbowkit/styles.css'
2import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
3import * as React from 'react'
4import * as ReactDOM from 'react-dom/client'
5import { WagmiConfig } from 'wagmi'
6import { chains, config } from './wagmi'

Importez le code de la bibliothèque dont nous avons besoin.

1import { App } from './App'

Importez le composant React qui met en œuvre l'application (voir ci-dessous).

1ReactDOM.createRoot(document.getElementById('root')!).render(

Créez le composant React racine. Le paramètre de render est JSX (opens in a new tab), un langage d'extension qui utilise à la fois HTML et JavaScript/TypeScript. Le point d'exclamation ici indique au composant TypeScript : « vous ne savez pas que document.getElementById('root') sera un paramètre valide pour ReactDOM.createRoot, mais ne vous inquiétez pas - je suis le développeur et je vous dis qu'il y en aura un ».

1 <React.StrictMode>

L'application se trouve à l'intérieur d'un composant React.StrictMode (opens in a new tab). Ce composant indique à la bibliothèque React d'insérer des vérifications de débogage supplémentaires, ce qui est utile pendant le développement.

1 <WagmiConfig config={config}>

L'application se trouve également à l'intérieur d'un composant WagmiConfig (opens in a new tab). La bibliothèque wagmi (we are going to make it) (opens in a new tab) connecte les définitions de l'interface utilisateur React avec la bibliothèque viem (opens in a new tab) pour l'écriture d'une application décentralisée Ethereum.

1 <RainbowKitProvider chains={chains}>

Et enfin, un composant RainbowKitProvider (opens in a new tab). Ce composant gère la connexion et la communication entre le portefeuille et l'application.

1 <App />

Nous pouvons maintenant avoir le composant pour l'application, qui met réellement en œuvre l'interface utilisateur. Le /> à la fin du composant indique à React que ce composant n'a pas de définitions à l'intérieur, conformément à la norme XML.

1 </RainbowKitProvider>
2 </WagmiConfig>
3 </React.StrictMode>,
4)

Bien sûr, nous devons fermer les autres composants.

src/App.tsx

1import { ConnectButton } from '@rainbow-me/rainbowkit'
2import { useAccount } from 'wagmi'
3import { Greeter } from './components/Greeter'
4
5export function App() {

C'est la manière standard de créer un composant React - définir une fonction qui est appelée chaque fois qu'elle doit être rendue. Cette fonction a généralement du code TypeScript ou JavaScript en haut, suivi d'une instruction return qui renvoie le code JSX.

1 const { isConnected } = useAccount()

Ici, nous utilisons useAccount (opens in a new tab) pour vérifier si nous sommes connectés à une blockchain via un portefeuille ou non.

Par convention, dans React, les fonctions appelées use... sont des hooks (opens in a new tab) qui renvoient un certain type de données. Lorsque vous utilisez de tels hooks, non seulement votre composant obtient les données, mais lorsque ces données changent, le composant est re-rendu avec les informations mises à jour.

1 return (
2 <>

Le JSX d'un composant React doit renvoyer un seul composant. Lorsque nous avons plusieurs composants et que nous n'avons rien qui les englobe « naturellement », nous utilisons un composant vide (<> ... </>) pour en faire un seul composant.

1 <h1>Greeter</h1>
2 <ConnectButton />

Nous obtenons le composant ConnectButton (opens in a new tab) de RainbowKit. Lorsque nous ne sommes pas connectés, il nous donne un bouton Connect Wallet qui ouvre une fenêtre modale qui explique les portefeuilles et vous permet de choisir celui que vous utilisez. Lorsque nous sommes connectés, il affiche la blockchain que nous utilisons, l'adresse de notre compte et notre solde d'ETH. Nous pouvons utiliser ces affichages pour changer de réseau ou pour nous déconnecter.

1 {isConnected && (

Lorsque nous devons insérer du JavaScript réel (ou du TypeScript qui sera compilé en JavaScript) dans un JSX, nous utilisons des accolades ({}).

La syntaxe a && b est un raccourci pour [a ? b : a](https://www.w3schools.com/react/react_es6_ternary.asp). C'est-à-dire que si aest vrai, il est évalué àbet sinon, il est évalué àa(qui peut êtrefalse, 0`, etc.). C'est un moyen facile d'indiquer à React qu'un composant ne doit être affiché que si une certaine condition est remplie.

Dans ce cas, nous ne voulons montrer à l'utilisateur Greeter que si l'utilisateur est connecté à une blockchain.

1 <Greeter />
2 )}
3 </>
4 )
5}

src/components/Greeter.tsx

Ce fichier contient la plupart des fonctionnalités de l'interface utilisateur. Il inclut des définitions qui seraient normalement dans plusieurs fichiers, mais comme il s'agit d'un tutoriel, le programme est optimisé pour être facile à comprendre la première fois, plutôt que pour la performance ou la facilité de maintenance.

1import { useState, ChangeEventHandler } from 'react'
2import { useNetwork,
3 useReadContract,
4 usePrepareContractWrite,
5 useContractWrite,
6 useContractEvent
7 } from 'wagmi'

Nous utilisons ces fonctions de bibliothèque. Encore une fois, elles sont expliquées ci-dessous là où elles sont utilisées.

1import { AddressType } from 'abitype'

La bibliothèque abitype (opens in a new tab) nous fournit des définitions TypeScript pour divers types de données Ethereum, tels que AddressType (opens in a new tab).

1let greeterABI = [
2 .
3 .
4 .
5] as const // greeterABI

L'ABI pour le contrat Greeter. Si vous développez les contrats et l'interface utilisateur en même temps, vous les placez normalement dans le même dépôt et utilisez l'ABI généré par le compilateur Solidity comme fichier dans votre application. Cependant, ce n'est pas nécessaire ici car le contrat est déjà développé et ne va pas changer.

1type AddressPerBlockchainType = {
2 [key: number]: AddressType
3}

TypeScript est fortement typé. Nous utilisons cette définition pour spécifier l'adresse à laquelle le contrat Greeter est déployé sur différentes chaînes. La clé est un nombre (le chainId), et la valeur est un AddressType (une adresse).

1const contractAddrs: AddressPerBlockchainType = {
2 // Holesky
3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
4
5 // Sepolia
6 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
7}

L'adresse du contrat sur les deux réseaux pris en charge : Holesky (opens in a new tab) et Sepolia (opens in a new tab).

Note : Il y a en fait une troisième définition, pour Redstone Holesky, elle sera expliquée ci-dessous.

1type ShowObjectAttrsType = {
2 name: string,
3 object: any
4}

Ce type est utilisé comme paramètre pour le composant ShowObject (expliqué plus tard). Il inclut le nom de l'objet et sa valeur, qui sont affichés à des fins de débogage.

1type ShowGreetingAttrsType = {
2 greeting: string | undefined
3}

À tout moment, nous pouvons soit savoir quel est le message d'accueil (parce que nous l'avons lu sur la blockchain), soit ne pas le savoir (parce que nous ne l'avons pas encore reçu). Il est donc utile d'avoir un type qui peut être soit une chaîne de caractères, soit rien.

Composant Greeter
1const Greeter = () => {

Enfin, nous arrivons à la définition du composant.

1 const { chain } = useNetwork()

Informations sur la chaîne que nous utilisons, gracieuseté de wagmi (opens in a new tab). Comme il s'agit d'un hook (use...), chaque fois que cette information change, le composant est redessiné.

1 const greeterAddr = chain && contractAddrs[chain.id]

L'adresse du contrat Greeter, qui varie selon la chaîne (et qui est undefined si nous n'avons pas d'informations sur la chaîne ou si nous sommes sur une chaîne sans ce contrat).

1 const readResults = useReadContract({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: "greet" , // Pas d'arguments
5 watch: true
6 })

Le hook useReadContract (opens in a new tab) lit les informations d'un contrat. Vous pouvez voir exactement quelles informations il renvoie en développant readResults dans l'interface utilisateur. Dans ce cas, nous voulons qu'il continue à chercher afin d'être informés lorsque le message d'accueil change.

Note : Nous pourrions écouter les événements setGreeting (opens in a new tab) pour savoir quand le message d'accueil change et le mettre à jour de cette manière. Cependant, bien que cela puisse être plus efficace, cela ne s'appliquera pas dans tous les cas. Lorsque l'utilisateur passe à une chaîne différente, le message d'accueil change également, mais ce changement n'est pas accompagné d'un événement. Nous pourrions avoir une partie du code qui écoute les événements et une autre qui identifie les changements de chaîne, mais ce serait plus compliqué que de simplement définir le paramètre watch (opens in a new tab).

1 const [ newGreeting, setNewGreeting ] = useState("")

Le hook useState de React (opens in a new tab) nous permet de spécifier une variable d'état, dont la valeur persiste d'un rendu du composant à un autre. La valeur initiale est le paramètre, dans ce cas la chaîne vide.

Le hook useState renvoie une liste avec deux valeurs :

  1. La valeur actuelle de la variable d'état.
  2. Une fonction pour modifier la variable d'état si nécessaire. Comme il s'agit d'un hook, chaque fois qu'il est appelé, le composant est à nouveau rendu.

Dans ce cas, nous utilisons une variable d'état pour le nouveau message d'accueil que l'utilisateur veut définir.

1 const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>
2 setNewGreeting(evt.target.value)

C'est le gestionnaire d'événements pour le changement du champ de saisie du nouveau message d'accueil. Le type, ChangeEventHandler<HTMLInputElement> (opens in a new tab), spécifie qu'il s'agit d'un gestionnaire pour un changement de valeur d'un élément d'entrée HTML. La partie <HTMLInputElement> est utilisée car il s'agit d'un type générique (opens in a new tab).

1 const preparedTx = usePrepareContractWrite({
2 address: greeterAddr,
3 abi: greeterABI,
4 functionName: 'setGreeting',
5 args: [ newGreeting ]
6 })
7 const workingTx = useContractWrite(preparedTx.config)

Voici le processus pour soumettre une transaction blockchain du point de vue du client :

  1. Envoyez la transaction à un nœud de la blockchain en utilisant eth_estimateGas (opens in a new tab).
  2. Attendez une réponse du nœud.
  3. Lorsque la réponse est reçue, demandez à l'utilisateur de signer la transaction via le portefeuille. Cette étape doit avoir lieu après la réception de la réponse du nœud, car le coût en gaz de la transaction est montré à l'utilisateur avant qu'il ne la signe.
  4. Attendez l'approbation de l'utilisateur.
  5. Envoyez à nouveau la transaction, cette fois en utilisant eth_sendRawTransaction (opens in a new tab).

L'étape 2 est susceptible de prendre un temps perceptible, pendant lequel les utilisateurs se demanderaient si leur commande a bien été reçue par l'interface utilisateur et pourquoi on ne leur demande pas déjà de signer la transaction. Cela crée une mauvaise expérience utilisateur (UX).

La solution est d'utiliser des hooks de préparation (opens in a new tab). Chaque fois qu'un paramètre change, envoyez immédiatement au nœud la requête eth_estimateGas. Ensuite, lorsque l'utilisateur veut réellement envoyer la transaction (dans ce cas en appuyant sur Mettre à jour le message d'accueil), le coût en gaz est connu et l'utilisateur peut voir la page du portefeuille immédiatement.

1 return (

Nous pouvons maintenant enfin créer le HTML réel à renvoyer.

1 <>
2 <h2>Greeter</h2>
3 {
4 !readResults.isError && !readResults.isLoading &&
5 <ShowGreeting greeting={readResults.data} />
6 }
7 <hr />

Créez le composant ShowGreeting (expliqué ci-dessous), mais uniquement si le message d'accueil a été lu avec succès à partir de la blockchain.

1 <input type="text"
2 value={newGreeting}
3 onChange={greetingChange}
4 />

C'est le champ de saisie de texte où l'utilisateur peut définir un nouveau message d'accueil. Chaque fois que l'utilisateur appuie sur une touche, nous appelons greetingChange qui appelle setNewGreeting. Comme setNewGreeting provient du hook useState, il provoque un nouveau rendu du composant Greeter. Cela signifie que :

  • Nous devons spécifier value pour conserver la valeur du nouveau message d'accueil, sinon il reviendrait à la valeur par défaut, la chaîne vide.
  • usePrepareContractWrite est appelé chaque fois que newGreeting change, ce qui signifie qu'il aura toujours le dernier newGreeting dans la transaction préparée.
1 <button disabled={!workingTx.write}
2 onClick={workingTx.write}
3 >
4 Mettre à jour le message d'accueil
5 </button>

S'il n'y a pas de workingTx.write, nous attendons toujours les informations nécessaires pour envoyer la mise à jour du message d'accueil, donc le bouton est désactivé. S'il y a une valeur workingTx.write, alors c'est la fonction à appeler pour envoyer la transaction.

1 <hr />
2 <ShowObject name="readResults" object={readResults} />
3 <ShowObject name="preparedTx" object={preparedTx} />
4 <ShowObject name="workingTx" object={workingTx} />
5 </>
6 )
7}

Enfin, pour vous aider à voir ce que nous faisons, montrez les trois objets que nous utilisons :

  • readResults
  • preparedTx
  • workingTx
Composant ShowGreeting

Ce composant montre

1const ShowGreeting = (attrs : ShowGreetingAttrsType) => {

Une fonction de composant reçoit un paramètre avec tous les attributs du composant.

1 return <b>{attrs.greeting}</b>
2}
Composant ShowObject

À titre d'information, nous utilisons le composant ShowObject pour montrer les objets importants (readResults pour la lecture du message d'accueil et preparedTx et workingTx pour les transactions que nous créons).

1const ShowObject = (attrs: ShowObjectAttrsType ) => {
2 const keys = Object.keys(attrs.object)
3 const funs = keys.filter(k => typeof attrs.object[k] == "function")
4 return <>
5 <details>

Nous ne voulons pas encombrer l'interface utilisateur avec toutes les informations, donc pour permettre de les voir ou de les fermer, nous utilisons une balise details (opens in a new tab).

1 <summary>{attrs.name}</summary>
2 <pre>
3 {JSON.stringify(attrs.object, null, 2)}

La plupart des champs sont affichés en utilisant JSON.stringify (opens in a new tab).

1 </pre>
2 { funs.length > 0 &&
3 <>
4 Fonctions :
5 <ul>

L'exception concerne les fonctions, qui ne font pas partie de la norme JSON (opens in a new tab), elles doivent donc être affichées séparément.

1 {funs.map((f, i) =>

Dans JSX, le code à l'intérieur des accolades { } est interprété comme du JavaScript. Ensuite, le code à l'intérieur des parenthèses ( ) est interprété à nouveau comme du JSX.

1 (<li key={i}>{f}</li>)
2 )}

React exige que les balises dans l'arbre DOM (opens in a new tab) aient des identifiants distincts. Cela signifie que les enfants de la même balise (dans ce cas, la liste non ordonnée (opens in a new tab)), ont besoin d'attributs key différents.

1 </ul>
2 </>
3 }
4 </details>
5 </>
6}

Terminez les différentes balises HTML.

L'exportation finale
1export { Greeter }

Le composant Greeter est celui que nous devons exporter pour l'application.

src/wagmi.ts

Enfin, diverses définitions liées à WAGMI se trouvent dans src/wagmi.ts. Je ne vais pas tout expliquer ici, car la plupart est du code passe-partout que vous n'aurez probablement pas besoin de changer.

Le code ici n'est pas exactement le même que sur GitHub (opens in a new tab) car plus tard dans l'article, nous ajoutons une autre chaîne (Redstone Holesky (opens in a new tab)).

1import { getDefaultWallets } from '@rainbow-me/rainbowkit'
2import { configureChains, createConfig } from 'wagmi'
3import { holesky, sepolia } from 'wagmi/chains'

Importez les blockchains que l'application prend en charge. Vous pouvez voir la liste des chaînes prises en charge dans le GitHub de viem (opens in a new tab).

1import { publicProvider } from 'wagmi/providers/public'
2
3const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575'

Pour pouvoir utiliser WalletConnect (opens in a new tab), vous avez besoin d'un ID de projet pour votre application. Vous pouvez l'obtenir sur cloud.walletconnect.com (opens in a new tab).

1const { chains, publicClient, webSocketPublicClient } = configureChains(
2 [ holesky, sepolia ],
3 [
4 publicProvider(),
5 ],
6)
7
8const { connectors } = getDefaultWallets({
9 appName: 'My wagmi + RainbowKit App',
10 chains,
11 projectId: walletConnectProjectId,
12})
13
14export const config = createConfig({
15 autoConnect: true,
16 connectors,
17 publicClient,
18 webSocketPublicClient,
19})
20
21export { chains }
Afficher tout

Ajouter une autre blockchain

De nos jours, il existe de nombreuses solutions de mise à l'échelle de couche 2, et vous pourriez vouloir en prendre en charge certaines que viem ne prend pas encore en charge. Pour ce faire, vous devez modifier src/wagmi.ts. Ces instructions expliquent comment ajouter Redstone Holesky (opens in a new tab).

  1. Importez le type defineChain depuis viem.

    1import { defineChain } from 'viem'
  2. Ajoutez la définition du réseau.

    1const redstoneHolesky = defineChain({
    2 id: 17_001,
    3 name: 'Redstone Holesky',
    4 network: 'redstone-holesky',
    5 nativeCurrency: {
    6 decimals: 18,
    7 name: 'Ether',
    8 symbol: 'ETH',
    9 },
    10 rpcUrls: {
    11 default: {
    12 http: ['https://rpc.holesky.redstone.xyz'],
    13 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    14 },
    15 public: {
    16 http: ['https://rpc.holesky.redstone.xyz'],
    17 webSocket: ['wss://rpc.holesky.redstone.xyz/ws'],
    18 },
    19 },
    20 blockExplorers: {
    21 default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' },
    22 },
    23})
    Afficher tout
  3. Ajoutez la nouvelle chaîne à l'appel configureChains.

    1 const { chains, publicClient, webSocketPublicClient } = configureChains(
    2 [ holesky, sepolia, redstoneHolesky ],
    3 [ publicProvider(), ],
    4 )
  4. Assurez-vous que l'application connaît l'adresse de vos contrats sur le nouveau réseau. Dans ce cas, nous modifions src/components/Greeter.tsx :

    1const contractAddrs : AddressPerBlockchainType = {
    2 // Holesky
    3 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
    4
    5 // Redstone Holesky
    6 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3',
    7
    8 // Sepolia
    9 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
    10}
    Afficher tout

Conclusion

Bien sûr, vous ne vous souciez pas vraiment de fournir une interface utilisateur pour Greeter. Vous voulez créer une interface utilisateur pour vos propres contrats. Pour créer votre propre application, suivez ces étapes :

  1. Spécifiez la création d'une application wagmi.

    1pnpm create wagmi
  2. Nommez l'application.

  3. Sélectionnez le framework React.

  4. Sélectionnez la variante Vite.

  5. Vous pouvez ajouter le kit Rainbow (opens in a new tab).

Maintenant, allez rendre vos contrats utilisables pour le monde entier.

Voir ici pour plus de mon travail (opens in a new tab).

Dernière mise à jour de la page : 3 mars 2026

Ce tutoriel vous a été utile ?