Graph
Graph 是区块链的一个去中心化的查询协议和索引服务。它允许开发人员轻松地跟踪各种网络上的智能合约所发出的事件,并编写定制的数据转换脚本,这些脚本是实时运行的。这些数据也通过一个简单的 GraphQL API 提供,然后开发人员可以用它来在他们的前端显示东西。
准备
- 我们将使用 yarn,它是一个像 npm 一样的包管理器。
 - 如果你的电脑没有安装 yarn,请从这里安装 yarn。
 - 请观看这个 40 分钟的 GraphQL 教程。
 - 如果你不知道 axios 是什么,请看这个简短的教程
 - 你应该已经完成了 Chainlink VRF 的教程
 
如何工作
- 一个 DApp 发送一个交易,一些数据被存储在智能合约中。2/ 这个智能合约然后发出一个或多个事件。
 - Graph 的节点不断扫描 Ethereum 的新区块和这些区块可能包含的你的子图的数据。
 - 如果节点找到你要找的并在你的子图中定义的事件,它就会运行你定义的数据转换脚本(映射)。映射是一个 WASM(Web assembly)模块,它在 Graph 节点上创建或更新数据 Entities 以响应事件。
 - 我们可以使用 GraphQL 端点来查询 Graph 的节点上的这些数据
 
(参考图表的网站)
构建
我们将使用你在 Chainlink VRF 教程中创建的名为 RandomWinnerGame 的文件夹。
在你的 RandomWinnerGame 文件夹内创建一个 abi.json 文件(你将需要这个文件来初始化你的图)并复制以下内容。
请注意,这是你在 Chainlink VRF 教程中创建的 RandomWinnerGame 合约的 ABI。
所以你的最终文件夹结构应该是这样的。
RandomWinnerGame
    - Hardhat-tutorial
    - abi.json
要创建你的子图,你将需要去 The Graph 的托管服务
使用你的 GitHub 账户登录,并访问 "我的仪表盘 "标签
点击添加子图,填写信息并创建子图
当你的子图被创建后,它将显示一些安装、启动和部署的命令
在你的终端执行这个命令,指向 RandomWinnerGame 文件夹。
yarn global add @graphprotocol/graph-cli
# 或 npm install -g @graphprotocol/graph-cli
之后执行这个命令,但用你的 Github 用户名替换 GITHUB_USERNAME,用你在 Chainlink VRF 教程中部署的 RandomWinnerGame 合约的地址替换 YOUR_RANDOM_WINNER_GAME_CONTRACT_ADDRESS。
graph init --contract-name RandomWinnerGame \
  --product hosted-service GITHUB_USERNAME/Learnweb3 \
  --from-contract YOUR_RANDOM_WINNER_GAME_CONTRACT_ADDRESS \
  --abi ./abi.json \
  --network mumbai graph
对于部署密钥,进入 The Graph 的托管服务,点击我的仪表板,复制访问令牌并粘贴它作为部署密钥
图形认证
现在要执行的最后两个命令是。
cd graph
yarn deploy
# 或`npm run deploy`
你可以回到 The Graph 的托管服务,点击 My Dashboard,你就可以看到你的图了,它已经部署好了。
你已经部署了第一个图。
现在,有趣的部分来了,我们将把 The Graph 提供给我们的默认代码修改为可以帮助我们跟踪合约事件的代码。
让我们开始吧
打开 graph 文件夹中的 subgraph.yaml,在 abi.RamdomWinnerGame 一行之后添加一个 startBlock 到该 yaml 文件中。RamdomWinnerGame 一行,为了获得 startBlock,你需要到 Mumbai PolygonScan 中搜索你的合约地址,然后你需要复制你的合约所在区块的编号。
开始区块没有默认设置,但是因为我们知道我们只需要跟踪合约部署区块的事件,所以我们不需要同步整个区块链,只需要同步合约部署后的部分来跟踪事件。
source:
  address: "0x889Ef69261272Caa27f0655D0208bAc7055EDAD5"
  abi: RandomWinnerGame
  startBlock: BLOCK_NUMBER
