メインコンテンツまでスキップ

lesson-4_WEBアプリ上でNFTキャラクターを選ぶページを作ろう

🐱 NFT キャラクターをフロントエンドに表示する

前回のレッスンでは、Webアプリケーションからスマートコントラクトを呼び出すコードを実装し、 SelectCharacterコンポーネントを作成しました。

これから、スマートコントラクトからNFTキャラクターを取得してフロントエンドに表示させていきましょう。

👀 deploy.jsを整理する

Webアプリケーションの開発を進める前に、contract/scriptsにある、deploy.jsファイルを整理しましょう。

mintCharacterNFTattackBoss関数を排除していきます。

もうすでに排除されている場合は、このプロセスをスキップして問題ありません。

deploy.jsが下記のようになっていることを確認したら、次に進みましょう。

const main = async () => {
const gameContractFactory = await hre.ethers.getContractFactory("MyEpicGame");

const gameContract = await gameContractFactory.deploy(
["ZORO", "NAMI", "USOPP"], // キャラクターの名前
[
"https://i.imgur.com/TZEhCTX.png", // キャラクターの画像
"https://i.imgur.com/WVAaMPA.png",
"https://i.imgur.com/pCMZeiM.png",
],
[100, 200, 300],
[100, 50, 25],
"CROCODILE", // Bossの名前
"https://i.imgur.com/BehawOh.png", // Bossの画像
10000, // Bossのhp
50 // Bossの攻撃力
);
// ここでは、nftGame コントラクトが、
// ローカルのブロックチェーンにデプロイされるまで待つ処理を行っています。
const nftGame = await gameContract.deployed();

console.log("Contract deployed to:", nftGame.address);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();

deploy.jsを整理することにより、フロントエンドにおける状態エラーを防ぐことができます。

deploy.jsを更新した後、もう一度スマートコントラクトをデプロイすると、これからWebアプリケーション上でNFTキャラクターをMintするプロセスがスムーズになります。

復習も兼ねて、下記を実行していきましょう。

1 . 再度、コントラクトをデプロイする。

  • yarn contract deployを実行する必要があります。

2 . フロントエンド( constants.js)のCONTRACT_ADDRESSを更新する。

3 . の ABI ファイルを更新する。

  • contract/artifacts/contracts/MyEpicGame.sol/MyEpicGame.jsonの中身を新しく作成するclient/src/utils/MyEpicGame.jsonの中に貼り付ける必要があります。

♻️ index.jsを更新する

client/src/Components/SelectCharacterにあるindex.jsは、プログラムの中で何度も登場する変数や関数をまとめているファイルです。

これから、index.jsの中身を更新していきます。

まず、index.jsimportの部分を下記のように更新してください。

import React, { useEffect, useState } from "react";
import "./SelectCharacter.css";
import { ethers } from "ethers";
import { CONTRACT_ADDRESS, transformCharacterData } from "../../constants";
import myEpicGame from "../../utils/MyEpicGame.json";

次に、SelectCharacterを下記のように更新しましょう。

// SelectCharacter コンポーネントを定義しています。
const SelectCharacter = ({ setCharacterNFT }) => {
const [characters, setCharacters] = useState([]);
const [gameContract, setGameContract] = useState(null);

// ページがロードされた瞬間に下記を実行します。
useEffect(() => {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const gameContract = new ethers.Contract(
CONTRACT_ADDRESS,
myEpicGame.abi,
signer
);

// gameContract の状態を更新します。
setGameContract(gameContract);
} else {
console.log("Ethereum object not found");
}
}, []);

return (
<div className="select-character-container">
<h2>⏬ 一緒に戦う NFT キャラクターを選択 ⏬</h2>
</div>
);
};
export default SelectCharacter;

追加したコードを詳しく見ていきましょう。

const [characters, setCharacters] = useState([]);
const [gameContract, setGameContract] = useState(null);

