Part 2: How to Create an NFT Game Frontend

Learn how to create a Next.js frontend that powers an on-chain game similar to Cryptokitties and Axie Infinity

In the last tutorial on Part 1: How to Create an NFT Game Smart Contract, we leveraged Hardhat, Solidity, and Alchemy to deploy an NFT game smart contract that allowed users to breed and collect creatures called Alchemon.

1920

Cryptokitties, a breeding game that Alchemon is based on

In this tutorial, we will build a frontend using Next.js that is capable of interacting with our smart contract and allows users to breed and view their Alchemons directly from a web app. In case you haven’t already, we suggest that you complete Part I of the tutorial first before proceeding with Part II.

About the Frontend

Through our game’s frontend, users will be able to do the following:

  1. Breed new Alchemons using the Alchemons they already have (genesis or otherwise).
  2. View all the Alchemons they own.
  3. View all other projects created by the creators of Alchemon.
  4. View a few sample Alchemons.

We will achieve this by interfacing with our smart contract using ethers, wagmi, and Alchemy. Additionally, we will use the Alchemy NFT API to easily extract useful data such as NFT ownership and metadata.

Creating the NFT Game Frontend

Step 1: Create an Alchemy App

We will need an Alchemy app to use the NFT API, implement connect wallet functionality, and send transactions to the Goerli blockchain. You can use the same app that you created in Part I. To re-iterate, this is how you go about creating an app:

  1. From Alchemy’s dashboard, hover over the Apps drop-down menu and choose Create App.
  2. Provide a Name and Description for your app. For Chain, select Ethereum and for Network select Goerli.
  3. Click the Create App button.

2880

Creating an app on the Alchemy Dashboard

Once you have created your app, click on your app’s View Key button in the dashboard and save the HTTP URL. We will use this later.

Step 2: Download the starter repository

We have already created a starter repository for you that implements helpful functionality such as the connect wallet feature.

Download or clone the repository here.

Once this is done, open the repository in a Terminal and run the following command:

shell
$npm install

This will install Next.js as well as all other libraries we will be using including the Alchemy SDK, ethers, and wagmi.

Once the installation is complete, open the repository in your favorite code editor (e.g. VS Code).

Step 3: Populate the constants.js file

In the data folder of the project, there is a constants.js file. This file will contain all the values required by us to interface with the NFT API and our smart contract. Populate the contents of the file with the following:

constants.js
1const alchemyApiKey = "<-- ALCHEMY APP API KEY -->";
2const ownerAddress = "<-- WALLET ADDRESS USED TO DEPLOY CONTRACT IN PART I -->";
3const contractAddress = "<-- DEPLOYED ALCHEMON CONTRACT ADDRESS FROM PART I -->";
4
5export { alchemyApiKey, ownerAddress, contractAddress }

Step 4: Obtain the contract ABI file

In order to invoke functions on the contract from our frontend, the latter requires an ABI file which gives it information on all the functions in the contract such as its view scope, inputs, outputs, name, etc.

Fortunately for us, our Hardhat project from Part I generates this file automatically upon compilation of our contract.

In the alchemon project from Part I, navigate to artifacts/contracts/AlchemonNft.sol and copy the AlchemonNft.json file.

Next, in our current alchemon-frontend repository, create a new folder called contracts and paste this file into it.

Step 5: Create the main breeding page

We are now in a good place to write code for the main page. On this page, users will be able to do the following:

  1. Choose 2 distinct Alchemons they own to breed a new Alchemon. We will use the getNftsForOwner function of the NFT API along with ethers to achieve this.
  2. View a few sample Alchemons that already exist using the getNftsForContract function.
  3. View a few other projects that have been launched by the Alchemon creators using the getContractsForOwner function.

This page assumes that your MetaMask wallet is already connected to the app. If it isn’t, the page redirects you to the Connect page.

Open the index.js file in the pages folder and add the following code:

