ローカル環境でNFTをmintしよう
✨ NFT を Mint する
さて、キャラクターのデータが整ったので、次は実際にNFTをMintしていきましょう。
下記のように、MyEpicGame.solを更新してください。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// NFT発行のコントラクト ERC721.sol をインポートします。
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
//OpenZeppelinが提供するヘルパー機能をインポートします。
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "hardhat/console.sol";
// MyEpicGameコントラクトは、NFTの標準規格であるERC721を継承します。
contract MyEpicGame is ERC721 {
struct CharacterAttributes {
uint characterIndex;
string name;
string imageURI;
uint hp;
uint maxHp;
uint attackDamage;
}
//OpenZeppelin が提供する tokenIds を簡単に追跡するライブラリを呼び出しています。
using Counters for Counters.Counter;
// tokenIdはNFTの一意な識別子で、0, 1, 2, .. N のように付与されます。
Counters.Counter private _tokenIds;
// キャラクターのデフォルトデータを保持するための配列 defaultCharacters を作成します。それぞれの配列は、CharacterAttributes 型です。
CharacterAttributes[] defaultCharacters;
// NFTの tokenId と CharacterAttributes を紐づける mapping を作成します。
mapping(uint256 => CharacterAttributes) public nftHolderAttributes;
// ユーザーのアドレスと NFT の tokenId を紐づける mapping を作成しています。
mapping(address => uint256) public nftHolders;
constructor(
// プレイヤーが新しく NFT キャラクターを Mint する際に、キャラクターを初期化するために渡されるデータを設定しています。これらの値は フロントエンド(js ファイル)から渡されます。
string[] memory characterNames,
string[] memory characterImageURIs,
uint[] memory characterHp,
uint[] memory characterAttackDmg
)
// 作成するNFTの名前とそのシンボルをERC721規格に渡しています。
ERC721("OnePiece", "ONEPIECE")
{
// ゲームで扱う全てのキャラクターをループ処理で呼び出し、それぞれのキャラクターに付与されるデフォルト値をコントラクトに保存します。
// 後でNFTを作成する際に使用します。
for(uint i = 0; i < characterNames.length; i += 1) {
defaultCharacters.push(CharacterAttributes({
characterIndex: i,
name: characterNames[i],
imageURI: characterImageURIs[i],
hp: characterHp[i],
maxHp: characterHp[i],
attackDamage: characterAttackDmg[i]
}));
CharacterAttributes memory character = defaultCharacters[i];
// ハードハットのconsole.log()では、任意の順番で最大4つのパラメータを指定できます。
// 使用できるパラメータの種類: uint, string, bool, address
console.log("Done initializing %s w/ HP %s, img %s", character.name, character.hp, character.imageURI);
}
// 次の NFT が Mint されるときのカウンターをインクリメントします。
_tokenIds.increment();
}
// ユーザーは mintCharacterNFT 関数を呼び出して、NFT を Mint ことができます。
// _characterIndex は フロントエンドから送信されます。
function mintCharacterNFT(uint _characterIndex) external {
// 現在の tokenId を取得します(constructor内でインクリメントしているため、1から始まります)。
uint256 newItemId = _tokenIds.current();
// msg.sender でフロントエンドからユーザーのアドレスを取得して、NFT をユーザーに Mint します。
_safeMint(msg.sender, newItemId);
// mapping で定義した tokenId を CharacterAttributesに紐付けます。
nftHolderAttributes[newItemId] = CharacterAttributes({
characterIndex: _characterIndex,
name: defaultCharacters[_characterIndex].name,
imageURI: defaultCharacters[_characterIndex].imageURI,
hp: defaultCharacters[_characterIndex].hp,
maxHp: defaultCharacters[_characterIndex].maxHp,
attackDamage: defaultCharacters[_characterIndex].attackDamage
});
console.log("Minted NFT w/ tokenId %s and characterIndex %s", newItemId, _characterIndex);
// NFTの所有者を簡単に確認できるようにします。
nftHolders[msg.sender] = newItemId;
// 次に使用する人のためにtokenIdをインクリメントします。
_tokenIds.increment();
}
}
一行ずつ、更新されたコードを見ていきましょう。
// NFT発行のコントラクト ERC721.sol をインポートします。
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
//OpenZeppelinが提供するヘルパー機能をインポートします。
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
OpenZepplin は、イーサリアムネットワーク上の開発を便利にするフレームワークです。
OpenZeppelinは、NFTの標準規格を実装し、そのうえに独自のロジックを書いてカスタマイズできるライブラリを提供しています。ここでは、それらをMyEpicGame.solにインポートしています。
次に、下記のコードを見ていきましょう。
contract MyEpicGame is ERC721 {
ここでは、コントラクトを宣言する際に、is ERC721を使用してOpenZeppelinのコントラクトを「継承」しています。
「 継承」とは、OpenZeppelinのようなライブラリやほかのスマートコントラクトから、必要なモジュールを呼び出すことを意味します。
-
これは関数をインポートするようなイメージで理解してください。
-
NFTのモジュールは
ERC721として知られています。 -
このモジュールには、NFTの発行に必要な標準機能が含まれているため、開発者は自分のコントラクトをカスタマイズすることに集中できます。
次に、下記のコードを見ていきましょう。
using Counters for Counters.Counter;
using Counters for Counters.CounterはOpenZeppelinが_tokenIdsを追跡するために提供するライブラリを呼び出しています。
これにより、トラッキングの際に起こりうるオーバーフローを防ぎます。
💡 オーバーフローとは?
例えば、8 ビットの情報量しか持てない
uint8があるとします。つまり、格納できる最大の数値は 2 進数の 11111111(10 進 数では、2^8 - 1 = 255)です。 次のコードを見てください。最後の number は何に等しいでしょうか?uint8 number = 255;
number++;この場合、オーバーフローを起こしたので、
numberは増加したにもかかわらず、実は 0 になっています。( 2 進数の 11111111 に 1 を加えると、時計が 23 時 59 分 から 00 時 00 分 に戻るように、00000000 にリセットされます)。アンダーフローも同様で、0 に等しい
uint8から 1 を引くと、255 になります(uintは符号なしなので、負にすることはできないからです)。ここでは
uint8を使用していませんし、uint256が毎回 1 ずつ増加するときにオーバーフローする可能性は低いと思われますが(2^256は本当に大きな数です)、将来的に DApp が予期せぬ動作をすることがないように、コントラクトに保護規定を設けることはグッとプラクティスです!👍
次に、下記のコードを見ていきましょう。
Counters.Counter private _tokenIds;
ここでは、private _tokenIdsを宣言して、_tokenIdsを初期化しています。
_tokenIdsの初期値は0です。
tokenIdはNFTの一意な識別子で、0, 1, 2, .. Nのように付与されます。
次に、下記のコードを見ていきましょう。
mapping(uint256 => CharacterAttributes) public nftHolderAttributes;
nftHolderAttributesはプレイヤーのNFTの状態を保存する変数になります。
ここでは、NFTのIDをCharacterAttributes構造体にmappingしています。