Développer une dApp sur Horizen EON : The Faucet Coin

Je vais, dans ce tutoriel, vous montrer rapidement comment développer une dApp sur Horizen EON. Je vais créer un jeton ERC-20 dont le contrat embarque son propre faucet, TheFaucetCoin, dont le symbole sera TFC. Dans un second temps, je créérai une dApp permettant de réclamer des TFC en utilisant Ankr comme service RPC associé à RainbowKit et Wagmi pour la partie API de haut niveau.

Le Token TFC

Pour développer une dApp sur Horizen EON on a besoin d’un minimum de 2 composants, un smartcontract et une application pour l’utilisateur.

On attaque directement avec le code solidity du smart contracts du Token $TFC : The Faucet Coin

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract TheFaucetCoin is ERC20, ReentrancyGuard {
// Well ... Max Supply ...
uint maxSupply = 1000000000 * 10 ** 18;
uint dropAmount = 10 ** 18; // 1

// Store each address last airdrop time
mapping (address => uint) usersLastTime;

// Check if the user has claimed in the last ~24H
function canClaim() public view returns (bool) {
    return (block.timestamp > usersLastTime[msg.sender] + 86400);
}

// Method to call to get a faucet drop
function useFaucet() external nonReentrant {
    require (canClaim(), "Only once a day baby");
    require (totalSupply() < maxSupply, "Max supply reached");
    
    usersLastTime[msg.sender] = block.timestamp;
    _mint(msg.sender, dropAmount);
}

constructor() ERC20("The Faucet coin", "TFC") {
    // What else ?
}

}

On part d’un contrat ERC20 modèle OpenZeppelin de base dont on vide le constructeur.

On ajoute 3 variables :

  • maxSupply contenant le nombre maximum de jetons pouvant être créés, ici un milliard.
  • dropAmount contenant le nombre de jetons octroyés par utilisation du faucet, ici 1.
  • usersLastTime contenant le moment de dernière utilisation du faucet par une adresse.

L’idée est donc de distribuer 1 TFC à chaque adresse utilisant le faucet en limitant à une utilisation par jour.

Le Faucet continue jusqu’à ce qu’on atteigne la supply totale maximum d’un milliard de tokens, soit environ 3 ans pour un million d’adresses.

J’utilise pour cela une méthode canClaim qui vérifie si une adresse a le droit d’utiliser le Faucet.

Je ne me suis pas embêté et ai tapé ce code directement dans un nouveau fichier dans Remix, j’ai compilé et déployé sur Gobi et EON en utilisant Metamask comme environnement de déploiement. Clic droit sur le fichier source pour choisir Flatten et ainsi valider et publier le contrat sur les 2 blockchains testnet (gobi) et mainnet (eon).

La dApp

Autre composant indispensable pour développer une dApp sur Horizen EON, l’interface utilisateur.

On va construire la dApp étape par étape pour ne pas tout mélanger.

Application minimale et serveur de dév

On crée un nouveau répertoire thefaucetcoin, et, à l’intérieur, une nouvelle app ReactJS. Je n’utilise n’utilise pas create-wagmi afin que l’on ajoute nous mêmes les éléments un par un pour bien comprendre.

npx create-react-app .

Il faut ensuite ajouter wagmi et viem (et on ajoute typescript pour éviter des conflits avec une vieille dépendance de react-scripts, –force ou –legacy-peer-deps fonctionnent aussi. Il faudrait surtout que j’arrrête d’utiliser create-react-app) :

npm i wagmi viem typescript

On vide index.css et app.css et on repart avec un fichier App.js réduit à sa plus simple expression :

import './App.css';

function App() {
  return (
    <div className="App">
    </div>
  );
}

export default App;

On lance le serveur de développement avec npm start pour constater que l’on obtient une page blanche (tout ça pour ça …).

Ajout de gobi aux chains de wagmi

Pour l’instant wagmi n’intègre pas gobi et eon dans les chains préconfigurées. Il faut donc que l’on ajoute un fichier de configuration pour chacune.

Voyons comment faire avec gobi, par exemple. J’ai donc créé un sous-répertoire utils dans src et y ai créé le fichier gobi.ts avec le contenu suivant :

import { Chain } from 'wagmi'
 
export const gobi = {
  id: 1_663,
  name: 'Gobi',
  network: 'gobi',
  nativeCurrency: {
    decimals: 18,
    name: 'ZEN',
    symbol: 'ZEN',
  },
  rpcUrls: {
    public: { http: ['https://gobi-testnet.horizenlabs.io/ethv1'] },
    default: { http: ['https://gobi-testnet.horizenlabs.io/ethv1'] },
  },
  blockExplorers: {
    etherscan: { name: 'Gobi Explorer', url: 'https://gobi-explorer.horizen.io/' },
    default: { name: 'Gobi Explorer', url: 'https://gobi-explorer.horizen.io/' },
  },
  contracts: {
  },
  testnet: true,
} as const satisfies Chain