index.jsx
1// Standard Next and CSS imports
2import Head from 'next/head'
3import styles from '@/styles/Home.module.css'
4import { useRouter } from "next/router";
5import { useState, useEffect, Fragment } from "react";
6
7// Alchemy SDK imports for NFT API
8import { Network, Alchemy } from "alchemy-sdk";
9
10// Imports from the constants.js file
11import { alchemyApiKey, contractAddress } from "@/data/constants";
12
13// Wagmi import for connected wallet info
14import { useAccount } from "wagmi";
15
16// Contract ABI import
17import contract from '@/contracts/AlchemonNft.json';
18
19// Ethers for invoking functions on smart contract
20import { ethers } from 'ethers';
21
22// Extract ABI from the ABI JSON file
23const abi = contract.abi;
24
25export default function Home() {
26
27 // Standard Next router definition
28 const router = useRouter();
29
30 // Get connected wallet address and connection status
31 const { address, isConnected } = useAccount();
32
33 // Page mounting info to prevent hydration errors
34 const [hasMounted, setHasMounted] = useState(false);
35
36 // Variable that holds sample Alchemon NFTs
37 const [samples, setSamples] = useState([]);
38
39 // Variable that holds contracts created by Owner address
40 const [contracts, setContracts] = useState([]);
41
42 // Variable that holds all Alchemons created by connected wallet
43 const [nfts, setNfts] = useState([]);
44
45 // Parent Alchemons
46 const [parent1, setParent1] = useState('none');
47 const [parent2, setParent2] = useState('none');
48
49 // Initialize Alchemy SDK
50 const settings = {
51 apiKey: alchemyApiKey,
52 network: Network.ETH_GOERLI,
53 };
54
55 const alchemy = new Alchemy(settings);
56
57 // Mounting fix to avoid hydration errors
58 useEffect(() => {
59 setHasMounted(true)
60 }, []);
61
62 // Get all Alchemon NFTs owned by the connected wallet
63 useEffect(() => {
64 const getNfts = async () => {
65 const response = await alchemy.nft.getNftsForOwner(address);
66 const nfts = response.ownedNfts.filter(nft => nft.contract.address.toUpperCase() === contractAddress.toUpperCase());
67 setNfts(nfts);
68 }
69 getNfts();
70 }, [])
71
72 // Get sample Alchemon NFTs
73 useEffect(() => {
74 const getSamples = async () => {
75 const response = await alchemy.nft.getNftsForContract(contractAddress);
76 const nfts = response.nfts.slice(0, 3);
77 setSamples(nfts);
78 }
79 getSamples();
80 }, [])
81
82 // Get all contracts for owner
83 useEffect(() => {
84 const options = { method: 'GET', headers: { accept: 'application/json' } };
85
86 fetch(`https://eth-mainnet.g.alchemy.com/nft/v2/${alchemyApiKey}/getContractsForOwner?owner=${address}`, options)
87 .then(response => response.json())
88 .then(response => setContracts(response.contracts))
89 .catch(err => console.error(err));
90 }, [])
91
92 // Do not render until entire UI is mounted
93 if (!hasMounted) return null;
94
95 // Redirect to Connect page if wallet is not connected
96 if (!isConnected) {
97 router.replace('/connect');
98 }
99
100 // Form handlers
101 const parent1Handler = (e) => {
102 setParent1(e.target.value);
103 }
104
105 const parent2Handler = (e) => {
106 setParent2(e.target.value);
107 }
108
109 // Function that allows breeding of NFTs using 2 parent NFTs
110 const mintNft = async (e) => {
111
112 e.preventDefault();
113
114 // Only allow breeding if the parents are distinct
115 if (parent1 === 'none' || parent2 === 'none' || parent1 === parent2) {
116 console.log("Incorrect parents");
117 return;
118 }
119
120 // Call the breed function from connected wallet
121 try {
122 const { ethereum } = window;
123
124 if (ethereum) {
125 const provider = new ethers.providers.Web3Provider(ethereum);
126 const signer = provider.getSigner();
127 const nftContract = new ethers.Contract(contractAddress, abi, signer);
128
129 console.log("Initialize payment");
130 let nftTxn = await nftContract.breed(parseInt(parent1), parseInt(parent2));
131
132 console.log("Mining... please wait");
133 await nftTxn.wait();
134
135 console.log(`Mined, see transaction: https://goerli.etherscan.io/tx/${nftTxn.hash}`);
136 router.replace('/dashboard');
137
138 } else {
139 console.log("Ethereum object does not exist");
140 }
141
142 } catch (err) {
143 console.log(err);
144 }
145 }
146
147 return (
148 <Fragment>
149
150 <Head>
151 <title>Alchemon</title>
152 <meta name="description" content="A simple NFT based game" />
153 <meta name="viewport" content="width=device-width, initial-scale=1" />
154 <link rel="icon" href="/favicon.ico" />
155 </Head>
156
157 <main className={styles.main}>
158 <h1>Alchemon</h1>
159
160 <button onClick={() => router.push('/dashboard')}>
161 Profile Dashboard
162 </button>
163
164 <h2>Breed a New Alchemon</h2>
165
166 <form className={styles.breed_form} onSubmit={mintNft}>
167 <select name="parent1" id="parent1" value={parent1} onChange={parent1Handler}>
168 <option value="none">---</option>
169 {nfts.map(nft => {
170 return <option value={nft.tokenId} key={nft.tokenId}>{nft.title}</option>
171 })}
172 </select>
173 <select name="parent2" id="parent2" value={parent2} onChange={parent2Handler}>
174 <option value="none">---</option>
175 {nfts.map(nft => {
176 return <option value={nft.tokenId} key={nft.tokenId}>{nft.title}</option>
177 })}
178 </select>
179 <button type='submit'>Breed</button>
180 </form>
181
182 <h2>Sample Alchemon NFTs</h2>
183 <div className={styles.sample_nfts}>
184 {samples.map(nft => {
185 return (
186 <div className={styles.nft} key={nft.tokenId}>
187 <h3>{nft.title}</h3>
188 <img src={nft.media[0].raw} />
189 </div>
190 )
191 })}
192 </div>
193
194 <h2>Projects by Alchemon Creators</h2>
195 <ul className={styles.contract_container}>
196 {contracts.map(contract => {
197 return (
198 <li key={contract.address}>
199 <a href={`https://goerli.etherscan.io/address/${contract.address}`} target="_blank">
200 {contract.address}
201 </a>
202 </li>
203 )
204 })}
205 </ul>
206
207 </main>
208 </Fragment>
209 )
210}

