Créer des dApps pour les consommateurs sur Morph : Intégrer des données Offchain en utilisant les flux de prix Pyth (Pyth Price Feeds).

Pourquoi utiliser des oracles ?

Dans les applications décentralisées, les contrats intelligents (smart contract) sont conçus pour exécuter des transactions et des logiques de manière autonome. Cependant, leur fonctionnalité est intrinsèquement limitée aux données de la blockchain elle-même. Pour de nombreuses applications, c'est une contrainte significative. Pour créer une dApp véritablement efficace et interactive, les développeurs doivent souvent intégrer des informations du monde réel situées en dehors de la blockchain.

Prenons l'exemple d'une dApp conçue pour le trading financier. Imaginez que vous construisiez une plateforme de trading décentralisée où les utilisateurs peuvent effectuer des transactions basées sur les derniers prix des cryptomonnaies. Pour que la plateforme fonctionne correctement, elle a besoin de flux de prix en temps réel provenant de divers échanges. Sans accès à ces données offchain, votre contrat intelligent ne peut pas refléter correctement les conditions actuelles du marché, ce qui pourrait entraîner des transactions erronées et une mauvaise expérience utilisateur.

Ou encore, considérez une plateforme d'assurance décentralisée qui calcule les paiements en fonction des données météorologiques. Si vous proposez une assurance contre les catastrophes naturelles, votre contrat intelligent a besoin d'informations météorologiques à jour pour déterminer si une réclamation est valide. Sans données météorologiques fiables en dehors de la chaîne, le contrat intelligent ne peut pas prendre des décisions éclairées, ce qui pourrait entraîner des litiges et des échecs opérationnels.

C'est là que les oracles deviennent indispensables. Les oracles servent de ponts entre la blockchain et les sources de données externes, récupérant et transmettant des informations cruciales à vos contrats intelligents. Ils permettent à votre dApp de réagir aux événements du monde réel et de prendre des décisions basées sur des données précises et opportunes.

Dans ce tutoriel, nous vous guiderons à travers l'intégration des oracles Pyth à votre dApp basée sur Morph. Nous vous montrerons comment extraire des données du monde réel dans vos contrats intelligents, vous permettant ainsi de créer des applications non seulement fonctionnelles mais aussi réactives à la nature changeante des informations externes.

Objectives

À la fin de ce tutoriel, vous devriez être capable de :

  • Configurer l'environnement en utilisant le kit de démarrage Morph.

  • Installer les contrats Pyth.

  • Consommer un flux de prix Pyth dans votre contrat intelligent.

  • Déployer votre contrat intelligent sur Morph.

  • Interagir avec votre contrat intelligent depuis le frontend.

Configuration de votre environnement de développement

Avant de commencer à écrire des contrats intelligents et à interagir avec eux, nous devons configurer notre environnement de développement en utilisant le kit de démarrage Morph.

Tout d'abord, nous créons un nouveau répertoire pour notre projet. Dans votre terminal, exécutez la commande suivante :

mkdir myProject
cd myProject

puis la commande suivante:

npx @morphl2/create-morph-app@latest create
  • Ouvrez le kit téléchargé dans votre IDE (VS Code ou tout autre de votre choix) et dans le dossier contract, créez un fichier .env. Ici, nous allons ajouter nos variables d'environnement telles que RPC_URL et la clé privée pour nous permettre de déployer sur Morph. Votre fichier .env devrait ressembler à ceci :
PRIVATE_KEY= Votre-clé-privée
RPC_URL=https://rpc-quicknode-holesky.morphl2.io
  • Exécutez forge compile dans le répertoire contract pour télécharger les dépendances.

  • De même, dans le dossier frontend, créez un fichier .env et ajoutez votre projectId, qui correspond à votre ID wallet-connect.

NEXT_PUBLIC_PROJECT_ID= Votre-wallet-connect-id
  • Exécutez les commandes suivantes dans le répertoire frontend pour télécharger les dépendances et démarrer la dApp dans votre environnement local :
yarn
yarn dev

Écrire et Déployer Votre Smart Contract

Installer les contrats intelligents Pyth

Revenons dans notre dossier contract et exécutons la commande suivante pour télécharger les contrats Oracle Pyth en tant que dépendance :

forge install pyth-network/pyth-sdk-solidity@v2.2.0 --no-git --no-commit

Une fois installé, nous mettons à jour le fichier foundry.toml en ajoutant la ligne suivante :

remappings = ['@pythnetwork/pyth-sdk-solidity/=lib/pyth-sdk-solidity']