On l’importe en tête du fichier App.js :

import { gobi } from './utils/gobi.ts'

Configuration de wagmi

On peut ensuite passer à la configuration minimale de wagmi en appelant configureChains et createConfig et en wrappant notre app dans une balise WagmiConfig :

import { WagmiConfig, createConfig, configureChains } from 'wagmi'
import { publicProvider } from 'wagmi/providers/public'
import { gobi } from './utils/gobi.ts'
import './App.css'

function App() {
  const { publicClient } = configureChains(
    [gobi],
    [publicProvider()],
  )

  const config = createConfig({
    autoConnect: true,
    publicClient,
  })

  return (
    <WagmiConfig config={config}>
      <div className="App">
      </div>
    </WagmiConfig>
  );
}

export default App;

Connexion au wallet

A ce stade on a toujours une page blanche, c’est frustrant. Ajoutons un composant Profile pour gérer la connexion.

function Profile() {
  const { address, isConnected } = useAccount()
  const { connect } = useConnect({
    connector: new InjectedConnector(),
  })
 
  if (isConnected) {
    return <div>
        Connected to {address}
      </div>
  }

  return <button onClick={() => connect()}>Connect Wallet</button>
}

Wagmi fonctionne à l’aide de hooks. On utilise ici useAccount qui reflète l’état de connexion et l’adresse connectée, le cas échéant et useConnect, qui permet d’interagir avec un Connecteur, ici, celui intégré au Browser (metamask/Brave wallet/Rabby/…).

On doit charger ces éléments avant de s’en servir, on ajoute donc en tête de fichier :

import { useAccount, useConnect } from 'wagmi'
import { InjectedConnector } from 'wagmi/connectors/injected'

Enfin, on insère le composant dans le corps de l’App :

    <WagmiConfig config={config}>
      <div className="App">
        <Profile />
      </div>
    </WagmiConfig>

Et notre page n’est plus vide :

On veut Claim !!

Bien ! Maintenant qu’on sait se connecter, on va faire le composant Claim qui permettra d’utiliser le faucet à TFC.

On va commencer par récupérer l’abi du smart contract déployé. L’ABI est généré lors de la compilation du contrat. Je l’ai placée dans le répertoire utils, dans un fichier tfcAbi.json. Il faut donc l’importer dans l’App.

import tfcContractAbi from './utils/tfcAbi.json'

On doit ensuite récupérer l’adresse de notre contrat pour s’en servir plus tard.

const tfcContractAddress = "0xd38D3BFcc7c29765F5c7569DFB931d851E3c7844"

Enfin, on ajoute quelques imports supplémentaires qui seront nécessaires à ce nouveau composant.

import { useState } from 'react';
import { useContractRead } from 'wagmi'
import { useContractWrite, usePrepareContractWrite } from 'wagmi'

Ok, voyons le code du composant Claim :

function Claim(props) {
  const [walletCanClaim, setCanClaim] = useState(false);

  useContractRead({
    address: tfcContractAddress,
    abi: tfcContractAbi,
    functionName: 'canClaim',
    account: props.address,
    onSuccess(data) {
      setCanClaim(data);
    },
  })

  const { config } = usePrepareContractWrite({
    address: tfcContractAddress,
    abi: tfcContractAbi,
    functionName: 'useFaucet',
  })
  const { data, isLoading, isSuccess, write } = useContractWrite(config)
 
  return <div>
      <div>{ walletCanClaim ? "You can claim 1 $TFC" : "This wallet already claimed in the last 24h" }</div>
      <div>
        <button disabled={!walletCanClaim} onClick={() => write?.()}>
          Claim my daily $TFC
        </button>
      </div>
      {isLoading && <div>Check Wallet</div>}
      {isSuccess && <div>Success ! <a href={"https://gobi-explorer.horizen.io/tx/" + data.hash}>Check Transaction</a></div>}
  </div>
}

On déclare une variable d’état walletCanClaim avec son setter setCanClaim.

Vient ensuite l’appel de la fonction canClaim du contrat. On fournit l’adresse du contrat, son ABI, la fonction appelée. On prend garde d’appeler avec l’adresse connectée pour éviter les soucis de cache. Enfin, en cas de bon déroulement, on stocke la valeur de retour dans la variable walletCanClaim.