你的最终文件应该看起来像这样
好了,现在是时候创建一些实体了。实体是定义你的数据如何存储在图的节点上的结构的对象。如果你想阅读更多关于它们的信息, 请点击这个链接
我们将需要一个实体,它可以涵盖我们事件中的所有变量,这样我们就可以跟踪所有的变量。打开 schema.graphql 文件,用以下几行代码替换已有的代码。
type Game @entity {
  id: ID!
  maxPlayers: Int!
  entryFee: BigInt!
  winner: Bytes
  requestId: Bytes
  players: [Bytes!]!
}
- ID 是一个游戏的唯一标识符,将等同于我们在合约中的 gameId 变量。
 - maxPlayers 将记录这个游戏中允许有多少最大的玩家。
 - entryFee 是进入游戏的费用,它是一个 BigInt,因为在我们的合约中, entryFee 是一个 uint256,是一个 BigNumber。
 - winner 是游戏中赢家的地址,定义为 Bytes,因为地址是一个十六进制的字符串。
 - requestId 也是一个十六进制的字符串,因此被定义为 Bytes。
 - players 是游戏中玩家的地址列表,因为每个地址都是十六进制字符串,所以我们把 player 定义为 Bytes 数组。
 - 另外,请注意!表示所需的变量,我们将 maxPlayers、entryFee、player 和 id 标记为所需,因为当游戏最初启动时,它将发出 GameStarted 事件,该事件将发出这三个变量(maxPlayers、entryFee 和 id),所以没有这三个变量,游戏实体永远无法创建,对于 player 阵列,它将被我们初始化为一个空阵列。
 - winner 和 requestId 将与 GameEnded 事件一起出现,而 player 将跟踪每一个由 PlayerJoined 事件发出的玩家地址。
 
如果你想了解更多关于这些类型的信息,你可以访问这个链接
好了,现在我们已经让图表知道了我们将追踪什么样的数据,以及它将包含什么 。
现在是时候查询这些数据了
图形有一个惊人的功能,给定的实体可以为你自动生成大块的代码。
这不是很神奇吗?让我们来使用这个功能。在你的终端上,指向 graph 目录,执行以下命令
yarn codegen
在这之后,Graph 已经为你创建了大部分的代码,其中包括映射。如果你看一下 src 中名为 random-winner-game.ts 的文件,Graph CLI 将为你创建一些函数,每个函数都指向你在合约中创建的一个事件。每当 Graph 找到与这些函数相关的事件时,这些函数就会被调用。我们将在这些函数中添加一些代码,这样我们就可以在有事件发生时存储数据。
用以下几行替换 random-winner-game.ts 文件的内容
import { BigInt } from "@graphprotocol/graph-ts";
import {
  PlayerJoined,
  GameEnded,
  GameStarted,
  OwnershipTransferred,
} from "../generated/RandomWinnerGame/RandomWinnerGame";
import { Game } from "../generated/schema";
export function handleGameEnded(event: GameEnded): void {
  // Entities can be loaded from the store using a string ID; this ID
  // needs to be unique across all entities of the same type
  let entity = Game.load(event.params.gameId.toString());
  // Entities only exist after they have been saved to the store;
  // `null` checks allow to create entities on demand
  if (!entity) {
    return;
  }
  // Entity fields can be set based on event parameters
  entity.winner = event.params.winner;
  entity.requestId = event.params.requestId;
  // Entities can be written to the store with `.save()`
  entity.save();
}
export function handlePlayerJoined(event: PlayerJoined): void {
  // Entities can be loaded from the store using a string ID; this ID
  // needs to be unique across all entities of the same type
  let entity = Game.load(event.params.gameId.toString());
  // Entities only exist after they have been saved to the store;
  // `null` checks allow to create entities on demand
  if (!entity) {
    return;
  }
  // Entity fields can be set based on event parameters
  let newPlayers = entity.players;
  newPlayers.push(event.params.player);
  entity.players = newPlayers;
  // Entities can be written to the store with `.save()`
  entity.save();
}
export function handleGameStarted(event: GameStarted): void {
  // Entities can be loaded from the store using a string ID; this ID
  // needs to be unique across all entities of the same type
  let entity = Game.load(event.params.gameId.toString());
  // Entities only exist after they have been saved to the store;
  // `null` checks allow to create entities on demand
  if (!entity) {
    entity = new Game(event.params.gameId.toString());
    entity.players = [];
  }
  // Entity fields can be set based on event parameters
  entity.maxPlayers = event.params.maxPlayers;
  entity.entryFee = event.params.entryFee;
  // Entities can be written to the store with `.save()`
  entity.save();
}
export function handleOwnershipTransferred(event: OwnershipTransferred): void {}
让我们了解一下 handleGameEnded 函数中发生了什么?
- 它接收 GameEnded 事件并期望返回 void,这意味着函数没有返回任何东西。
 - 它从 Graph 的数据库中加载一个游戏对象,其 ID 等于 Graph 检测到的事件中的 gameId。
 - 如果一个具有给定 ID 的实体不存在,则从函数中返回,不做任何事情。
 - 如果它存在,将事件中的获胜者和 requestId 设置到我们的游戏对象中(注意 GameEnded 事件有获胜者和 requestId)。
 - 然后将这个更新的游戏对象保存到图表的数据库中。
 - 每个游戏都会有一个独特的 Game 对象,它将有一个独特的 gameId。
 