Écrire notre contrat intelligent

Une fois notre projet créé et toutes les dépendances installées, nous pouvons enfin commencer à écrire notre contrat intelligent.

  • Dans le dossier contract, naviguez jusqu’au dossier src et créez un nouveau fichier appelé wager.sol.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";

contract EthBettingDapp {
    IPyth public pyth;
    bytes32 public ethUsdPriceId;

    struct Bet {
        uint id;
        string title;
        uint threshold;
        uint totalPoolForExceed;
        uint totalPoolForNotExceed;
        bool epochEnded;
        address[] bettors;
        mapping(address => UserBet) userBets;
    }

    struct UserBet {
        uint amount;
        bool betForExceed;
    }

     struct BetInfo {
        uint id;
        string title;
        uint threshold;
        uint totalPoolForExceed;
        uint totalPoolForNotExceed;
        bool epochEnded;
    }



    uint public nextBetId;
    mapping(uint => Bet) public bets;
    
    event BetCreated(uint indexed betId, string title, uint threshold);
    event BetPlaced(uint indexed betId, address indexed user, uint amount, bool betForExceed);
    event EpochEnded(uint indexed betId, uint finalPrice, bool exceeded);
    event RewardTransferred(address indexed bettor, uint reward);

    /**
     * Network: Morph Holesky 
     * Address: 0x2880aB155794e7179c9eE2e38200202908C17B43
     */

    constructor() {
        pyth = IPyth(0x2880aB155794e7179c9eE2e38200202908C17B43);
        ethUsdPriceId = 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace;
    }

    modifier onlyBeforeEpochEnd(uint betId) {
        require(!bets[betId].epochEnded, "Betting period has ended");
        _;
    }

    modifier onlyAfterEpochEnd(uint betId) {
        require(bets[betId].epochEnded, "Epoch is still ongoing");
        _;
    }

    function createBet(string memory title, uint threshold) public {
        Bet storage newBet = bets[nextBetId];
        newBet.id = nextBetId;
        newBet.title = title;
        newBet.threshold = threshold * 10**18; // Set threshold with 18 decimals

        emit BetCreated(nextBetId, title, threshold);
        nextBetId++;
    }

    function placeBet(uint betId, bool _betForExceed) public payable onlyBeforeEpochEnd(betId) {
        require(msg.value > 0, "Bet amount must be greater than 0");

        Bet storage bet = bets[betId];

        if (bet.userBets[msg.sender].amount == 0) {
            bet.bettors.push(msg.sender);
        }

        bet.userBets[msg.sender].amount += msg.value;
        bet.userBets[msg.sender].betForExceed = _betForExceed;
        if (_betForExceed) {
            bet.totalPoolForExceed += msg.value;
        } else {
            bet.totalPoolForNotExceed += msg.value;
        }

        emit BetPlaced(betId, msg.sender, msg.value, _betForExceed);
    }

    function endEpoch(uint betId, bytes[] calldata pythPriceUpdate) public payable onlyBeforeEpochEnd(betId) {
        Bet storage bet = bets[betId];
        require(!bet.epochEnded, "Epoch already ended");

        uint updateFee = pyth.getUpdateFee(pythPriceUpdate);
        pyth.updatePriceFeeds{value: updateFee}(pythPriceUpdate);

        PythStructs.Price memory price = pyth.getPrice(ethUsdPriceId);
        uint ethPrice18Decimals = (uint(uint64(price.price)) * (10 ** 18)) / (10 ** uint8(uint32(-1 * price.expo)));

        bool priceExceeded = ethPrice18Decimals > bet.threshold;

        distributeRewards(betId, priceExceeded);
        bet.epochEnded = true;

        emit EpochEnded(betId, ethPrice18Decimals, priceExceeded);
    }

    function distributeRewards(uint betId, bool priceExceeded) private {
        Bet storage bet = bets[betId];
        uint winnersTotalBet = priceExceeded ? bet.totalPoolForExceed : bet.totalPoolForNotExceed;
        if (winnersTotalBet == 0) return; // No winners

        uint losersTotalBet = priceExceeded ? bet.totalPoolForNotExceed : bet.totalPoolForExceed;
        uint totalPool = winnersTotalBet + losersTotalBet;

        for (uint i = 0; i < bet.bettors.length; i++) {
            address bettor = bet.bettors[i];
            if (bet.userBets[bettor].betForExceed == priceExceeded) {
                uint reward = (bet.userBets[bettor].amount * totalPool) / winnersTotalBet;
                (bool success, ) = payable(bettor).call{value: reward}("");
                require(success, "Transfer failed");
                emit RewardTransferred(bettor, reward);
            }
        }
    }

     function getAllBets() public view returns (BetInfo[] memory) {
        BetInfo[] memory allBets = new BetInfo[](nextBetId);
        for (uint i = 0; i < nextBetId; i++) {
            Bet storage bet = bets[i];
            allBets[i] = BetInfo({
                id: bet.id,
                title: bet.title,
                threshold: bet.threshold,
                totalPoolForExceed: bet.totalPoolForExceed,
                totalPoolForNotExceed: bet.totalPoolForNotExceed,
                epochEnded: bet.epochEnded
            });
        }
        return allBets;
     }
    

    function getBetAmount(uint betId, address user) external view returns (uint) {
        return bets[betId].userBets[user].amount;
    }

    function getBetPosition(uint betId, address user) external view returns (bool) {
        return bets[betId].userBets[user].betForExceed;
    }

    function getTotalPoolForExceed(uint betId) external view returns (uint) {
        return bets[betId].totalPoolForExceed;
    }

    function getTotalPoolForNotExceed(uint betId) external view returns (uint) {
        return bets[betId].totalPoolForNotExceed;
    }

    function getBettors(uint betId) external view returns (address[] memory) {
        return bets[betId].bettors;
    }
}