ここでは、いくつかの状態変数を設定していきます。

  • characters : コントラクトから返されるNFTキャラクターのメタデータを保持するプロパティ。

  • setCharacters : charactersの状態を更新するプロパティ。

  • gameContract : コントラクトの状態を初期化して保存するプロパティ。

    プログラムの中でコントラクトは複数回呼び出されるので、いったん初期化した状態で保存し、コントラクト全体で使用できるようにします。

  • setGameContract : gameContractの状態を更新するプロパティ。

次に、下記のコードを見ていきましょう。

// ページがロードされた瞬間に下記を実行します。
useEffect(() => {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const gameContract = new ethers.Contract(
CONTRACT_ADDRESS,
myEpicGame.abi,
signer
);
// gameContract の状態を更新します。
setGameContract(gameContract);
} else {
console.log("Ethereum object not found");
}
}, []);

ここではuseEffectを使って、SelectCharacterコンポーネントが呼び出されたら、すぐにgameContractを作成して、使用できるようにしています。

この処理により、フロントエンドでNFTキャラクターを表示する準備が整います。

😎 NFT キャラクターのデータを取得する

NFTキャラクターのデータをスマートコントラクトから取得するために、getCharacters関数を作成します。

gameContractを使用する準備ができたら、すぐにgetCharacters関数を呼び出したいので、ここでもuseEffectを使用していきます。

それでは、SelectCharacterの中に記載したuseEffect関数を確認しましょう。

useEffect(() => {
const { ethereum } = window;

if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const gameContract = new ethers.Contract(
CONTRACT_ADDRESS,
myEpicGame.abi,
signer
);
// gameContract の状態を更新します。
setGameContract(gameContract);
} else {
console.log("Ethereum object not found");
}
}, []);

この関数の直下に、下記を追加していきましょう。

useEffect(() => {
// NFT キャラクターのデータをスマートコントラクトから取得します。
const getCharacters = async () => {
try {
console.log("Getting contract characters to mint");
// ミント可能な全 NFT キャラクター をコントラクトをから呼び出します。
const charactersTxn = await gameContract.getAllDefaultCharacters();

console.log("charactersTxn:", charactersTxn);

// すべてのNFTキャラクターのデータを変換します。
const characters = charactersTxn.map((characterData) =>
transformCharacterData(characterData)
);

// ミント可能なすべてのNFTキャラクターの状態を設定します。
setCharacters(characters);
} catch (error) {
console.error("Something went wrong fetching characters:", error);
}
};
// gameContractの準備ができたら、NFT キャラクターを読み込みます。
if (gameContract) {
getCharacters();
}
}, [gameContract]);

コードの中身を見ていきましょう。

// ミント可能な全 NFT キャラクター をコントラクトをから呼び出します。
const charactersTxn = await gameContract.getAllDefaultCharacters();

ここでは、gameContractを使用して、MyEpicGame.solに記載したgetAllDefaultCharacters関数を呼び出しています。

✍️: getAllDefaultCharactersは、3 体の NFT キャラクターのデフォルト情報を取得する関数です。

次に、下記のコードを見ていきましょう。

// すべてのNFTキャラクターのデータを変換します。
const characters = charactersTxn.map((characterData) =>
transformCharacterData(characterData)
);

ここでは、transformCharacterDataを使用して、NFTキャラクターのデータをWebアプリケーションで扱えるオブジェクトに変換しています。

✍️: map()の使い方 map()は配列データに使うメソッドです。 map()メソッドを使って、配列に入っている NFT キャラクターそれぞれの属性情報( HP など)に対してtransformCharacterDataを実行し、その結果を新しい配列( characters )として返しています。

次に下記のコードを見ていきましょう。

// ミント可能なすべてのNFTキャラクターの状態を設定します。
setCharacters(characters);

ここでは、コントラクトから取得したNFTキャラクターのデータを状態として保存しています。

この処理により、NFTキャラクターのデータをフロントエンドで使い始めることができます。

最後に、下記のコードを見ていきましょう。

// gameContractの準備ができたら、NFT キャラクターを読み込みます。
if (gameContract) {
getCharacters();
}

ここでは、gameContractが更新されるたびに、中身がnullでないことを確認し、getCharacters関数を呼び出す処理を実装しています。

この処理により、NFTキャラクターのデータが更新されるたびに、キャラクターの状態を更新て、フロントエンドに反映させることができます。

