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

lesson-5_ローカル環境で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のコントラクトを「継承」しています。

  • NFTの標準規格はERC721と呼ばれます。こちら に説明が記載されています。

  • ERC721コントラクトの詳細は、こちら をご覧ください。

「継承」とは、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という状態変数には、tokenIdCharacterAttributes構造体に格納されたデータが対になって保存されます。

  • コードの後半に、nftHolderAttributes[newItemId] = CharacterAttributes({...})という処理が記載されています。ここでは、現在のtokenIdであるnewItemIdCharacterAttributes構造体に紐付ける処理が行われています。

    • 後で詳しく解説します。

同じようにmappingを使用している下記のコードを見ていきましょう。

mapping(address => uint256) public nftHolders;

ここでは、ユーザーのaddresstokenIdを紐づけるため、mappingを使用しています。

  • nftHoldersという状態変数には、ユーザーのaddresstokenIdに格納されたデータが対になって保存されます。

  • コードの後半に、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です。

ここでは_tokenIDs1を加算しています。

constructorの中で _tokenIDs1にインクリメントするのは、1番目のtokenId1とした方が、あとあと処理が楽になるためです。

  • increment()関数に関しては、こちら を参照してください。

次に、mintCharacterNFT関数の中身を見ていきましょう。

function mintCharacterNFT(uint _characterIndex) external {

この関数を呼び出すことにより、NFT の Mint が行われます。

_characterIndexはフロントエンドから送信される変数です。

_characterIndexmintCharacterNFT関数に渡すことで、プレイヤーがどのキャラクター(例:ナミ)を欲しいか、コントラクトに伝えます。

たとえば、mintCharacterNFT(1)とすると、defaultCharacters[1]のデータを持つキャラクターがMintされます。

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

uint256 newItemId = _tokenIds.current();

ここでは、ユーザーが新しくNFTをMintする際に発行されるtokenIDを格納するために、newItemId変数を定義してます。

これはNFT自体のIDです。

各NFTは「一意」であり、そのために各トークンに一意のIDを付与しています。

通常_tokenIds.current()0から始まりますが、constructor_tokenIds.increment()を行ったので、newItemId1になります。

NFTの一意な識別子を追跡するために_tokenIdsを使用していますが、これは単なる数字です。

  • 最初にmintCharacterNFTを呼び出すとnewItemId1になり、もう一度呼び出すとnewItemId2になり、これが繰り返されます。

  • newItemIdの値を変更すると、その値はグローバル変数のように直接コントラクトに格納され、メモリ上に永久に残ります。

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

_safeMint(msg.sender, newItemId);

上記が実行されると、newItemIdというIDのNFTキャラクターがmsg.sender(=フロントエンドからユーザーのアドレス)に、Mintされます。

✍️: msg.senderについて msg.senderSolidity が提供する 変数で、フロントエンドからコントラクトを呼び出したユーザーの 公開アドレス を保持した変数です。

原則として、ユーザーは、コントラクトを匿名で呼び出すことはできません。

ユーザーは、フロントエンドからウォレット認証を行って、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.jsconsole.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には、tokenId1の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.solnftHolderAttributesを更新して、tokenURIを添付していきます。

⭐️ tokenURIをセットアップする

tokenURIには、NFTデータを JSON 形式で渡す必要があります。

まず、contract/contractsディレクトリの下にlibrariesというディレクトリを作成しましょう。

下記のディレクトリ構図を参考にしてください。

contract
|_ contracts
|_ libraries

librariesディレクトリにBase64.solという名前のファイルを作成し、下記のコードを貼り付けてください。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

/// [MIT License]
/// @title Base64
/// @notice Provides a function for encoding some bytes in base64
/// @author Brecht Devos <brecht@loopring.org>
library Base64 {
bytes internal constant TABLE =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/// @notice Encodes some bytes to the base64 representation
function encode(bytes memory data) internal pure returns (string memory) {
uint256 len = data.length;
if (len == 0) return "";

// multiply by 4/3 rounded up
uint256 encodedLen = 4 * ((len + 2) / 3);

// Add some extra buffer at the end
bytes memory result = new bytes(encodedLen + 32);

bytes memory table = TABLE;

assembly {
let tablePtr := add(table, 1)
let resultPtr := add(result, 32)

for {
let i := 0
} lt(i, len) {

} {
i := add(i, 3)
let input := and(mload(add(data, i)), 0xffffff)

let out := mload(add(tablePtr, and(shr(18, input), 0x3F)))
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)
)
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)
)
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)
)
out := shl(224, out)

mstore(resultPtr, out)

resultPtr := add(resultPtr, 4)
}

switch mod(len, 3)
case 1 {
mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
}
case 2 {
mstore(sub(resultPtr, 1), shl(248, 0x3d))
}

mstore(result, encodedLen)
}

return string(result);
}
}

このファイルには、SVGとJSONをBase64に変換するための関数が含まれています。

次に、MyEpicGame.solBase64.solをインポートするコードを追加します。

下記のコードを、MyEpicGame.solの先頭付近(ライブラリをインポートしているコードブロックの近く)に、追加してください。

// Base64.sol からヘルパー関数をインポートする。
import "./libraries/Base64.sol";