Explication du contrat

Le contrat wager utilise l'interface Pyth du SDK Solidity de Pyth. Le contrat permet à un utilisateur de parier sur l’évolution du prix de l'ETH sur une période donnée. Il permet aux utilisateurs de créer un pari et de miser de l'ETH sur son issue. Pour simplifier, le contrat dispose également d'une fonction endEpoch qui nous permet de terminer manuellement l'Epoch (la durée du pari), mettant ainsi fin au pari. Dans le constructeur, l'interface Pyth attend une adresse de contrat, qui dans ce cas est l'adresse du contrat Pyth pour le réseau de test Morph Holesky.

function endEpoch(uint betId, bytes[] calldata pythPriceUpdate) public payable onlyBeforeEpochEnd(betId) {
        Bet storage bet = bets[betId];
        require(!bet.epochEnded, "Epoch already ended");

        uint updateFee = pyth.getUpdateFee(pythPriceUpdate);
        pyth.updatePriceFeeds{value: updateFee}(pythPriceUpdate);

        PythStructs.Price memory price = pyth.getPrice(ethUsdPriceId);
        uint ethPrice18Decimals = (uint(uint64(price.price)) * (10 ** 18)) / (10 ** uint8(uint32(-1 * price.expo)));

        bool priceExceeded = ethPrice18Decimals > bet.threshold;

        distributeRewards(betId, priceExceeded);
        bet.epochEnded = true;

        emit EpochEnded(betId, ethPrice18Decimals, priceExceeded);
    }

Cette fonction a deux objectifs :

  1. Met fin manuellement au pari.

  2. Interagit avec les contrats Pyth.

La fonction prend en argument pthPriceUpdate, qui est la donnée de mise à jour de prix nécessaire pour obtenir des données de prix actualisées. C’est une mise à jour de prix signée, diffusée hors chaîne à partir de Pyth.

"Pyth Network utilise un système de mise à jour de prix à la demande où les utilisateurs récupèrent les prix on-chain uniquement lorsque cela est nécessaire. "

La première fonction Pyth que nous appelons est pyth.getUpdateFee. Il s'agit des frais facturés par Pyth pour mettre à jour le prix. Ensuite, nous appelons la fonction update price feeds qui met à jour le prix et paie les frais que nous avons calculés dans la fonction précédente (getUpdateFee).

Pour récupérer le prix mis à jour, nous appelons la fonction pyth.getPrice(priceFeedId). Elle prend comme argument un Id, qui dans notre cas est l'Id du flux de prix ETH/USD.

(Vous devriez supprimer src/Counter.sol, test/Counter.t.sol, et script/Counter.s.sol qui ont été générés avec le kit de démarrage.)

Déployer votre contrat

Le déploiement de notre contrat sur le réseau de test Morph Holesky est simple. Tout d'abord, nous compilons notre contrat intelligent en exécutant :

forge build

Ensuite, nous créons un fichier dans le dossier script appelé Deployer.s.sol. C’est ici que nous écrirons notre script de déploiement. Collez dans le fichier le code suivant :

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console2} from "forge-std/Script.sol";
import {EthBettingDapp} from "../src/wager.sol";