⚡️ Web アプリケーション上でテストを行う

Webアプリケーション上で、NFTキャラクターの情報が取得できているか、確認してみましょう。

ローカル環境でホストされているWebアプリケーション上で、Inspectを実行し、Consoleに向かいましょう。

Webアプリケーションをリフレッシュして、ウォレット接続が完了したら、下記のような結果がConsoleに出力されているか確認してください。

charactersTxn:
(3) [Array(6), Array(6), Array(6)]
0: (6) [BigNumber, 'ZORO', 'https://i.imgur.com/TZEhCTX.png', BigNumber, BigNumber, BigNumber, characterIndex: BigNumber, name: 'ZORO', imageURI: 'https://i.imgur.com/TZEhCTX.png', hp: BigNumber, maxHp: BigNumber, …]
1: (6) [BigNumber, 'NAMI', 'https://i.imgur.com/WVAaMPA.png', BigNumber, BigNumber, BigNumber, characterIndex: BigNumber, name: 'NAMI', imageURI: 'https://i.imgur.com/WVAaMPA.png', hp: BigNumber, maxHp: BigNumber, …]
2: (6) [BigNumber, 'USOPP', 'https://i.imgur.com/pCMZeiM.png', BigNumber, BigNumber, BigNumber, characterIndex: BigNumber, name: 'USOPP', imageURI: 'https://i.imgur.com/pCMZeiM.png', hp: BigNumber, maxHp: BigNumber, …]
length: 3
[[Prototype]]: Array(0)

上記のような結果がConsoleに表示されていればテストは成功です。

👓 NFT キャラクター を Web アプリケーションにレンダリングする

それでは、NFTキャラクターの情報をWebアプリケーションに反映させていきましょう。

まず、index.jsを開き、SelectCharacterコンポーネントの中に、renderCharactersメソッドを追加しましょう。

  • 2つ目に作成した、useEffect関数の直下に、下記を貼り付けてください。
// NFT キャラクターをフロントエンドにレンダリングするメソッドです。
const renderCharacters = () =>
characters.map((character, index) => (
<div className="character-item" key={character.name}>
<div className="name-container">
<p>{character.name}</p>
</div>
<img src={character.imageURI} alt={character.name} />
<button
type="button"
className="character-mint-button"
//onClick={mintCharacterNFTAction(index)}
>{`Mint ${character.name}`}</button>
</div>
));

⚠️: 注意

エラー処理を円滑にするため、onClick={mintCharacterNFTAction(index)}はコメントアウトしたままにしてください。 後で解除します。

次に、index.jsの中のreturn();の中身を下記のように更新してください。

return (
<div className="select-character-container">
<h2>⏬ 一緒に戦う NFT キャラクターを選択 ⏬</h2>
{/* キャラクターNFTがフロントエンド上で読み込めている際に、下記を表示します*/}
{characters.length > 0 && (
<div className="character-grid">{renderCharacters()}</div>
)}
</div>
);

それでは、Webアプリケーションをリフレッシュして、下記のようにNFTキャラクターがフロントエンドに反映されていることを確認してください。

✨ Web アプリケーションから NFT キャラクター を Mint する

これから、NFTキャラクターをMintするmintCharacterNFTAction関数を作成していきます。

index.jsを開き、const [gameContract, setGameContract] = useState(null);の直下に下記を追加しましょう。

// NFT キャラクターを Mint します。
const mintCharacterNFTAction = (characterId) => async () => {
try {
if (gameContract) {
console.log("Minting character in progress...");
const mintTxn = await gameContract.mintCharacterNFT(characterId);
await mintTxn.wait();
console.log("mintTxn:", mintTxn);
}
} catch (error) {
console.warn("MintCharacterAction Error:", error);
}
};

⚠️: 注意

renderCharacters関数の中にあるonClick = {mintCharacterNFTAction(index)}のコメントアウトを解除してください。