On prépare l’appel à la fonction activant le faucet useFaucet. Et on a tout ce qu’il faut pour générer notre composant :

  • Selon la valeur de walletCanClaim, on affiche un message pour indiquer l’éligibilité de ce wallet au drop quotidien.
  • On génère le bouton qui appele la fonction useFaucet mais, selon la valeur de walletCanClaim, on le désactive ou non.
  • Viennent ensuite 2 messages d’information affichés lorsque l’on attend une interaction de l’utilisateur dans son wallet et le message en cas de succès permettant l’affichage de la transaction dans l’explorer.

Le composant Claim est défini. On peut le charger dans Profile :

  if (isConnected) {
    return <div>
        Connected to {address}
        <Claim address={address} />
      </div>
  }

On a vu comment développer une dApp sur Horizen EON et En l’état, la dApp est fonctionnelle. Mais il lui reste plusieurs problèmes ou améliorations possibles, que l’on va aborder dans la suite de l’article.

Sélectionner le bon réseau

Un sélecteur de réseau est bien évidemment pratique, sinon indispensable, pour s’assurer que les requêtes ne partent pas sur d’autres réseaux que gobi ou eon !

On va placer cet élément dans le composant Profile afin de bloquer l’App si on se trouve sur un réseau différent.

Le sélecteur utilisera les 2 hooks suivnats : useNetwork et useSwitchNetwork. On les importe donc depuis wagmi :

import { useNetwork, useSwitchNetwork } from 'wagmi'

On peut ensuite modifier le composant Profile :

function Profile() {
  const { address, isConnected } = useAccount()
  const { connect } = useConnect({
    connector: new InjectedConnector(),
  })

  const { chain } = useNetwork()
  const { switchNetwork } = useSwitchNetwork(gobi.id);
 
  if (isConnected) {
    if (chain.id === gobi.id) {
      return <div>
        Connected to {address}
        <Claim address={address} />
      </div>
    } 

    return <button onClick={() => switchNetwork(gobi.id)}>Switch to Gobi Network</button>
  }

  return <button onClick={() => connect()}>Connect Wallet</button>
}

Et c’est tout. Une fois connecté, si vous n’êtes pas sur le bon réseau, un bouton vous propose de switcher. Et si vous quittez le réseau, l’interface de la dApp disparaît et réaffiche le bnouton de switch.

Utiliser un fournisseur de RPC : Ankr

On profite aussi de ce tutoriel pour explorer les outils pour le développement de dApp sur EON. On va utiliser Ankr pour l’interface RPC avec la blockchain.

Pourquoi ne pas utiliser les interfaces RPC publiques ? Parce que si une dApp génère trop de trafic sur les RPC publiques, il y a un risque qu’elle se retrouve rapidement limitée en nombre de requêtes acceptées, ce qui dégrade l’expérience utilisateur ou va même jusqu’à rendre inutilisable la dApp.

Accessoirement, les fournisseurs comme Ankr, Alchemy, ChainStack, Blockstream, etc. proposent des fonctionnalités supplémentaires et ne se limitent pas à la simple fourniture de d’interfaces RPC, notamment pour la manipulation de NFTs ou autres bonus.

On se rend sur le site d’Ankr pour y créer un compte gratuit. Ce dernier permet d’obtenir une adresse personnalisée pour l’interface et le suivi des requêtes qu’on y envoie.
— > https://www.ankr.com/rpc/horizen/

Une fois qu’on a notre endpoint personnalisé (on peut utiliser l’endpoint public d’Ankr pour les tests), on adapte la déclaration du client dans App().

  const { publicClient } = configureChains(
    [gobi],
    [
      jsonRpcProvider({
        rpc: () => { 
          return {
            http: "https://rpc.ankr.com/horizen_testnet_evm/14594...bbe3"
          };
        },
      }),
    ],
  )

A noter : Il est tout à fait possible de laisser en plus publicProvider(), qui pourra servir de solution de repli « au cas où ».

A l’usage, on ne voit pas la différence … C’est frustrant, mais c’est bien.

Multiplier les wallets : RainbowKit

Pour enfin faire quelque chose qui se voit et améliorer l’interface utilisateur, on va s’attaquer à la partie connector de wagmi et utiliser RainbowKit, souvent utilisé sur les dApps en alternative à Web3Modal ou d’autres solutions multiwallet.

On commence par ajouter la bibliothèque de code nécessaire au projet (ajout de typescript pour la même raison que plus haut, create-react-app n’est plus maintenu, il faut que je me mette à jour) :

npm install @rainbow-me/rainbowkit typescript

On a 2 imports à charger pour l’aspect de l’interface et les fonctionnalités et un de plus pour le bouton de connexion et son interface :