次に、MyEpicGameコントラクトの中に、tokenURIという関数を記述します。

  • mintCharacterNFT関数の下に、下記の関数を追加してください。
// nftHolderAttributes を更新して、tokenURI を添付する関数を作成
function tokenURI(uint256 _tokenId) public view override returns (string memory) {
CharacterAttributes memory charAttributes = nftHolderAttributes[_tokenId];
// charAttributes のデータ編集して、JSON の構造に合わせた変数に格納しています。
string memory strHp = Strings.toString(charAttributes.hp);
string memory strMaxHp = Strings.toString(charAttributes.maxHp);
string memory strAttackDamage = Strings.toString(charAttributes.attackDamage);

string memory json = Base64.encode(
// abi.encodePacked で文字列を結合します。
// OpenSeaが採用するJSONデータをフォーマットしています。
abi.encodePacked(
'{"name": "',
charAttributes.name,
' -- NFT #: ',
Strings.toString(_tokenId),
'", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "',
charAttributes.imageURI,
'", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ',
strAttackDamage,'} ]}'
)
);
// 文字列 data:application/json;base64, と json の中身を結合して、tokenURI を作成しています。
string memory output = string(
abi.encodePacked("data:application/json;base64,", json)
);
return output;
}

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

  // nftHolderAttributes を更新して、tokenURI を添付する関数を作成
function tokenURI(uint256 _tokenId) public view override returns (string memory) {
// _tokenId を使って特定の NFT データを照会し、データを取得しています。
CharacterAttributes memory charAttributes = nftHolderAttributes[_tokenId];
:

ここでは、nftHolderAttributes関数に渡された_tokenIdを使って、特定のNFTデータを照会し、そのデータをcharAttributesに格納しています。

  • もし私がtokenURI(151)を実行したら、151番目のNFTに関連するJSONデータを返します。

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

string memory strHp = Strings.toString(charAttributes.hp);
string memory strMaxHp = Strings.toString(charAttributes.maxHp);
string memory strAttackDamage = Strings.toString(charAttributes.attackDamage);

ここでは、charAttributesデータ編集して、JSONの構造に合わせた変数に格納しています。

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

string memory json = Base64.encode(
// abi.encodePacked で文字列を結合します。
abi.encodePacked(
'{"name": "',
charAttributes.name,
' -- NFT #: ',
Strings.toString(_tokenId),
'", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "',
charAttributes.imageURI,
'", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ',
strAttackDamage,'} ]}'
)
);

ここでは、OpenSeaなどのNFTマーケットプレイスが採用しているNFTのメタデータのデータ構造に沿って、JSONをフォーマットしています。

  • OpenSeaなどのNFTマーケットプレイスで扱えるJSONのデータ構造に関しては、こちら をご覧ください。

NFTのメタデータとして使用できるJSONは、下記のようなデータ構造になります。

{
"name": "USOPP",
"description": "This is an NFT that lets people play in the game Metaverse Slayer!",
"image": "https://i.imgur.com/WVAaMPA.png",
"attributes": [
{ "trait_type": "Health Points", "value": 200, "max_value": 200 },
{ "trait_type": "Attack Damage", "value": 50 }
]
}

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

abi.encodePacked("data:application/json;base64,", json);

ここでは、文字列data:application/json;base64,jsonの中身を結合して、tokenURIを作成しています。

それでは、ターミナル上で下記を実行してみましょう。

yarn contract run:script

下記のような結果がターミナルに出力されていれば、テストは成功です。

Compiling 2 files with 0.8.17
Solidity compilation finished successfully
Done initializing ZORO w/ HP 100, img https://i.imgur.com/TZEhCTX.png
Done initializing NAMI 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: data:application/json;base64,eyJuYW1lIjogIlpFTklHQU1FIC0tIE5GVCAjOiAxIiwgImRlc2NyaXB0aW9uIjogIkJyYXZlIGFzIGEgYmxhemluZyBmaXJlLiIsICJpbWFnZSI6ICJodHRwczovL2kuaW1ndXIuY29tL2NmdG9kajkucG5nIiwgImF0dHJpYnV0ZXMiOiBbIHsgInRyYWl0X3R5cGUiOiAiSGVhbHRoIFBvaW50cyIsICJ2YWx1ZSI6IDMwMCwgIm1heF92YWx1ZSI6MzAwfSwgeyAidHJhaXRfdHlwZSI6ICJBdHRhY2sgRGFtYWdlIiwgInZhbHVlIjogMjV9IF19

Token URIがターミナルに出力されました!

Token URI:の後に続く文字列全体をコピーしてください。

例)

data:application/json;base64 eyJuYW1lIjogIlpFTklHQU1FIC0tIE5GVC...

その文字列をブラウザのURLバーに貼り付けて、中身を確認してみましょ。

ブラウザに表示されるているのは、MyEpicGame.solに追加したtokenURI関数の中で、NFTキャラクターの情報がJSONファイル形式にフォーマットされ、さらにBase64形式でエンコードされた結果です。

  • JSONファイルの前にdata:application/json;base64,を付けると、上記のようなエンコード文字列になり、ブラウザで読み込めます。

🙋‍♂️ 質問する

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

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

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

次のレッスンに進んで、テストネットにコントラクトをデプロイしていきましょう 🎉