Tutoriel Complet : Créer un Frontend pour Interagir avec des Smart Contracts sur Morph

Apercu

Dans "Comment Construire une DApp sur Morph : Un Guide Complet", vous avez appris à configurer et déployer vos smart contracts iToken et iTokenFaucet en utilisant Hardhat. Maintenant, il est temps de donner vie à ces contrats avec une application frontend conviviale.

Dans ce tutoriel, nous vous guiderons à travers la configuration d’une application React intégrée à Wagmi v2 et RainbowKit pour interagir avec vos contrats déployés sur le réseau Morph Holesky. Vous apprendrez à connecter des portefeuilles, afficher les soldes de jetons et permettre aux utilisateurs de réclamer des jetons depuis le faucet.

Dans ce tutoriel, nous allons configurer une application frontend React en utilisant Vite avec TypeScript, intégrer Wagmi v2 pour les interactions avec Ethereum, RainbowKit pour la connexion des portefeuilles, et Tailwind CSS pour le style. Nous nous assurerons que toutes les configurations sont conformes aux dernières directives de migration de Wagmi v2.

Prérequis

  • Node.js (v14 ou supérieur)

  • npm (v6 ou supérieur)

  • Git

  • Contrats iToken et iTokenFaucet déployés sur le réseau Morph Holesky