mintCharacterNFTAction関数は、MyEpicGame.solに記載されているmintCharacterNFT関数を呼び出します。

  • どのNFTキャラクターをMintするかコントラクトに伝えるために、そのキャラクターのインデックス(characterId)を引数として取り巻す。

  • onClick = {mintCharacterNFTAction(index)}indexがNFTキャラクターのインデックスです。

🏓 コントラクトでemitされたeventをフロントエンドで受け取る

NFTキャラクターがMintされたことをフロントエンドに伝えるeventをコントラクト上に作成したことを覚えてますか?

これから、Webアプリケーション上でeventの情報を「キャッチ」するコードを実装していきます。

index.js内でgetCharacters関数を定義したuseEffectのコードブロックを下記のように編集してください。

useEffect(() => {
// NFT キャラクターのデータをスマートコントラクトから取得します。
const getCharacters = async () => {
try {
console.log("Getting contract characters to mint");

// ミント可能な全 NFT キャラクター をコントラクトをから呼び出します。
const charactersTxn = await gameContract.getAllDefaultCharacters();

console.log("charactersTxn:", charactersTxn);

// すべてのNFTキャラクターのデータを変換します。
const characters = charactersTxn.map((characterData) =>
transformCharacterData(characterData)
);

// ミント可能なすべてのNFTキャラクターの状態を設定します。
setCharacters(characters);
} catch (error) {
console.error("Something went wrong fetching characters:", error);
}
};

// イベントを受信したときに起動するコールバックメソッド onCharacterMint を追加します。
const onCharacterMint = async (sender, tokenId, characterIndex) => {
console.log(
`CharacterNFTMinted - sender: ${sender} tokenId: ${tokenId.toNumber()} characterIndex: ${characterIndex.toNumber()}`
);
// NFT キャラクターが Mint されたら、コントラクトからメタデータを受け取り、アリーナ(ボスとのバトルフィールド)に移動するための状態に設定します。
if (gameContract) {
const characterNFT = await gameContract.checkIfUserHasNFT();
console.log("CharacterNFT: ", characterNFT);
setCharacterNFT(transformCharacterData(characterNFT));
}
};

if (gameContract) {
getCharacters();
// リスナーの設定:NFT キャラクターが Mint された通知を受け取ります。
gameContract.on("CharacterNFTMinted", onCharacterMint);
}

return () => {
// コンポーネントがマウントされたら、リスナーを停止する。

if (gameContract) {
gameContract.off("CharacterNFTMinted", onCharacterMint);
}
};
}, [gameContract]);

新しく追加したコードを詳しく見ていきましょう。

// イベントを受信したときに起動するコールバックメソッド onCharacterMint を追加します。
const onCharacterMint = async (sender, tokenId, characterIndex) => {
console.log(
`CharacterNFTMinted - sender: ${sender} tokenId: ${tokenId.toNumber()} characterIndex: ${characterIndex.toNumber()}`
);
// NFT キャラクターが Mint されたら、コントラクトからメタデータを受け取り、アリーナ(ボスとのバトルフィールド)に移動するための状態に設定します。
if (gameContract) {
const characterNFT = await gameContract.checkIfUserHasNFT();
console.log("CharacterNFT: ", characterNFT);
setCharacterNFT(transformCharacterData(characterNFT));
}
};

下記は、MyEpicGame.solに記載したNFTキャラクターがMintされたeventをフロントエンドに知らせる(emit)コードです。

// ユーザーが NFT を Mint したこと示すイベント
event CharacterNFTMinted(address sender, uint256 tokenId, uint256 characterIndex);
// ユーザーが NFT を Mint したことをフロントエンドに伝えます。
emit CharacterNFTMinted(msg.sender, newItemId, _characterIndex);

onCharacterMintメソッドは、このイベントをキャッチするので、新しいNFTキャラクターがMintされるたびに呼び出されます。

次に、下記のコードを詳しく見ていきましょう。

// NFT キャラクターが Mint されたら、コントラクトからメタデータを受け取り、アリーナ(ボスとのバトルフィールド)に移動するための状態に設定します。
if (gameContract) {
const characterNFT = await gameContract.checkIfUserHasNFT();
console.log("CharacterNFT: ", characterNFT);
setCharacterNFT(transformCharacterData(characterNFT));
}