现在让我们看看在 handlePlayerJoined 中发生了什么
- 它从 Graph 的数据库中加载一个 Game 对象,其 ID 与 Graph 检测到的事件中的 gameId 相同。
 - 如果一个具有给定 ID 的实体不存在,则从函数中返回,不做任何事情。
 - 为了实际更新玩家的数组,我们需要重新分配包含数组的实体上的属性(即玩家),类似于我们给实体上的其他属性(如 maxPlayers)赋值的方法。因此,我们需要创建一个临时数组,其中包含所有现有的 entity.pers 元素,推送到该数组,并重新分配 entity.pers,使其与新数组相等。
 - 然后将这个更新的游戏对象保存到图形的 DB 中。
 - 现在让我们看看 handleGameStarted 中发生了什么
- 它从 Graph 的数据库中加载一个 Game 对象,其 ID 等于 Graph 检测到的事件中存在的 gameId。
 - 如果一个这样的实体不存在,就创建一个新的,同时初始化玩家数组
 - 然后在我们的游戏对象中设置事件中的 maxPlayer 和 entryFee。
 - 将这个更新的游戏对象保存到 Graph 的数据库中。
 
 
现在你可以去你的终端,指向 graph 文件夹,执行以下命令。
yarn deploy # 或`npm run deploy`。
部署后,Graph 的托管服务,点击我的仪表盘,你就可以看到你的图形。
点击你的图表,确保它显示已同步,如果没有,请等待它同步后再继续。
网站
为了开发这个网站,我们将使用 React 和 Next Js。React 是一个用于制作网站的 javascript 框架,Next Js 是建立在 React 之上的。
首先,你需要创建一个新的 Next 应用程序。你的文件夹结构应该是这样的
- RandomWinnerGame - 图形 - Hardhat-tutorial - 下一个应用程序 - abi.json 要创建这个下一个应用程序,在终端指向 RandomWinnerGame 文件夹并输入
 