contract DeployerScript is Script {
    function setUp() public {}

   function run() public returns (EthBettingDapp)  {
        vm.startBroadcast();
        EthBettingDapp app = new EthBettingDapp();

        vm.stopBroadcast();

         return app;

    }
}

Ensuite, exécutez :

source .env

Cela permet de provisionner vos variables d'environnement (clé privée & URL RPC) dans votre terminal.

Pour déployer notre contrat, exécutez la commande suivante :

forge script script/Deployer.s.sol --rpc-url $RPC_URL --broadcast --legacy --private-key $PRIVATE_KEY

Et voilà ! Notre contrat est déployé sur le réseau de test Morph Holesky.

Démarrage du frontend

Préparer notre environnement frontend

Pour interagir avec notre contrat intelligent à partir du frontend, suivez ces étapes :

  1. Naviguez vers le dossier out dans le répertoire contract et localisez le dossier wager.sol.

  2. Copiez le contenu du fichier JSON trouvé à cet emplacement et collez-le dans le fichier src/constants/abi.ts dans le répertoire frontend. Assurez-vous de structurer l'ABI comme un module React et de supprimer les détails du bytecode.

  3. Copiez l'ABI pour notre projet .

export const wagerAbi = [
  { type: "constructor", inputs: [], stateMutability: "nonpayable" },
  {
    type: "function",
    name: "bets",
    inputs: [{ name: "", type: "uint256", internalType: "uint256" }],
    outputs: [
      { name: "id", type: "uint256", internalType: "uint256" },
      { name: "title", type: "string", internalType: "string" },
      { name: "threshold", type: "uint256", internalType: "uint256" },
      {
        name: "totalPoolForExceed",
        type: "uint256",
        internalType: "uint256",
      },
      {
        name: "totalPoolForNotExceed",
        type: "uint256",
        internalType: "uint256",
      },
      { name: "epochEnded", type: "bool", internalType: "bool" },
    ],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "createBet",
    inputs: [
      { name: "title", type: "string", internalType: "string" },
      { name: "threshold", type: "uint256", internalType: "uint256" },
    ],
    outputs: [],
    stateMutability: "nonpayable",
  },
  {
    type: "function",
    name: "endEpoch",
    inputs: [
      { name: "betId", type: "uint256", internalType: "uint256" },
      { name: "pythPriceUpdate", type: "bytes[]", internalType: "bytes[]" },
    ],
    outputs: [],
    stateMutability: "payable",
  },
  {
    type: "function",
    name: "ethUsdPriceId",
    inputs: [],
    outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "getAllBets",
    inputs: [],
    outputs: [
      {
        name: "",
        type: "tuple[]",
        internalType: "struct EthBettingDapp.BetInfo[]",
        components: [
          { name: "id", type: "uint256", internalType: "uint256" },
          { name: "title", type: "string", internalType: "string" },
          { name: "threshold", type: "uint256", internalType: "uint256" },
          {
            name: "totalPoolForExceed",
            type: "uint256",
            internalType: "uint256",
          },
          {
            name: "totalPoolForNotExceed",
            type: "uint256",
            internalType: "uint256",
          },
          { name: "epochEnded", type: "bool", internalType: "bool" },
        ],
      },
    ],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "getBetAmount",
    inputs: [
      { name: "betId", type: "uint256", internalType: "uint256" },
      { name: "user", type: "address", internalType: "address" },
    ],
    outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "getBetPosition",
    inputs: [
      { name: "betId", type: "uint256", internalType: "uint256" },
      { name: "user", type: "address", internalType: "address" },
    ],
    outputs: [{ name: "", type: "bool", internalType: "bool" }],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "getBettors",
    inputs: [{ name: "betId", type: "uint256", internalType: "uint256" }],
    outputs: [{ name: "", type: "address[]", internalType: "address[]" }],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "getTotalPoolForExceed",
    inputs: [{ name: "betId", type: "uint256", internalType: "uint256" }],
    outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "getTotalPoolForNotExceed",
    inputs: [{ name: "betId", type: "uint256", internalType: "uint256" }],
    outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "nextBetId",
    inputs: [],
    outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
    stateMutability: "view",
  },
  {
    type: "function",
    name: "placeBet",
    inputs: [
      { name: "betId", type: "uint256", internalType: "uint256" },
      { name: "_betForExceed", type: "bool", internalType: "bool" },
    ],
    outputs: [],
    stateMutability: "payable",
  },
  {
    type: "function",
    name: "pyth",
    inputs: [],
    outputs: [{ name: "", type: "address", internalType: "contract IPyth" }],
    stateMutability: "view",
  },
  {
    type: "event",
    name: "BetCreated",
    inputs: [
      {
        name: "betId",
        type: "uint256",
        indexed: true,
        internalType: "uint256",
      },
      {
        name: "title",
        type: "string",
        indexed: false,
        internalType: "string",
      },
      {
        name: "threshold",
        type: "uint256",
        indexed: false,
        internalType: "uint256",
      },
    ],
    anonymous: false,
  },
  {
    type: "event",
    name: "BetPlaced",
    inputs: [
      {
        name: "betId",
        type: "uint256",
        indexed: true,
        internalType: "uint256",
      },
      {
        name: "user",
        type: "address",
        indexed: true,
        internalType: "address",
      },
      {
        name: "amount",
        type: "uint256",
        indexed: false,
        internalType: "uint256",
      },
      {
        name: "betForExceed",
        type: "bool",
        indexed: false,
        internalType: "bool",
      },
    ],
    anonymous: false,
  },
  {
    type: "event",
    name: "EpochEnded",
    inputs: [
      {
        name: "betId",
        type: "uint256",
        indexed: true,
        internalType: "uint256",
      },
      {
        name: "finalPrice",
        type: "uint256",
        indexed: false,
        internalType: "uint256",
      },
      {
        name: "exceeded",
        type: "bool",
        indexed: false,
        internalType: "bool",
      },
    ],
    anonymous: false,
  },
  {
    type: "event",
    name: "RewardTransferred",
    inputs: [
      {
        name: "bettor",
        type: "address",
        indexed: true,
        internalType: "address",
      },
      {
        name: "reward",
        type: "uint256",
        indexed: false,
        internalType: "uint256",
      },
    ],
    anonymous: false,
  },
];