import '@rainbow-me/rainbowkit/styles.css'
import { getDefaultWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { ConnectButton } from '@rainbow-me/rainbowkit';

RainbowKit, comme Web3Modal, utilise WalletConnect. Vous devrez donc créer un compte et un projet sur cloud.walletconnect.com pour définir un nom et une ID de projet qui nous servira dans la configuration. Tout cela est rapide et gratuit.

Bien, éditons à présent notre App() :

function App() {
  const { chains, publicClient } = configureChains(
    [gobi],
    [
      jsonRpcProvider({
        rpc: () => { 
          return {
            http: "https://rpc.ankr.com/horizen_testnet_evm/1459...7bbe3"
          };
        },
      }),
      publicProvider(),
    ],
  )

  const { connectors } = getDefaultWallets({
    appName: 'TheFaucetCoin',
    projectId: '896cf........7ae4',
    chains
  });
  
  const config = createConfig({
    autoConnect: true,
    connectors,
    publicClient,
  })

  return (
    <WagmiConfig config={config}>
      <RainbowKitProvider chains={chains}>
        <div className="App">
          <Profile />
        </div>
      </RainbowKitProvider>
    </WagmiConfig>
  );
}
  • Ligne 2, on récupère chains en plus en retour de l’appel à configureChains, pour s’en servir plus tard.
  • Lignes 17 et 18, on initialise les connectors de RainbowKit avec le nom et l’ID récupérés sur notre projet déclaré sur cloud.walletconnect.com précédemment ainsi que la variable chains récupérée juste au dessus.
  • Ligne 24, on ajoute les connectors à la config wagmi
  • Lignes 30 et 34, on enserre notre App, dans le composant RainbowKitProvider.

Bon, après tout ça, on est prêts à remplacer notre bouton de connexion tout moche. On reprends le composant Profile :

function Profile() {
  const { address, isConnected } = useAccount()

  const { chain } = useNetwork()
  const { switchNetwork } = useSwitchNetwork(gobi.id);
 
  if (isConnected) {
    if (chain.id === gobi.id) {
      return <div>
        Connected to {address}
        <Claim address={address} />
      </div>
    } 

    return <button onClick={() => switchNetwork(gobi.id)}>Switch to Gobi Network</button>
  }

  return <ConnectButton />
}
  • Ligne 3, l’appel à useConnect disparaît. C’est désormais le boulot de RainbowKit.
  • Ligne 18, on supprime le précédent bouton de connexion pour y mettre celui de RainbowKit.
Ça a tout de suite plus de gueule !!

Bonus : RainbowKit peut gérer tout seul le changement de réseau !

Constantes de configuration

Afin de faciliter la maintenance et la portabilité du code, on va en sortir toutes les constantes de configuration, nom de projet, id walletconnect, rpc, etc.

On va pour cela créer un fichier .env contenant les variables qu’on souhaite « déporter » du code de l’app. Les variables peuvent avoir le nom que l’on souhaite, mais ce dernier doit commencer par REACT_APP_, sans quoi, il sera ignoré.

REACT_APP_TFC_CONTRACT_ADDRESS = "0xd38D3BFcc7c29765F5c7569DFB931d851E3c7844"
REACT_APP_BLOCK_EXPLORER_BASE_URL = "https://gobi-explorer.horizen.io/tx/"
REACT_APP_RPC_HTTP_URL = "https://rpc.ankr.com/horizen_testnet_evm/1459...7bbe3"
REACT_APP_WC_PROJECT_NAME = "TheFaucetCoin"
REACT_APP_WC_PROJECT_ID = "896c...7ae4"

On peut ensuite les utiliser dans le code en utilisant process.env.NOM_DE_VARIABLE :

const tfcContractAddress = process.env.REACT_APP_TFC_CONTRACT_ADDRESS
...
<a href={process.env.REACT_APP_BLOCK_EXPLORER_BASE_URL + data.hash}>Check Transaction</a>
...
  const { chains, publicClient } = configureChains(
    [gobi],
    [
      jsonRpcProvider({
        rpc: () => { 
          return {
            http: process.env.REACT_APP_RPC_HTTP_URL
          };
        },
      }),
      publicProvider(),
    ],
  )
...
  const { connectors } = getDefaultWallets({
    appName: process.env.REACT_APP_WC_PROJECT_NAME,
    projectId: process.env.REACT_APP_WC_PROJECT_ID,
    chains
  });
  ...

Pour que les modifications dans le fichier .env soient prises en compte, il faut relancer le serveur de développement.

Conclusion

Voila ce tutoriel « Développer une dApp sur Horizen EON » est maintenant terminé. Si tu as eu un souci à le suivre, tu peux me contacter sur twitter (euh X, pardon) et sur le Discord Horizen !

Liens