Guide Étape par Étape

  1. Initialiser un Nouveau Projet React TypeScript avec Vite
    Commencez par créer un nouveau projet React en utilisant Vite avec le modèle TypeScript :

     npm create vite@latest morph-frontend -- --template react-ts
     cd morph-frontend
    
  2. Initialiser un Dépôt Git

     git init
    
  3. Installer les Dépendances
    Installez les dépendances nécessaires :

     npm install @rainbow-me/rainbowkit wagmi viem@2.x @tanstack/react-query ethers
     npm install -D tailwindcss postcss autoprefixer
    
  4. Configurer Tailwind CSS
    Initialisez Tailwind CSS :

     npx tailwindcss init -p
    

    Mettez à jour tailwind.config.cjs :

     /** @type {import('tailwindcss').Config} */
     module.exports = {
       content: [
         "./index.html",
         "./src/**/*.{js,ts,jsx,tsx}",
       ],
       theme: {
         extend: {},
       },
       plugins: [],
     }
    

    Mettez à jour postcss.config.cjs :

     module.exports = {
       plugins: {
         tailwindcss: {},
         autoprefixer: {},
       },
     }
    

    Ajoutez les directives Tailwind à src/index.css :

     @tailwind base;
     @tailwind components;
     @tailwind utilities;
    
  5. Définir les Constantes du Contrat
    Créez src/constants.ts :

     export const ITOKEN_ADDRESS = '0xYourITokenAddressHere'
     export const ITOKEN_FAUCET_ADDRESS = '0xYourITokenFaucetAddressHere'
    
     export const ITOKEN_ABI = [
       'function name() view returns (string)',
       'function symbol() view returns (string)',
       'function decimals() view returns (uint8)',
       'function balanceOf(address owner) view returns (uint256)',
       'function transfer(address to, uint amount) returns (bool)',
       'event Transfer(address indexed from, address indexed to, uint amount)',
     ]
    
     export const ITOKEN_FAUCET_ABI = [
       'function claim() external',
       'function drain() external',
       'function dailyClaim() view returns (uint256)',
       'function lastClaimed(address) view returns (uint256)',
       'event Claimed(address indexed *claimer, uint256 *amount)',
     ]
    
  6. Configurer les Hooks de Contrat
    Créez src/hooks/useContracts.ts :

     import { ITOKEN_ADDRESS, ITOKEN_ABI, ITOKEN_FAUCET_ADDRESS, ITOKEN_FAUCET_ABI } from '../constants'
    
     export const useIToken = () => ({
       address: ITOKEN_ADDRESS,
       abi: ITOKEN_ABI,
     })
    
     export const useITokenFaucet = () => ({
       address: ITOKEN_FAUCET_ADDRESS,
       abi: ITOKEN_FAUCET_ABI,
     })
    
  7. Configurer le Point d'Entrée Principal
    Mettez à jour src/main.tsx :

    ```typescript import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import './index.css' import '@rainbow-me/rainbowkit/styles.css' import { RainbowKitProvider, getDefaultWallets, darkTheme, } from '@rainbow-me/rainbowkit' import { createConfig, WagmiProvider } from 'wagmi' import { http } from 'wagmi' import { mainnet, sepolia } from 'wagmi/chains' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

    const morphTestnet = { id: 2810, name: 'Morph Holesky', network: 'morphTestnet', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18, }, rpcUrls: { default: ['rpc-quicknode-holesky.morphl2.io'], }, blockExplorers: { default: { name: 'Blockscout', url: 'explorer-holesky.morphl2.io' }, }, testnet: true, }

    const queryClient = new QueryClient()

    const { connectors } = getDefaultWallets({ appName: 'Morph Frontend', projectId: 'YOUR_PROJECT_ID', chains: [morphTestnet, mainnet, sepolia], })

    const config = createConfig({ chains: [morphTestnet, mainnet, sepolia], transports: {

}, connectors, })

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


8. **Mettre à Jour le Composant App**  
    Mettez à jour `src/App.tsx` :

    ```typescript
    import React, { useEffect } from 'react'
    import { ConnectButton } from '@rainbow-me/rainbowkit'
    import { useSwitchChain } from 'wagmi'
    import { useAccount } from 'wagmi'
    import Balances from './components/Balances'
    import Faucet from './components/Faucet'

    function App() {
      const { chain, isConnected } = useAccount()
      const { switchChain } = useSwitchChain()

      useEffect(() => {
        if (isConnected && chain?.id !== 2810) {
          switchChain?.(2810)
        }
      }, [isConnected, chain, switchChain])

      return (
        <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100 p-4">
          <h1 className="text-4xl font-bold mb-8">Morph L2 DApp</h1>
          <ConnectButton />
          {isConnected && chain?.id !== 2810 && (
            <p className="mt-4 text-red-500">
              Please switch to the Morph Holesky network.
            </p>
          )}
          {isConnected && chain?.id === 2810 && (
            <>
              <Balances />
              <Faucet />
            </>
          )}
        </div>
      )
    }

    export default App
  1. Créer le Composant Balances
    Créez src/components/Balances.tsx :

     import React, { useEffect, useState } from 'react'
     import { useAccount, useReadContracts } from 'wagmi'
     import { useIToken } from '../hooks/useContracts'
     import { erc20Abi } from 'viem'
     import { formatUnits } from 'viem'
    
     const Balances: React.FC = () => {
       const { address, isConnected } = useAccount()
       const iToken = useIToken()
       const [balance, setBalance] = useState<string>('0')
    
       const { data, isError, isLoading } = useReadContracts({
         contracts: [
           {
             address: iToken.address,
             abi: erc20Abi,
             functionName: 'balanceOf',
             args: [address!],
           },
           {
             address: iToken.address,
             abi: erc20Abi,
             functionName: 'decimals',
           },
           {
             address: iToken.address,
             abi: erc20Abi,
             functionName: 'symbol',
           },
         ],
       })
    
       useEffect(() => {
         if (data && data[0] && data[1]) {
           const rawBalance = data[0].result as bigint
           const decimals = Number(data[1].result)
           const formatted = formatUnits(rawBalance, decimals)
           setBalance(formatted)
         }
       }, [data])
    
       if (!isConnected) {
         return null
       }
    
       if (isLoading) {
         return <p className="mt-8 text-xl">Loading balance...</p>
       }
    
       if (isError) {
         return <p className="mt-8 text-red-600">Error fetching balance.</p>
       }
    
       return (
         <div className="mt-8 bg-white shadow-lg rounded-lg p-6">
           <h2 className="text-2xl font-semibold">Your iToken Balance</h2>
           <p className="mt-2 text-xl">{balance} IT</p>
         </div>
       )
     }
    
     export default Balances
    
  2. Créer le Composant Faucet
    Créez src/components/Faucet.tsx :

    import React, { useState } from 'react'
    import { useITokenFaucet } from '../hooks/useContracts'
    import { useAccount, useWriteContract, useSimulateContract } from 'wagmi'
    
    const Faucet: React.FC = () => {
      const { isConnected } = useAccount()
      const faucet = useITokenFaucet()
      const [loading, setLoading] = useState<boolean>(false)
      const [message, setMessage] = useState<string>('')
    
      const { data: simulationData, isLoading: isSimulating } = useSimulateContract({
        address: faucet.address,
        abi: faucet.abi,
        functionName: 'claim',
        args: [],
      })
    
      const { writeContract: claimWrite, isPending: isWriting } = useWriteContract()
    
      const handleClaim = () => {
        setLoading(true)
        setMessage('')
        claimWrite?.({
          address: faucet.address,
          abi: faucet.abi,
          functionName: 'claim',
          args: [],
        }, {
          onSuccess() {
            setMessage('Claim successful!')
            setLoading(false)
          },
          onError(error: any) {
            setMessage(error.message || 'Claim failed')
            setLoading(false)
          },
        })
      }
    
      return (
        <div className="mt-8 bg-white shadow-lg rounded-lg p-6">
          <h2 className="text-2xl font-semibold">iToken Faucet</h2>
          <button
            onClick={handleClaim}
            disabled={!isConnected || loading || isSimulating}
            className={`px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75 disabled:opacity-50`}
          >
            {loading || isSimulating ? 'Claiming...' : 'Claim 1000 IT'}
          </button>
          {message && <p className="mt-2 text-green-600">{message}</p>}
          {(isWriting || isSimulating) && <p className="mt-2">Processing...</p>}
        </div>
      )
    }
    
    export default Faucet
    
  3. Configurer .gitignore
    Mettez à jour .gitignore :

    # dependencies
    /node_modules
    
    # production
    /dist
    
    # misc
    .DS_Store
    .env
    .env.local
    .env.development.local
    .env.test.local
    .env.production.local
    
    # local env files
    *.local
    
    # logs
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    
    # editor directories and files
    .vscode/
    .idea/
    *.suo
    *.ntvs*
    *.njsproj
    *.sln
    
  4. Dernières Étapes

  5. Remplacez les Variables :

    • Dans src/constants.ts, remplacez '0xYourITokenAddressHere' et '0xYourITokenFaucetAddressHere' par les adresses réelles de vos contrats.

    • Dans src/main.tsx, remplacez 'YOUR_PROJECT_ID' par votre identifiant de projet RainbowKit.

  6. Lancer le Serveur de Développement

    npm run dev
    
  7. Ouvrez votre navigateur et accédez à http://localhost:5173 pour voir votre application.

