ローカル環境で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しています。
✍️:
mappingについて ここでは、mappingと呼ばれる特別なデータ構造を使用しています。Solidity の
mappingは、他の言語におけるハッシュテーブルや辞書のような役割を果たします。これらは、下記のように
_Keyと_Valueのペアの形式でデータを格納するために使用されます。例:
mapping(_Key=> _Value)public mappingName
今回は、NFTキャラクターのtokenId(= _Key = uint256)をそのユーザーがMintするNFTのCharacterAttributes(= _Value)に関連付けるためにmappingを使用しました。
-
nftHolderAttributesという状態変数には、tokenIdとCharacterAttributes構造体に格納されたデータが対になって保存されます。 -
コードの後半に、
nftHolderAttributes[newItemId] = CharacterAttributes({...})という処理が記載されています。ここでは、現在のtokenIdであるnewItemIdをCharacterAttributes構造体に紐付ける処理が行われています。- 後で詳しく解説します。
同じようにmappingを使用している下記のコードを見ていきましょう。
mapping(address => uint256) public nftHolders;
ここでは、ユーザーのaddressとtokenIdを紐づけるため、mappingを使用しています。
-
nftHoldersという状態変数には、ユーザーのaddressとtokenIdに格納されたデータが対になって保存されます。 -
コードの後半に、
nftHolders[msg.sender] = newItemIdという処理が記載されています。ここでは、msg.sender(=フロントエンドから送信されるユーザーのaddress)にnewItemIdに紐付ける処理が行われています。- 後で詳しく解説します。
次に下記のコードを見ていきましょう。
ERC721("OnePiece", "ONEPIECE");
ここでは、作成するNFTの名前("OnePiece")とそのシンボル("ONEPIECE")をERC721の規格に渡しています。
NFTはNon-Fungible "Token" の略であり、Tokenには、必ず名前とシンボルを付与する必要があります。
例:
- トークンの名前:Ethereum
- トークンのシンボル:ETH
次に、下記のコードを見ていきましょう。
_tokenIds.increment();
Solidityにおいて、すべての数は0から始まるため、_tokenIdsの初期値は0です。
ここでは_tokenIDsに1を加算しています。
constructorの中で _tokenIDsを1にインクリメントするのは、1番目のtokenIdを1とした方が、あとあと処理が楽になるためです。
increment()関数に関しては、こちら を参照してください。
次に、mintCharacterNFT関数の中身を見ていきましょう。
function mintCharacterNFT(uint _characterIndex) external {
この関数を呼び出すことにより、NFT の Mint が行われます。
_characterIndexはフロントエンドから送信される変数です。
_characterIndexをmintCharacterNFT関数に渡すことで、プレイヤーがどのキャラクター(例:ナミ)を欲しいか、コントラクトに伝えます。
たとえば、mintCharacterNFT(1)とすると、defaultCharacters[1]のデータを持つキャラクターがMintされます。
次に下記のコードを見ていきましょう。
uint256 newItemId = _tokenIds.current();
ここでは、ユーザーが新しくNFTをMintする際に発行されるtokenIDを格納するために、newItemId変数を定義してます。
これはNFT自体のIDです。
各NFTは「一意」であり、そのために各トークンに一意のIDを付与しています。
通常_tokenIds.current()は0から始まりますが、constructorで_tokenIds.increment()を行ったので、newItemIdは1になります。
NFTの一意な識別子を追跡するために_tokenIdsを使用していますが、これは単なる数字です。
-
最初に
mintCharacterNFTを呼び出すとnewItemIdは1になり、もう一度呼び出すとnewItemIdは2になり、これが繰り返されます。 -
newItemIdの値を変更すると、その値はグローバル変数のように直接コントラクトに格納され、メモリ上に永久に残ります。
次に、下記のコードを見ていきましょう。
_safeMint(msg.sender, newItemId);
上記が実行されると、newItemIdというIDのNFTキャラクターがmsg.sender(=フロントエンドからユーザーのアドレス)に、Mintされます。
✍️:
msg.senderについてmsg.senderは Solidity が提供する 変数で、フロントエンドからコントラク トを呼び出したユーザーの 公開アドレス を保持した変数です。原則として、ユーザーは、コントラクトを匿名で呼び出すことはできません。
ユーザーは、フロントエンドからウォレット認証を行って、NFT を Mint する必要があります。
- これは、コントラクトへの「サインイン」機能のようなものです。
🎨 NFT のデータを更新する
引き続き、MyEpicGame.solの内容を見ていきます。
今回のWebアプリケーションゲームでは、ボスに攻撃されると、プレイヤーの保持するNFTキャラクターのHPが減少します。
例を見ていきましょう。
私が新しくNFTをMintした際、私のNFTキャラクターには以下のようなデフォルト値が与えられます。
{
characterIndex: 1,
name: "USOPP",
imageURI: "https://i.imgur.com/WVAaMPA.png",
hp: 200,
maxHp: 200,
attackDamage: 50
}
たとえば、私のキャラクターが攻撃を受けてHPが50減ったとします。
HPは200 → 150になります。
その値を下記のように、NFT上で変更する必要があります。
{
characterIndex: 1,
name: "USOPP",
imageURI: "https://i.imgur.com/WVAaMPA.png",
hp: 150, // 更新された値
maxHp: 200,
attackDamage: 50
}
⚠️: 注意
すべてのプレイヤーは、それぞれ自分のキャラクタ ー NFT を持っており、それぞれの NFT がキャラクターの状態に関する固有のデータを保持しています。
このようなゲームの仕様を実装するために、コントラクトの中に、NFTキャラクターのHPが減ったことをデータとして保存するロジックを追加しました。
それでは、下記のコードを見ていきましょう。
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,
});
ここでは、newItemIdというIDを持つNFTキャラクターの状態を、更新しています。
データを更新するために、NFTのtokenId(= newItemId)をCharacterAttributes構造体にマップするnftHolderAttributes変数を使用します。
これにより、プレイヤーのNFTに関連する値を簡単に更新できます。
-
プレイヤーが攻撃されて、NFTキャラクターの
hp値が減ると、nftHolderAttributes上でそのキャラクターのhp値が更新されます。 -
この処理によって、プレイヤー固有のNFTデータをコントラクトに保存できます。
✍️:
mappingを覚えていますか?// MyEpicGame.sol
mapping(uint256 => CharacterAttributes) public nftHolderAttributesここで、現在の
tokenId(=newItemId)をCharacterAttributes構造体に紐づけるnftHolderAttributes変数を定義しました。
nftHolderAttributesはプレイヤーの NFT の状態を保存する変数になります。
NFTのメタデータは変更できないと思われがちですが、そんなことはありません。実はクリエイター次第なんです 😊
次に、下記の処理を見ていきましょう。
nftHolders[msg.sender] = newItemId;
ここでは、ユーザーのパブリックウォレットアドレスをNFTのtokenI(= newItemId)にマップしています。
この処理によって、誰がどのNFTを所有しているかを簡単に追跡できます。
🦖: プレイヤーと NFT キャラクター
簡単のために、今回のプロジェクトでは、各プレイヤーはウォレットアドレスにつき 1 つの NFT キャラクターしか保有できないようになっています。
もし興味があれば、プレイヤーが複数のキャラクターを保持できるようにコントラクトを調整してみてください 😊
最後に、下記のコードを見ていきましょう。
_tokenIds.increment();
NFTをMintした後、OpenZeppelinが提供する関数_tokenIds.increment()を使って_tokenIdsをインクリメントしています。
この 処理によって、次回NFTをミントするユーザーには、新しいtokenIdが付与されます。すでにMintされたtokenIdは誰も持つことができません。
😳 ローカル環境でテストを実行する
次は、run.jsに、mintCharacterNFT関数を呼び出す処理を追加していきます。
以下のコードをrun.jsのconsole.logの直下に追加しましょう。
// 再代入可能な変数 txn を宣言
let txn;
// 3体のNFTキャラクターの中から、3番目のキャラクターを Mint しています。
txn = await gameContract.mintCharacterNFT(2);
// Minting が仮想マイナーにより、承認されるのを待ちます。
await txn.wait();
// NFTのURIの値を取得します。tokenURI は ERC721 から継承した関数です。
let returnedTokenUri = await gameContract.tokenURI(1);
console.log("Token URI:", returnedTokenUri);
コードを1行ずつ見ていきましょう。
// 再代 入可能な変数 txn を宣言
let txn;
// 3体のNFTキャラクターの中から、3番目のキャラクターを Mint しています。
txn = await gameContract.mintCharacterNFT(2);
https://qiita.com/y-temp4/items/289686fbdde896d22b5e
ここでは、MyEpicGame.solからmintCharacterNFT関数を呼び出して、3体のNFTキャラクターの中から、3番目のキャラクターをMintしています。
run.jsからmintCharacterNFT関数を実行する際、Hardhatはあなたのローカル環境に設定された デフォルトウォレット をコントラクトに展開します。
- したがって 、
MyEpicGame.sol上では、Hardhatが提供したデフォルトウォレットのパブリックアドレスがmsg.senderに格納されます。
次に、下記のコードを見ていきましょう。
// Minting が仮想マイナーにより、承認されるのを待ちます。
await txn.wait();
// NFTのURIの値を取得します。tokenURI は ERC721 から継承した関数です。
let returnedTokenUri = await gameContract.tokenURI(1);
console.log("Token URI:", returnedTokenUri);
tokenURI()は、NFTにアタッチされている 実際のデータ を返す関数です。
gameContract.tokenURI(1)が呼び出されると、returnedTokenUriには、tokenId = 1のNFTキャラクターのデータ(キャラクターの名前、HPなど)が格納されます。
それでは、ターミナル上で下記を実行してみましょう。
yarn contract run:script
下記のような結果がターミナルに出力されていれば、テストは成功です。
Compiling 11 files with 0.8.17
Solidity compilation finished successfully
Done initializing ZORO w/ HP 100, img https://i.imgur.com/TZEhCTX.png
Done initializing USOPP w/ HP 200, img https://i.imgur.com/WVAaMPA.png
Done initializing USOPP w/ HP 300, img https://i.imgur.com/pCMZeiM.png
Contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Minted NFT w/ tokenId 1 and characterIndex 2
Token URI:
現在、NFTに実際のデータは添付されていないので、ターミナルには、Token URIが出力されていません。
これから、MyEpicGame.solのnftHolderAttributesを更新して、tokenURIを添付していきます。