まず、if (gameContract)でNFTキャラクターがすでにMintされていることを確認しています。

ユーザーがすでにNFTキャラクターをMintしている場合は、checkIfUserHasNFT関数を使って、コントラクト(gameContract)に保存されているそのNFTキャラクターのメタデータ(HPなど)をcharacterNFTに格納します。

setCharacterNFT(transformCharacterData(characterNFT));は、コントラクトに保存されているメタデータをフロントエンドで扱えるオブジェクト形式に変換する処理です。

これらがすべて完了すると、NFT キャラクターがボスとバトルすることになるArenaコンポーネントでメタデータが使用できます。

次に、下記のコードを見ていきましょう。

if (gameContract) {
getCharacters();
// リスナーの設定:NFT キャラクターが Mint された通知を受け取ります。
gameContract.on("CharacterNFTMinted", onCharacterMint);
}

gameContract.on('CharacterNFTMinted', onCharacterMint)により、フロントエンドは、CharacterNFtMintedイベントがコントラクトから発信されたときに、情報を受け取ります。これにより、情報がフロントエンドに反映されます。

  • このことを、コンポーネント(情報)がマウント(フロントエンドに反映)されると言います。

  • また、フロントエンドでイベントを受信する機能のことを「リスナ」と呼びます。

フロントエンドでCharacterNFtMintedイベントが受信されると、onCharacterMintメソッドが実行されます。

最後に、下記のコードを見ていきましょう。

return () => {
// コンポーネントがマウントされたら、リスナーを停止する。
if (gameContract) {
gameContract.off("CharacterNFTMinted", onCharacterMint);
}
};

ここでは、NFTキャラクターが一度Mintされた後、CharacterNFTMintedの受信を停止する処理を行っています。

コンポーネントがマウントされる状態をそのままにしておくと、メモリリーク(コンピュータを動作させている内に、使用可能なメモリの容量が減っていってしまう現象)が発生する可能性があります。

メモリリークを防ぐために、gameContract.off('CharacterNFTMinted', onCharacterMint)では、onCharacterMint関数の稼働をやめています。これは、イベントリスナをやめることを意味しています。

🐝 gemcase で Mint した NFT キャラクターを確認する

それでは、Webアプリケーションから、キャラクターをいったいMintして、gemcase(閲覧できるサービス) に反映されるか確認していきましょう。

1️⃣ NFT キャラクターを Mint する

Webアプリケーション上で、Mintボタンを押したら、MetaMask上で承認作業(Confirm)を行ってください。

Mintが成功した後に、Webアプリケーションをリフレッシュすると下記のような結果が、Consoleに表示されます。

Checking for Character NFT on address: 0x3a0a49fb3cf930e599f0fa7abe554dc18bd1f135

Getting contract characters to mint

User has character NFT

このような結果が表示されているということは、あなたのウォレットアドレスにNFTキャラクターが存在していることになります。

2️⃣ OpenSea で NFT キャラクターを確認する

gemcase(NFT を閲覧できるサービス)で、NFTキャラクターを参照してみましょう。

あなたのCONTACT_ADDRESSTOKEN_IDを取得して、下記のアドレスを更新したら、ブラウザに貼り付けてみてください。

https://gemcase.vercel.app/view/evm/sepolia/CONTRACT_ADDRES/TOKEN_ID

下記のように、オンライン上でもあなたのNFTキャラクターが表示されることを確認しましょう(画像は学習コンテンツ制作時に利用したRarible rinkeby testnetのものになります)。

🪄 おまけ

ユーザーにNFTキャラクターを確認するgemcaseリンクを発行しましょう。

index.jsを開いて、onCharacterMint関数の中身を変更します。

  • setCharacterNFT(transformCharacterData(characterNFT));の直下に下記を追加しましょう。
alert(
`NFT キャラクターが Mint されました -- リンクはこちらです: https://gemcase.vercel.app/view/evm/sepolia/${
gameContract.address
}/${tokenId.toNumber()}`
);

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#ethereumで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

次のレッスンに進んで、ボスとのバトルフィールドを実装しましょう 🎉