In the main breeding page:

In order to understand what various parts of the code does, do make sure to read the comments associated with each section.

Once we wire up the remaining components of the app, this page should look something like this:

2880

The main Alchemon breeding page

Step 6: Create the profile dashboard page

In the profile dashboard page, users will be able to see their connected wallet address and all the Alchemons that they own. For the latter, we will be using the getNftsForOwner function of the Alchemy NFT API.

In the dashboard.js file within the pages folder, add the following code:

dashboard.js
1// Standard Next and CSS imports
2import Head from "next/head";
3import { Fragment, useState, useEffect } from "react";
4import styles from "../styles/dashboard.module.css";
5import { useRouter } from "next/router";
6
7// Alchemy SDK imports for NFT API
8import { Network, Alchemy } from "alchemy-sdk";
9
10// Imports from the constants.js file
11import { alchemyApiKey, contractAddress } from "@/data/constants";
12
13// Wagmi import for connected wallet info
14import { useAccount } from "wagmi";
15
16export default function Dashboard() {
17
18 // Standard Next router definition
19 const router = useRouter();
20
21 // Get connected wallet address and connection status
22 const { address, isConnected } = useAccount();
23
24 // Page mounting info to prevent hydration errors
25 const [hasMounted, setHasMounted] = useState(false);
26
27 // Variable that holds all Alchemons created by connected wallet
28 const [nfts, setNfts] = useState([]);
29
30 // Initialize Alchemy SDK
31 const settings = {
32 apiKey: alchemyApiKey,
33 network: Network.ETH_GOERLI,
34 };
35
36 const alchemy = new Alchemy(settings);
37
38 // Mounting fix to avoid hydration errors
39 useEffect(() => {
40 setHasMounted(true);
41 }, []);
42
43 // Get all Alchemon NFTs owned by the connected wallet
44 useEffect(() => {
45 const getNfts = async () => {
46 const response = await alchemy.nft.getNftsForOwner(address);
47 const nfts = response.ownedNfts.filter(nft => nft.contract.address.toUpperCase() === contractAddress.toUpperCase());
48 setNfts(nfts);
49 }
50
51 getNfts();
52 }, [])
53
54 // Do not render until entire UI is mounted
55 if (!hasMounted) return null;
56
57 // Redirect to Connect page if wallet is not connected
58 if (!isConnected) {
59 router.replace('/connect');
60 }
61
62 return (
63 <Fragment>
64 <Head>
65 <title>Profile Dashboard</title>
66 </Head>
67
68 <div className={styles.jumbotron}>
69
70 <h1>Dashboard</h1>
71
72 <h2>Contract Address</h2>
73 <p>{address}</p>
74
75 <h2 className={styles.nft_heading}>My NFTs</h2>
76 <div className={styles.nft_container}>
77 {nfts.map(nft => {
78 return (
79 <div className={styles.nft} key={nft.tokenId}>
80 <h3>{nft.rawMetadata.name}</h3>
81 <img src={nft.media[0].raw} />
82 </div>
83 )
84 })}
85 </div>
86
87 <button onClick={() => router.replace('/')}>
88 Back to Main Page
89 </button>
90
91 </div>
92 </Fragment>
93 )
94}

In the dashboard page, we use getNftsForOwner to get a list of all Alchemon NFTs owned by the wallet.

In order to understand what various parts of the code does, do make sure to read the comments associated with each section.

We are all set to go! Let’s launch our app on localhost using the following command:

shell
$npm run dev

Playing the game

  • When you open localhost:3000, you will first be prompted to visit the localhost:3000/connect page. Here, connect your MetaMask wallet to be redirected back to the main breeding page at localhost:3000.

  • For breeding to work, you must choose two different Alchemons as parents. In case you don’t have 2 Alchemons, you can simply mint Genesis NFTs by visiting localhost:3000/mint-genesis. Make sure you’ve got enough Goerli ETH from the Goerli Faucet before you mint.

  • You will be redirected back on the main page. Select an Alchemon in both the first and the second dropdown, then click on breed. Metamask will prompt you to pay gas for the transaction.

  • Mining the transaction will roughly take 15 seconds after which you will be re-directed to a Dashboard page where you can see your new Alchemon NFT! This completes the game.

  • You can keep doing this indefinitely to create newer generations of Alchemons from the main breeding page.

Conclusion

Congratulations! You now know how to create a Next.js frontend that interacts with gaming smart contracts and implements an on-chain breeding game.

As a learning exercise we would love for you to give more functionality to these alchemons. For example, you could give certain skillsets and skill levels to these alchemons and then have 2 Alchemons fight each other - so one of them wins.

If you enjoyed this tutorial about creating NFT games or happen to build more functionality on top of this, tweet us at @Alchemy!

Don’t forget to join our Discord server to meet other blockchain devs, builders, and entrepreneurs!

Ready to start building your NFT game?

Create a free Alchemy account and do share your project with us!