4. Copiez l'adresse de notre contrat déployé et collez-la dans le fichier index.ts dans le dossier constants.

Interagir avec notre contrat

Dans notre dossier frontend, naviguez jusqu’à src/app/page.tsx. Il s’agit de la page d'accueil de notre dApp, où se déroulera la plupart de la logique.

page.tsx

Ouvrez le fichier page.tsx et collez-y ce code.

"use client";
import { useEffect } from "react";
import { wagerAbi, wagerAddress } from "@/constants";
import { toast } from "sonner";
import { parseEther } from "viem";
import { EvmPriceServiceConnection } from "@pythnetwork/pyth-evm-js";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
  useWaitForTransactionReceipt,
  useWriteContract,
  useReadContract,
} from "wagmi";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import BetCard from "@/components/BetCard";

interface BetInfo {
  id: bigint;
  title: string;
  threshold: bigint;
  totalPoolForExceed: bigint;
  totalPoolForNotExceed: bigint;
  epochEnded: boolean;
}

export default function Home() {
  const formSchema = z.object({
    title: z.string(),
    threshold: z.string(),
  });

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: "",
      threshold: "",
    },
  });

  const {
    data: hash,
    error,
    isPending,
    writeContractAsync,
  } = useWriteContract();

  const { isLoading: isConfirming, isSuccess: isConfirmed } =
    useWaitForTransactionReceipt({
      hash,
    });

  useEffect(() => {
    if (isPending) {
      toast.loading("Transaction Pending");
    }
    if (isConfirmed) {
      toast.success("Transaction Successful", {
        action: {
          label: "View on Etherscan",
          onClick: () => {
            window.open(`https://explorer-holesky.morphl2.io/tx/${hash}`);
          },
        },
      });
    }
    if (error) {
      toast.error("Transaction Failed");
    }
  }, [isConfirming, isConfirmed, error, hash]);

  const { data: allBets } = useReadContract({
    abi: wagerAbi,
    address: wagerAddress,
    functionName: "getAllBets",
  }) as { data: BetInfo[] | undefined };

  const createBet = async (data: z.infer<typeof formSchema>) => {
    try {
      const createBetTx = await writeContractAsync({
        address: wagerAddress,
        abi: wagerAbi,
        functionName: "createBet",
        args: [data.title, data.threshold],
      });

      console.log("created wager hash:", createBetTx);
      toast.success("Transaction Successful", {
        action: {
          label: "View on Etherscan",
          onClick: () => {
            window.open(
              `https://explorer-holesky.morphl2.io/tx/${createBetTx}`
            );
          },
        },
      });
    } catch (err: any) {
      toast.error("Transaction Failed: " + err.message);
      console.log("Transaction Failed: " + err.message);
    }
  };

  const placeBet = async (
    betId: number,
    betForExceed: boolean,
    betAmount: string
  ) => {
    try {
      console.log(betId, betForExceed, betAmount, "yess");
      const bet = parseEther(betAmount);
      const placeBetTx = await writeContractAsync({
        address: wagerAddress,
        abi: wagerAbi,
        functionName: "placeBet",
        args: [betId, betForExceed],
        value: bet,
      });

      console.log("Bet placed hash:", placeBetTx);
    } catch (err: any) {
      toast.error("Transaction Failed: " + err.message);
      console.log("Transaction Failed: " + err.message);
    }
  };

  const endEpoch = async (betId: number) => {
    const connection = new EvmPriceServiceConnection(
      "https://hermes.pyth.network"
    );

    const priceIds = [
      "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace",
    ];

    const priceFeedUpdateData = await connection.getPriceFeedsUpdateData(
      priceIds
    );

    try {
      const feeAmount = parseEther("0.01");
      const endEpochTx = await writeContractAsync({
        address: wagerAddress,
        abi: wagerAbi,
        functionName: "endEpoch",
        args: [betId, priceFeedUpdateData as any],
        value: feeAmount,
      });

      console.log("end of epoch hash:", endEpochTx);
    } catch (err: any) {
      toast.error("Transaction Failed: " + err.message);
      console.log("Transaction Failed: " + err.message);
      console.log(err);
    }
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen  p-4">
      <h1 className="text-2xl font-bold mb-4">ETH Betting Dapp</h1>

      {/* Create Bet Form */}

      <div className=" flex flex-col w-full max-w-3xl  my-8   bg-white/10 backdrop-blur-md rounded-lg border border-white/20 p-4">
        <h2 className="text-xl font-semibold mb-2">Create a New Bet</h2>

        <Form {...form}>
          <form onSubmit={form.handleSubmit(createBet)} className="space-y-8">
            <FormField
              control={form.control}
              name="title"
              render={({ field }) => (
                <FormItem>
                  <FormLabel className="">
                    <h1 className="text-[#32393A]">Wager title</h1>
                  </FormLabel>
                  <FormControl>
                    <Input
                      className="rounded-full"
                      placeholder="abc.."
                      {...field}
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="threshold"
              render={({ field }) => (
                <FormItem>
                  <FormLabel className="">
                    <h1 className="text-[#32393A]">Threshold</h1>
                  </FormLabel>
                  <FormControl>
                    <Input
                      className="rounded-full"
                      placeholder="xyz"
                      {...field}
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <Button
              className="bg-[#007A86] self-center my-8 rounded-full w-full"
              size="lg"
              disabled={isConfirming}
              type="submit"
            >
              {isConfirming ? "Creating a wager..." : "Create a wager"}
            </Button>
          </form>
        </Form>
      </div>

      <div className="mb-4">
        <h2 className="text-xl font-semibold">All Bets</h2>
        {allBets ? (
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
            {allBets.map((bet) => (
              <BetCard
                bet={bet}
                key={bet.id.toString()}
                onPlaceBet={placeBet}
                onEndEpoch={endEpoch}
              />
            ))}
          </div>
        ) : (
          <p>Loading bets...</p>
        )}
      </div>
    </div>
  );
}

Nous allons passer en revue les fonctions principales contenues dans ce fichier.

Hook : useReadContract :

Ce hook provient de la bibliothèque de hooks React wagmi pour Ethereum. Il nous permet de lire des données à partir de contrats intelligents.

const { data: allBets } = useReadContract({ abi: wagerAbi, address: wagerAddress, functionName: "getAllBets", }) as { data: BetInfo[] | undefined };

Explication :

  • Objectif : Lire la liste de tous les paris depuis le contrat intelligent.

  • Sortie : Stocke le résultat dans la variable allBets.

  • Processus :

    1. Utilise le hook useReadContract de wagmi pour interagir avec le contrat intelligent.

    2. Appelle la fonction getAllBets définie dans l'ABI du contrat à l’adresse wagerAddress.

    3.Stocke les informations sur les paris récupérées dans la variable allBets, qui peut être de type BetInfo[] ou undefined.

Fonction : createBet :

Cette fonction gère la création de nouveaux paris.

const createBet = async (data: z.infer<typeof formSchema>) => {
    try {
      const createBetTx = await writeContractAsync({
        address: wagerAddress,
        abi: wagerAbi,
        functionName: "createBet",
        args: [data.title, data.threshold],
      });

      console.log("created wager hash:", createBetTx);
      toast.success("Transaction Successful", {
        action: {
          label: "View on Etherscan",
          onClick: () => {
            window.open(
              `https://explorer-holesky.morphl2.io/tx/${createBetTx}`
            );
          },
        },
      });
    } catch (err: any) {
      toast.error("Transaction Failed: " + err.message);
      console.log("Transaction Failed: " + err.message);
    }
  };

Explication :

  • Entrée : Prend des données contenant le titre et le seuil du pari.

  • Processus :

    • Utilise le hook writeContractAsync pour interagir avec le contrat. Le hook est extrait du hook useWriteContract fourni par wagmi.

    • Appelle la fonction createBet dans notre contrat tout en prenant l'ABI et l’adresse du contrat en paramètres. Si la transaction réussit, il enregistre le hash de la transaction.

    • Affiche une notification toast de succès avec un lien pour consulter la transaction sur Etherscan.

  • Gestion des erreurs : Capture et enregistre les erreurs, et affiche une notification toast d'erreur.

Fonction : placeBet

Comme son nom l'indique, cette fonction permet aux utilisateurs de placer des paris en misant leur ETH dans le contrat.

const placeBet = async (
    betId: number,
    betForExceed: boolean,
    betAmount: string
  ) => {
    try {
      const bet = parseEther(betAmount);
      const placeBetTx = await writeContractAsync({
        address: wagerAddress,
        abi: wagerAbi,
        functionName: "placeBet",
        args: [betId, betForExceed],
        value: bet,
      });

      console.log("Bet placed hash:", placeBetTx);
    } catch (err: any) {
      toast.error("Transaction Failed: " + err.message);
      console.log("Transaction Failed: " + err.message);
    }
  };

Explication :

  • Entrée : Prend betId, betForExceed (un booléen indiquant la direction du pari) et betAmount.

  • Processus :

    • Convertit betAmount en Ether à l'aide de parseEther.

    • Appelle writeContractAsync pour interagir avec le contrat intelligent, invoquant spécifiquement la fonction placeBet avec betId et betForExceed comme arguments, et en envoyant betAmount comme valeur.

    • Enregistre le hash de la transaction si elle réussit.

  • Gestion des erreurs : Capture et enregistre les erreurs, et affiche une notification toast d'erreur.

Fonction : endEpoch

Cette fonction met fin manuellement à l'epoch ou à la durée du pari. Elle a été créée à des fins de démonstration.

const endEpoch = async (betId: number) => {
    const connection = new EvmPriceServiceConnection(
      "https://hermes.pyth.network"
    );

    const priceIds = [
      "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace",
    ];

    const priceFeedUpdateData = await connection.getPriceFeedsUpdateData(
      priceIds
    );

    try {
      const feeAmount = parseEther("0.01");
      const endEpochTx = await writeContractAsync({
        address: wagerAddress,
        abi: wagerAbi,
        functionName: "endEpoch",
        args: [betId, priceFeedUpdateData as any],
        value: feeAmount,
      });

      console.log("end of epoch hash:", endEpochTx);
    } catch (err: any) {
      toast.error("Transaction Failed: " + err.message);
      console.log("Transaction Failed: " + err.message);
    }
  };

Explication :

  • Entrée : Prend betId.

  • Processus :

    • Établit une connexion avec le service de prix du réseau Pyth.

    • Définit priceIds pour récupérer les mises à jour des prix.

    • Récupère les données de mise à jour du flux de prix à partir du réseau Pyth.

    • Convertit un montant de frais fixe (0,01 Ether) à l'aide de parseEther.

    • Appelle writeContractAsync pour interagir avec le contrat intelligent, invoquant spécifiquement la fonction endEpoch avec betId et priceFeedUpdateData comme arguments, et en envoyant feeAmount comme valeur.

    • Enregistre le hash de la transaction si elle réussit.

  • Gestion des erreurs : Capture et enregistre les erreurs, et affiche une notification toast d'erreur.

BetCard.tsx

Cette carte affiche toutes les informations pertinentes sur notre pari et nous permet également de parier ou de mettre fin aux epochs. Naviguez vers le dossier src/components, créez un fichier nommé BetCard.tsx et collez-y ce code.

import { formatEther } from "viem";
import { Button } from "./ui/button";
import PlaceBetModal from "./BetModal";

interface BetProps {
  bet: {
    id: bigint;
    title: string;
    threshold: bigint;
    totalPoolForExceed: bigint;
    totalPoolForNotExceed: bigint;
    epochEnded: boolean;
  };

  onPlaceBet: (betId: number, betForExceed: boolean, amount: string) => void;
  onEndEpoch: (betId: number) => void;
}

const BetCard: React.FC<BetProps> = ({ bet, onPlaceBet, onEndEpoch }) => {
  return (
    <div className="border border-gray-600 p-6 rounded-lg shadow-lg bg-white/5 backdrop-blur-md mt-8">
      <div className="">
        <h3 className="text-xl font-bold text-gray-100 mb-2">{bet.title}</h3>
        <div className="text-gray-500 mb-2">
          <p className="">ID: {bet.id.toString()}</p>
          <p className="">Threshold: {formatEther(bet.threshold)} ETH</p>
          <p className="">
            Pool for Exceed: {formatEther(bet.totalPoolForExceed)} ETH
          </p>
          <p className="">
            Pool for Not Exceed: {formatEther(bet.totalPoolForNotExceed)} ETH
          </p>
        </div>
        <p
          className={`text-sm font-semibold ${
            bet.epochEnded ? "text-red-500" : "text-green-500"
          }`}
        >
          Status: {bet.epochEnded ? "Ended" : "Active"}
        </p>
      </div>

      <div className="mt-4 flex space-x-4">
        <PlaceBetModal
          betId={Number(bet.id.toString())}
          onPlaceBet={onPlaceBet}
        >
          <Button
            onClick={() => {}}
            className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600"
          >
            Place Bet
          </Button>
        </PlaceBetModal>

        <Button
          onClick={() => onEndEpoch(Number(bet.id.toString()))}
          className="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600"
        >
          End Epoch
        </Button>
      </div>
    </div>
  );
};

export default BetCard;

BetModal.tsx

Ce modal nous permet de placer des paris et également de stipuler la quantité d'ETH que nous misons avec notre pari.

Dans le dossier src/components, créez un fichier nommé BetModal.tsx et collez-y ce code.

"use client";
import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";

import { toast } from "sonner";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { MoveLeft } from "lucide-react";

interface BetModalProps {
  children: React.ReactNode;
  onPlaceBet: (betId: number, betForExceed: boolean, amount: string) => void;
  betId: number;
}
const PlaceBetModal = ({ children, onPlaceBet, betId }: BetModalProps) => {
  const formSchema = z.object({
    amount: z.string(),
    betForExceed: z.boolean(),
  });

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      amount: "",
      betForExceed: false,
    },
  });

  const handleSubmit = async (data: z.infer<typeof formSchema>) => {
    console.log(data, betId);
    try {
      onPlaceBet(betId, data.betForExceed, data.amount);
    } catch (err: any) {
      toast.error("Transaction Failed: " + err.message);
      console.log("Transaction Failed: " + err.message);
    }
  };

  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>
            <div className="flex items-center gap-6 justify-center">
              <AlertDialogCancel className="border-none">
                <MoveLeft size={24} />
              </AlertDialogCancel>
              <h1>Make a bet</h1>
            </div>
          </AlertDialogTitle>
        </AlertDialogHeader>
        <div>
          <Form {...form}>
            <form
              onSubmit={form.handleSubmit(handleSubmit)}
              className="space-y-8"
            >
              <FormField
                control={form.control}
                name="amount"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel className="">
                      <h1 className="text-[#32393A]">Amount</h1>
                    </FormLabel>
                    <FormControl>
                      <Input
                        className="rounded-full"
                        placeholder="Bet amount (ETH)"
                        {...field}
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="betForExceed"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>
                      <h1 className="text-[#32393A]">Bet</h1>
                    </FormLabel>
                    <Select
                      onValueChange={(value) =>
                        field.onChange(value === "true")
                      }
                    >
                      <FormControl>
                        <SelectTrigger>
                          <SelectValue placeholder="will exceed" />
                        </SelectTrigger>
                      </FormControl>
                      <SelectContent>
                        <SelectItem value="true">true</SelectItem>
                        <SelectItem value="false">false</SelectItem>
                      </SelectContent>
                    </Select>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <Button
                className="bg-[#007A86] self-center my-8 rounded-full w-full"
                size="lg"
                type="submit"
              >
                Submit
              </Button>
            </form>
          </Form>
        </div>
        <AlertDialogFooter className="mt-4">
          <AlertDialogCancel>Cancel</AlertDialogCancel>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
};

export default PlaceBetModal;

Conclusion

Dans ce guide, nous avons vu comment créer une dApp sur Morph en utilisant des données offchain provenant du réseau Pyth. Nous avons exploré comment récupérer des données à la demande depuis des flux de prix et comment interagir avec ces données dans le frontend.

Subscribe to Daniel kambale
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.