The Graph[1] 是区块链的一个去中心化的查询协议和索引服务。它允许开发人员轻松地跟踪各种网络上的智能合约所发出的事件,并编写定制的数据转换脚本,这些脚本是实时运行的。这些数据也通过一个简单的GraphQL API提供,然后开发者可以用它来在他们的前端显示东西。
(引用自 The Graph 网站)
RandomWinnerGame - hardhat-tutorial - abi.json
yarn global add @graphprotocol/graph-cli
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
graph auth
cd graphyarn deploy
你可以回到The Graph's Hosted Service[10] ,点击 My Dashboard,你将能够看到你的图表,因为它现在已经部署了
你已经部署了你的第一个图表!!!。
现在,有趣的部分来了,我们将把The Graph提供给我们的默认代码修改为可以帮助我们跟踪合同事件的代码。
让我们开始吧
source: address: "0x889Ef69261272Caa27f0655D0208bAc7055EDAD5" abi: RandomWinnerGame startBlock: BLOCK_NUMBER
你的最终文件应该是这样[12]的
我们将需要一个Entity,它可以涵盖我们事件中的所有变量,以便我们可以跟踪所有的变量。打开schema.graphql文件,用以下几行代码替换已有的代码。
type Game @entity { id: ID! maxPlayers: Int! entryFee: BigInt! winner: Bytes requestId: Bytes players: [Bytes!]!}
如果你想了解更多的类型,你可以访问这个链接[14]
好了,现在我们已经让the graph知道我们将追踪什么样的数据,以及它将包含什么
现在是查询这些数据的时候了
Graph 有一个惊人的功能,给定的Entity可以为你自动生成大块的代码!!。
这不是很神奇吗?让我们使用这个功能。在你的终端指向 the graph目录,执行以下命令
yarn codegen
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 {}
现在你可以去你的终端,指向graph文件夹,执行以下命令。
yarn deploy
- RandomWinnerGame - graph - hardhat-tutorial - next-app - abi.json
npx create-next-app@latest
并对所有问题按回车键
cd my-appnpm run dev
npm install web3modal
npm i ethers
npm i axios
.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; } }
export function FETCH_CREATED_GAME() { return `query { games(orderBy:id, orderDirection:desc, first: 1) { id maxPlayers entryFee winner players } }`;}
{ "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 }
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}`); }}
然后它用我们用axios post request创建的查询来调用这个网址
然后,它处理任何错误,如果没有错误,则发回答复。
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 ( ); } // If we are currently waiting for something, return a loading button if (loading) { return ; } // Render when the game has started if (gameStarted) { if (players.length === maxPlayers) { return ( ); } return ( ); } // Start the game if (isOwner && !gameStarted) { return ( { // 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)" /> { // 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" /> ); } }; return ( LW3Punks Welcome to Random Winner Game!
Its a lottery game where a winner is chosen at random and wins the entire lottery pool {renderButton()} {logs && logs.map((log, index) => ( {log} ))}
);}
export const abi =---your abi---export const RANDOM_GAME_NFT_CONTRACT_ADDRESS = "address of your RandomWinnerGame contract"
npm run dev
在继续之前,请确保你已经将所有的代码推送到github[21] :)
现在我们将部署你的DApp,这样大家就可以看到你的网站,你也可以和所有LearnWeb3 DAO的朋友分享。
[1] The Graph: https://thegraph.com/
[2] 这里: https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable
[3] GraphQL教程: https://www.youtube.com/watch?v=ZQL7tL2S0oQ
[4] 简短的教程: https://www.youtube.com/watch?v=6LyagkoRWYA
[5] Chainlink VRF : https://github.com/LearnWeb3DAO/Chainlink-VRFs
[6] GraphQL Endpoint: https://graphql.org/learn/
[7] 内容: https://github.com/LearnWeb3DAO/Graph/blob/master/abi.json
[8] The Graph's Hosted Service: https://thegraph.com/hosted-service/
[9] The Graph's Hosted Service: https://thegraph.com/hosted-service/
[10] The Graph's Hosted Service: https://thegraph.com/hosted-service/
[11] Mumbai PolygonScan: https://mumbai.polygonscan.com/
[12] 这样: https://github.com/LearnWeb3DAO/Graph/blob/master/graph/subgraph.yaml#L11
[13] 这个: https://thegraph.com/docs/en/developer/create-subgraph-hosted/#defining-entities
[14] 链接: https://thegraph.com/docs/en/developer/create-subgraph-hosted/#built-in-scalar-types
[15] The Graph's Hosted Service: https://thegraph.com/hosted-service/
[16] React: https://reactjs.org/
[17] Next Js: https://nextjs.org/
[18] 这张图片: https://github.com/LearnWeb3DAO/Graph/blob/master/my-app/public/randomWinner.png
[19] 这个链接: https://thegraph.com/hosted-service/subgraph/sneh1999/learnweb3
[20] hosted service: https://thegraph.com/hosted-service
[21] 推送到github: https://medium.com/hackernoon/a-gentle-introduction-to-git-and-github-the-eli5-way-43f0aa64f2e4
留言与评论(共有 0 条评论) “” |