Conclusion

Félicitations ! Vous avez configuré avec succès une application frontend React intégrée à Wagmi v2, RainbowKit et Tailwind CSS. Votre application permet désormais aux utilisateurs de :

  • Connecter leurs portefeuilles Ethereum via RainbowKit

  • Basculer automatiquement sur le réseau Morph Holesky

  • Voir leur solde de iToken

  • Réclamer des iTokens depuis le faucet

Points Clés :

  • Intégration de Wagmi v2 : Configurée correctement avec QueryClientProvider, WagmiProvider et RainbowKitProvider.

  • Interactions avec les Smart Contracts: Utilisation des hooks useReadContracts, useWriteContract et useSimulateContract pour des interactions efficaces avec les contrats.

  • Gestion des Erreurs : Mise en place d'une gestion des erreurs robuste et des mécanismes de retour d'information pour l'utilisateur.

  • Tailwind CSS : Configuré pour un style efficace et évolutif.

Prochaines Étapes :

  • Améliorer l'UI/UX : Personnalisez davantage vos composants avec Tailwind CSS.

  • Étendre les Fonctionnalités : Ajoutez des fonctionnalités comme le staking ou la gouvernance.

  • Déploiement : Préparez votre application pour un déploiement sur des plateformes comme Vercel, Netlify ou GitHub Pages.

Bon code! 🚀