创建网站
npx create-next-app@latest
cd my-app
npm run dev
现在让我们安装 Web3Modal 库(https://github.com/Web3Modal/web3modal)。Web3Modal是一个易于使用的库,帮助开发者通过简单的可定制的配置在他们的应用程序中添加对多个供应商的支持。默认情况下,Web3Modal库支持注入的提供者,如(Metamask、Dapper、Gnosis Safe、Frame、Web3 Browsers 等),你也可以轻松配置该库以支持 Portis、Fortmatic、Squarelink、Torus、Authereum、D'CENT Wallet 和 Arkane。打开终端指向 atmy-app 目录,执行以下命令
安装依赖
npm i web3modal ethersaxios
添加图片
在你的公共文件夹中,下载这个图片。确保下载的图片名称为 randomWinner.png。
修改样式
styles/Home.modules.css
.main {
  min-height: 90vh;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  font-family: "Courier New", Courier, monospace;
}
.footer {
  display: flex;
  padding: 2rem 0;
  border-top: 1px solid #eaeaea;
  justify-content: center;
  align-items: center;
}
.input {
  width: 200px;
  height: 100%;
  padding: 1%;
  margin: 2%;
  box-shadow: 0 0 15px 4px rgba(0, 0, 0, 0.06);
  border-radius: 10px;
}
.image {
  width: 70%;
  height: 50%;
  margin-left: 20%;
}
.title {
  font-size: 2rem;
  margin: 2rem 1rem;
}
.description {
  line-height: 1;
  margin: 2rem 1rem;
  font-size: 1.2rem;
}
.log {
  line-height: 1rem;
  margin: 1rem 1rem;
  font-size: 1rem;
}
.button {
  border-radius: 4px;
  background-color: blue;
  border: none;
  color: #ffffff;
  font-size: 15px;
  padding: 8px;
  width: 200px;
  cursor: pointer;
  margin: 2rem 1rem;
}
@media (max-width: 1000px) {
  .main {
    width: 100%;
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }
}
现在让我们写一些代码来查询图形,在你的 my-app 文件夹内创建一个新的文件夹,并命名为 query。在 queries 文件夹中创建一个名为 index.js 的新文件,并粘贴以下几行代码。
export function FETCH_CREATED_GAME() {
  return `query {
        games(orderBy:id, orderDirection:desc, first: 1) {
            id
            maxPlayers
            entryFee
            winner
            players
        }
    }`;
}
正如你所看到的,我们创建了一个 GraphQL 查询,我们说我们想要一个游戏对象,其中的数据按 Id(也就是 gameId)降序排列,我们想从这个有序的数据中找到第一个游戏。
让我们用一个例子来简化这个问题。假设你有三个游戏对象存储在 The Graph 的节点内。
{
    "id": "1",
    "maxPlayers": 2,
    "entryFee": "10000000000000",
    "winner": "0xdb6eaffa95899b53b27086bd784f3bbfd58ff843"
},
{
    "id": "2",
    "maxPlayers": 2,
    "entryFee": "10000000000000",
    "winner": "0x01573df433484fcbe6325a0c6e051dc62ab107d1"
},
{
    "id": "3",
    "maxPlayers": 2,
    "entryFee": "100000000000000",
    "winner": "0x01573df433484fcbe6325a0c6e051dc62ab107d1"
},
{
    "id": "4",
    "maxPlayers": 2,
    "entryFee": "10",
    "winner": null
}
现在你希望每次都是最新的游戏。为了得到最新的游戏,你首先要按 Id 排序,然后把这些数据按降序排列,这样 gameId 4 就可以排在最前面(它将是当前的游戏),然后我们说 first:1,因为我们只想要 gameId 4 对象,我们不关心旧游戏。
你实际上可以看到这个查询在图的托管服务上工作。让我们试着这样做。
我已经部署了一个图,让我们看看我的图并使用查询来查询它,去这个链接
用我们的查询替换示例查询,然后点击紫色的播放按钮
你可以看到我的图 👀 中出现了一些数据,游戏的 ID 是 4。
你可以通过 "我的仪表板 "进入你的图表,做同样的事情。
通过 Graph 已经提供的用户界面进行操作是很好的,但是要在我们的前端使用这个,我们需要写一个 axios post request,这样我们就可以从图中获得这些数据了
创建一个名为 utils 的新文件夹,并在该文件夹中创建一个名为 index.js 的新文件。在 index.js 文件中复制以下几行代码。
import axios from "axios";
export async function subgraphQuery(query) {
  try {
    // Replace YOUR-SUBGRAPH-URL with the url of your subgraph
    const SUBGRAPH_URL = "YOUR-SUBGRAPH-URL";
    const response = await axios.post(SUBGRAPH_URL, {
      query,
    });
    if (response.data.errors) {
      console.error(response.data.errors);
      throw new Error(`Error making subgraph query ${response.data.errors}`);
    }
    return response.data.data;
  } catch (error) {
    console.error(error);
    throw new Error(`Could not query the subgraph ${error.message}`);
  }
}
它需要一个 SUBGRAPH_URL,你需要将 "YOUR-SUBGRAPH-URL "替换为你的子图的 URL,你可以通过进入图的托管服务,点击我的仪表板,然后点击你的图来获得。复制查询(HTTP)图片下的网址
然后它用我们用 axios post request 创建的查询来调用这个网址。
然后它将处理任何错误,如果没有错误,就将回复发回。
现在打开 page 文件夹下的 index.js 文件,粘贴以下代码,代码的解释可以在评论中找到。
import { BigNumber, Contract, ethers, providers, utils } from "ethers";
import Head from "next/head";
import React, { useEffect, useRef, useState } from "react";
import Web3Modal from "web3modal";
import { abi, RANDOM_GAME_NFT_CONTRACT_ADDRESS } from "../constants";
import { FETCH_CREATED_GAME } from "../queries";
import styles from "../styles/Home.module.css";
import { subgraphQuery } from "../utils";
export default function Home() {
  const zero = BigNumber.from("0");
  // walletConnected keep track of whether the user's wallet is connected or not
  const [walletConnected, setWalletConnected] = useState(false);
  // loading is set to true when we are waiting for a transaction to get mined
  const [loading, setLoading] = useState(false);
  // boolean to keep track of whether the current connected account is owner or not
  const [isOwner, setIsOwner] = useState(false);
  // entryFee is the ether required to enter a game
  const [entryFee, setEntryFee] = useState(zero);
  // maxPlayers is the max number of players that can play the game
  const [maxPlayers, setMaxPlayers] = useState(0);
  // Checks if a game started or not
  const [gameStarted, setGameStarted] = useState(false);
  // Players that joined the game
  const [players, setPlayers] = useState([]);
  // Winner of the game
  const [winner, setWinner] = useState();
  // Keep a track of all the logs for a given game
  const [logs, setLogs] = useState([]);
  // Create a reference to the Web3 Modal (used for connecting to Metamask) which persists as long as the page is open
  const web3ModalRef = useRef();
  // This is used to force react to re render the page when we want to
  // in our case we will use force update to show new logs
  const forceUpdate = React.useReducer(() => ({}), {})[1];
  /*
    connectWallet: Connects the MetaMask wallet
  */
  const connectWallet = async () => {
    try {
      // Get the provider from web3Modal, which in our case is MetaMask
      // When used for the first time, it prompts the user to connect their wallet
      await getProviderOrSigner();
      setWalletConnected(true);
    } catch (err) {
      console.error(err);
    }
  };
  /**
   * Returns a Provider or Signer object representing the Ethereum RPC with or without the
   * signing capabilities of metamask attached
   *
   * A `Provider` is needed to interact with the blockchain - reading transactions, reading balances, reading state, etc.
   *
   * A `Signer` is a special type of Provider used in case a `write` transaction needs to be made to the blockchain, which involves the connected account
   * needing to make a digital signature to authorize the transaction being sent. Metamask exposes a Signer API to allow your website to
   * request signatures from the user using Signer functions.
   *
   * @param {*} needSigner - True if you need the signer, default false otherwise
   */
  const getProviderOrSigner = async (needSigner = false) => {
    // Connect to Metamask
    // Since we store `web3Modal` as a reference, we need to access the `current` value to get access to the underlying object
    const provider = await web3ModalRef.current.connect();
    const web3Provider = new providers.Web3Provider(provider);
    // If user is not connected to the Mumbai network, let them know and throw an error
    const { chainId } = await web3Provider.getNetwork();
    if (chainId !== 80001) {
      window.alert("Change the network to Mumbai");
      throw new Error("Change network to Mumbai");
    }
    if (needSigner) {
      const signer = web3Provider.getSigner();
      return signer;
    }
    return web3Provider;
  };
  /**
   * startGame: Is called by the owner to start the game
   */
  const startGame = async () => {
    try {
      // Get the signer from web3Modal, which in our case is MetaMask
      // No need for the Signer here, as we are only reading state from the blockchain
      const signer = await getProviderOrSigner(true);
      // We connect to the Contract using a signer because we want the owner to
      // sign the transaction
      const randomGameNFTContract = new Contract(
        RANDOM_GAME_NFT_CONTRACT_ADDRESS,
        abi,
        signer
      );
      setLoading(true);
      // call the startGame function from the contract
      const tx = await randomGameNFTContract.startGame(maxPlayers, entryFee);
      await tx.wait();
      setLoading(false);
    } catch (err) {
      console.error(err);
      setLoading(false);
    }
  };
  /**
   * startGame: Is called by a player to join the game
   */
  const joinGame = async () => {
    try {
      // Get the signer from web3Modal, which in our case is MetaMask
      // No need for the Signer here, as we are only reading state from the blockchain
      const signer = await getProviderOrSigner(true);
      // We connect to the Contract using a signer because we want the owner to
      // sign the transaction
      const randomGameNFTContract = new Contract(
        RANDOM_GAME_NFT_CONTRACT_ADDRESS,
        abi,
        signer
      );
      setLoading(true);
      // call the startGame function from the contract
      const tx = await randomGameNFTContract.joinGame({
        value: entryFee,
      });
      await tx.wait();
      setLoading(false);
    } catch (error) {
      console.error(error);
      setLoading(false);
    }
  };
  /**
   * checkIfGameStarted checks if the game has started or not and intializes the logs
   * for the game
   */
  const checkIfGameStarted = async () => {
    try {
      // Get the provider from web3Modal, which in our case is MetaMask
      // No need for the Signer here, as we are only reading state from the blockchain
      const provider = await getProviderOrSigner();
      // We connect to the Contract using a Provider, so we will only
      // have read-only access to the Contract
      const randomGameNFTContract = new Contract(
        RANDOM_GAME_NFT_CONTRACT_ADDRESS,
        abi,
        provider
      );
      // read the gameStarted boolean from the contract
      const _gameStarted = await randomGameNFTContract.gameStarted();
      const _gameArray = await subgraphQuery(FETCH_CREATED_GAME());
      const _game = _gameArray.games[0];
      let _logs = [];
      // Initialize the logs array and query the graph for current gameID
      if (_gameStarted) {
        _logs = [`Game has started with ID: ${_game.id}`];
        if (_game.players && _game.players.length > 0) {
          _logs.push(
            `${_game.players.length} / ${_game.maxPlayers} already joined 👀 `
          );
          _game.players.forEach((player) => {
            _logs.push(`${player} joined 🏃♂️`);
          });
        }
        setEntryFee(BigNumber.from(_game.entryFee));
        setMaxPlayers(_game.maxPlayers);
      } else if (!gameStarted && _game.winner) {
        _logs = [
          `Last game has ended with ID: ${_game.id}`,
          `Winner is: ${_game.winner} 🎉 `,
          `Waiting for host to start new game....`,
        ];
        setWinner(_game.winner);
      }
      setLogs(_logs);
      setPlayers(_game.players);
      setGameStarted(_gameStarted);
      forceUpdate();
    } catch (error) {
      console.error(error);
    }
  };
  /**
   * getOwner: calls the contract to retrieve the owner
   */
  const getOwner = async () => {
    try {
      // Get the provider from web3Modal, which in our case is MetaMask
      // No need for the Signer here, as we are only reading state from the blockchain
      const provider = await getProviderOrSigner();
      // We connect to the Contract using a Provider, so we will only
      // have read-only access to the Contract
      const randomGameNFTContract = new Contract(
        RANDOM_GAME_NFT_CONTRACT_ADDRESS,
        abi,
        provider
      );
      // call the owner function from the contract
      const _owner = await randomGameNFTContract.owner();
      // We will get the signer now to extract the address of the currently connected MetaMask account
      const signer = await getProviderOrSigner(true);
      // Get the address associated to the signer which is connected to  MetaMask
      const address = await signer.getAddress();
      if (address.toLowerCase() === _owner.toLowerCase()) {
        setIsOwner(true);
      }
    } catch (err) {
      console.error(err.message);
    }
  };
  // useEffects are used to react to changes in state of the website
  // The array at the end of function call represents what state changes will trigger this effect
  // In this case, whenever the value of `walletConnected` changes - this effect will be called
  useEffect(() => {
    // if wallet is not connected, create a new instance of Web3Modal and connect the MetaMask wallet
    if (!walletConnected) {
      // Assign the Web3Modal class to the reference object by setting it's `current` value
      // The `current` value is persisted throughout as long as this page is open
      web3ModalRef.current = new Web3Modal({
        network: "mumbai",
        providerOptions: {},
        disableInjectedProvider: false,
      });
      connectWallet();
      getOwner();
      checkIfGameStarted();
      setInterval(() => {
        checkIfGameStarted();
      }, 2000);
    }
  }, [walletConnected]);
  /*
    renderButton: Returns a button based on the state of the dapp
  */
  const renderButton = () => {
    // If wallet is not connected, return a button which allows them to connect their wllet
    if (!walletConnected) {
      return (
        <button onClick={connectWallet} className={styles.button}>
          Connect your wallet
        </button>
      );
    }
    // If we are currently waiting for something, return a loading button
    if (loading) {
      return <button className={styles.button}>Loading...</button>;
    }
    // Render when the game has started
    if (gameStarted) {
      if (players.length === maxPlayers) {
        return (
          <button className={styles.button} disabled>
            Choosing winner...
          </button>
        );
      }
      return (
        <div>
          <button className={styles.button} onClick={joinGame}>
            Join Game 🚀
          </button>
        </div>
      );
    }
    // Start the game
    if (isOwner && !gameStarted) {
      return (
        <div>
          <input
            type="number"
            className={styles.input}
            onChange={(e) => {
              // The user will enter the value in ether, we will need to convert
              // it to WEI using parseEther
              setEntryFee(
                e.target.value >= 0
                  ? utils.parseEther(e.target.value.toString())
                  : zero
              );
            }}
            placeholder="Entry Fee (ETH)"
          />
          <input
            type="number"
            className={styles.input}
            onChange={(e) => {
              // The user will enter the value in ether, we will need to convert
              // it to WEI using parseEther
              setMaxPlayers(e.target.value ?? 0);
            }}
            placeholder="Max players"
          />
          <button className={styles.button} onClick={startGame}>
            Start Game 🚀
          </button>
        </div>
      );
    }
  };
  return (
    <div>
      <Head>
        <title>LW3Punks</title>
        <meta name="description" content="LW3Punks-Dapp" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={styles.main}>
        <div>
          <h1 className={styles.title}>Welcome to Random Winner Game!</h1>
          <div className={styles.description}>
            Its a lottery game where a winner is chosen at random and wins the
            entire lottery pool
          </div>
          {renderButton()}
          {logs &&
            logs.map((log, index) => (
              <div className={styles.log} key={index}>
                {log}
              </div>
            ))}
        </div>
        <div>
          <img className={styles.image} src="./randomWinner.png" />
        </div>
      </div>
      <footer className={styles.footer}>Made with ❤ by Your Name</footer>
    </div>
  );
}
创建 constants/index.js
替换 "你的 RandomWinnerGame 合约的地址",你 部署和保存到你的记事本的 RandomWinnerGame 合约的地址。 替换---你的 ABI---与您的 RandomWinnerGame 合约的 ABI。为了得到你的合约的 ABI,去你的硬帽教程/artifacts/contracts/RandomWinnerGame.sol 文件夹,并从你的 RandomWinnerGame.json 文件得到 "ABI "键下的数组标记。
export const abi = "---your abi---";
export const RANDOM_GAME_NFT_CONTRACT_ADDRESS =
  "address of your RandomWinnerGame contract";