😗

NFT完全に理解した!!になるために独自コントラクトでNFTを発行してみる方法の解説

2021/11/03に公開

この記事では ブロックチェーン全くわからんという人 を対象に NFT を独自コントラクトで発行する方法を解説する。いわゆる完全に理解したレベルになれることを目指す。

また、自分はまだこの辺りの技術のキャッチアップを始めたのが 2 ヶ月程度なので間違っている部分もあると思うがその辺りは指摘してもらえるとありがたい。

前提知識

まずは前提知識として理解しておくべきことが多いのでざっくりとそれぞれ整理する。ブロックチェーンの世界における開発は普通の Web の世界とは異なる部分が多いので最初に 全体像 を把握しておくことが重要。

基本用語

いきなり NFT の話を始めると知らない用語が多く登場してしまい面食らってしまうと思うのでまずは今後出てくる各用語の ざっくりとしたイメージ を書く。ちなみに簡略化するため Ethereum の話が中心になってしまうのでその点は注意してほしい。

  • ブロックチェーン
    • 公開データベース
  • ノード
    • 世界中のブロックチェーン参加者の PC やサーバがノードと呼ばれる。これらが繋がって ブロックチェーンネットワーク を作っている。
  • ウォレット
    • ブロックチェーンネットワークにアクセスするために必要なもの。中身は公開鍵と秘密鍵のセット。普通の Web の世界でいうところのメールアドレスとパスワード。ブラウザの Extension としてインストールされる形が一般的。Ethereum だとMetaMaskが有名。
    • ウォレット = アカウント、ウォレットアドレス = 公開鍵という理解で とりあえず OK。
  • Ethereum(イーサリアム)
    • 世の中にある様々なブロックチェーンの中の一つ。イーサリアムでは ETH という暗号通貨が使われている。
  • スマートコントラクト
    • 正確ではないが、ブロックチェーンネットワークにデプロイできるアプリ(とかクラス)みたいなものとイメージするとわかりやすい。このスマートコントラクトというプログラムを書くことで通貨の取引や NFT の発行など様々なシステムや営みが開発できる。
    • スマートコントラクトはそれぞれ固有のストレージやメモリを持っていてここに発行した NFT の ID や何個発行したかなどの情報を格納できる。
  • トークン
    • FT(例えば暗号通貨などのこと)も NFT(Non FT、つまり FT じゃないもののこと) もどちらもトークンという形のデジタルな物の表現。抽象的でピンとこないかもしれないが、このトークンがブロックチェーンを行き交うデータの中心。例えばスマートコントラクトを記述して「江端さんの暗号通貨(トークン)を入江さんへ送る」といったトークンの移転をブロックチェーン上に記録したりする。
  • ガス代
    • ブロックチェーンに何かを記録したり、スマートコントラクトをデプロイしたりするために必要なお金。Ethereum では ETH として支払う。この ETH の一部がいわゆる マイニング をしているノードに支払われる。
    • このガス代はブロックチェーンネットワークの安全性の担保や需給によって変動する。Ethereum においては巷の NFT 熱の高まりもあり需要が爆増していて、現在ガス代はとても高騰している。独自で NFT を発行するために 1,2 万円かかったりすることもある。

とりあえずはこれだけ知っていればブロックチェーンネットワークにおけるコントラクトの全体像を理解するためには十分。 ブロックチェーンネットワークウォレット というユニークなアカウントで参加するんだみたいなイメージだけでも最低限あると良い。

他にもコンセンサスアルゴリズムについてとかトランザクションの仕組みや Web3.0 全体 や IPFS なんかについても色々書きたいことはあるけども説明し出すとキリがない。もっと知りたい人は Ethereum の公式ドキュメントがとてもとても分かりやすいので一読することをおすすめ。

https://ethereum.org/ja/developers/docs/

NFT について

NFT(Non-Fungible Token)とは先ほども書いた通りユニーク(唯一)な物を表現できるトークン。絵や写真のようなデジタルデータや映画チケットなんかも用途によっては NFT として扱われる。

ちょっと硬い話だが、この NFT は Ethereum においてはERC721という規格が定められている。どうやって送受信されるか、誰が保持しているか、どのくらいネットワーク上に存在するのかなどがわかるように共通のインターフェースが定義されており、NFT を実装するコントラクトではこれらのインターフェースをメソッドとして実装する必要がある。

例えば ERC721 に定められているtokenURI(uint256 _tokenId)というメソッドがあり、その返り値として"https://enftxamplebucket.s3.amazonaws.com/my-tokens/" + _tokenIdのような URL を返すようにする。その時仮に引数の_tokenIdが 42 の時はhttps://enftxamplebucket.s3.amazonaws.com/my-tokens/42を返す。そしてその URL にアクセスすると下記のようなシンプルな JSON を返すようにしておく。

{
  "name": "Juice=Juice",
  "image": "https://enftxamplebucket.s3.amazonaws.com/assets/hoge.jpeg",
  "external_url: "http://www.helloproject.com/juicejuice/",
  "description": "We are J=J."
}

するとこのimageの部分に NFT として販売したい画像の URL なんかを含めておくことで、各マーケットプレイス(NFT の販売サイト。OpenSeaFoundationRarible 等)では「この NFT ではどの画像を表示すれば良いのか?」というのが共通インターフェースを通じて呼び出せるので分かるという仕組み。

独自コントラクトである意味

実はこうしたマーケットプレイスでは自分のウォレットさえあれば独自でスマートコントラクトを書かなくとも画像投稿サイトに画像をアップロードするような感覚で NFT を売ることができる。しかし独自コントラクトで発行しない場合、正確にはマーケットが NFT を発行するので NFT の最初の所有者がクリエータ自身ではなくなる。これを嫌う場合は独自コントラクトで NFT を発行し自分で所有、その後その NFT を好きなマーケットプレイスで販売するという形を取るしかない。

この辺りのメリットやデメリットに関して深掘りすると脱線してしまうので詳しくは下記に譲る。
https://note.com/cryptopilot/n/n1383f5093b68

実際に書いてみる

では実際にスマートコントラクトを実装してみる。下記が簡単な仕様。

  • コントラクトをデプロイした本人のみが NFT 発行できる
  • NFT の ID は自動インクリメント(https://enftxamplebucket.s3.amazonaws.com/my-tokens/42とかの 42 の部分の話)

使うもの

  • Node.js
    • スクリプトの実行に使う
    • nodenv などで v12 以上の適当な version のものを用意しておく
  • Hardhat
    • スマートコントラクトのコンパイルやデプロイからローカル開発環境用のブロックチェーンネットワークの準備まであらゆる開発環境を用意できるツール。
    • npm で入れる。
  • Solidity
    • スマートコントラクトを書くための Ethereum の独自言語。自分でランタイムをインストールする必要はなし。

開発環境の準備

Node は使える環境の前提で進める。

まずは hardhat で開発のテンプレートを作成。

mkdir minimal-nft
cd minimal-nft
npm init --yes
npm install --save-dev hardhat
npm install @openzeppelin/contracts web3
npx hardhat

What do you want to do?と聞かれるのでCreate a basic sample projectを選択。するとテンプレートが作られる。

諸々作られたら下記を実行してテストが通るかチェック。1 passingになれば問題なし。

npx hardhat test

コントラクトを書く

contracts/Greeter.solを削除して、contracts/MYNFT.solを作成して下記のコードを書く。

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol";

contract MYNFT is ERC721PresetMinterPauserAutoId {
    constructor()
        ERC721PresetMinterPauserAutoId("MYNFT", "nft", "https://enftxamplebucket.s3.amazonaws.com/my-tokens/")
    {}
}

コントラクトは以上。強いて言えばhttps://enftxamplebucket.s3.amazonaws.com/my-tokens/が重要。この URL が NFT のコンテンツ URL の基礎になる。

シンプルな NFT であればOpenZeppelinの提供するライブラリを継承するだけで済むので他には何も定義しなくて問題ない。

デプロイ(ローカルネットワーク)

まずはローカル環境にネットワークを立ち上げてそこへ先ほど書いたコントラクトをデプロイしてみる。下記でローカルにネットワークが作成され、10000ETH 持ったアカウントも同時に 20 個ほど作成される。

npx hardhat node

次にscripts/sample-script.jsを削除して、scripts/deploy.jsを作成。下記のコードを書く。

const hre = require("hardhat");

async function main() {
  const NFT = await hre.ethers.getContractFactory("MYNFT");
  const nft = await NFT.deploy();

  await nft.deployed();

  console.log("NFT deployed to:", nft.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

そして別のタブを開いて、下記を実行。

npx hardhat run --network localhost scripts/deploy.js

するとMYNFT deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3のようなコメントが表示されるはずなので0x5FbDB2315678afecb367f032d93F642f64180aa3の部分はメモ。

NFT の発行

まずscripts/mint.jsを作成。下記のコードを書く。

CONTRACT_ADDRESSには先ほどメモしたものを記述。PUBLIC_KEYPRIVATE_KEYには先ほどnpx hardhat nodeを実行したときに自動で作成されたアカウントの Account #0: を選び、その Account #0: の方をPUBLIC_KEYに、Private Key: の方をPRIVATE_KEYに設定。(※ 先ほどデプロイした時のアカウントがデフォルトで Account #0。今はコントラクトをデプロイしたアカウントでしか NFT は発行できない仕様にしている。)

const Web3 = require("web3");

// ADDRESS, KEY and URL are examples.
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const PUBLIC_KEY = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
const PRIVATE_KEY =
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const PROVIDER_URL = "http://localhost:8545";

async function mintNFT() {
  const web3 = new Web3(PROVIDER_URL);
  const contract = require("../artifacts/contracts/MYNFT.sol/MYNFT.json");
  const nftContract = new web3.eth.Contract(contract.abi, CONTRACT_ADDRESS);
  const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest");
  const tx = {
    from: PUBLIC_KEY,
    to: CONTRACT_ADDRESS,
    nonce: nonce,
    gas: 500000,
    data: nftContract.methods.mint(PUBLIC_KEY).encodeABI(),
  };

  const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY);
  signPromise
    .then((signedTx) => {
      const tx = signedTx.rawTransaction;
      if (tx !== undefined) {
        web3.eth.sendSignedTransaction(tx, function (err, hash) {
          if (!err) {
            console.log("The hash of your transaction is: ", hash);
          } else {
            console.log(
              "Something went wrong when submitting your transaction:",
              err
            );
          }
        });
      }
    })
    .catch((err) => {
      console.log("Promise failed:", err);
    });
}

mintNFT();

そして下記を実行。するとPUBLIC_KEYに設定した Account に対してトークン ID が1の NFT が発行される。さらにもう一度実行するとトークン ID が2の NFT が発行される。

npx hardhat run --network localhost scripts/mint.js

確認

ちゃんと発行できているか確認するためにscripts/view.jsを作成。発行した個数を表示できるようにする。

先ほどと同様にCONTRACT_ADDRESSPUBLIC_KEYを記述。

const Web3 = require("web3");

// ADDRESS, KEY and URL are examples.
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const PUBLIC_KEY = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
const PROVIDER_URL = "http://localhost:8545";

async function viewNFT() {
  const web3 = new Web3(PROVIDER_URL);
  const contract = require("../artifacts/contracts/MYNFT.sol/MYNFT.json");
  const nftContract = new web3.eth.Contract(contract.abi, CONTRACT_ADDRESS);
  nftContract.methods.balanceOf(PUBLIC_KEY).call().then(console.log);
}

viewNFT();

そして下記を実行。するとscripts/mint.jsを実行した回数が表示されるはず。

npx hardhat run --network localhost scripts/view.js

デプロイ(テストネットワーク/本番ネットワーク)

ローカル環境以外のネットワークにデプロイする場合はいくつか注意が必要になる。

  • まずスマートコントラクトは一度デプロイすると基本的に変更できない。もしセキュリティ上の問題があった場合などは詰み。なのでデプロイする前にローカル&テスト環境で十分に検証を行う必要がある。
  • またデプロイするためにガス代がかかる。これも ETH の場合は数千円を普通に超えるので慎重に行いたい。
  • 自分用のウォレットが必須。MetaMask をインストールしておく。

上記を頭に入れた上で外部のネットワークへのデプロイ方法を解説する。

ウォレットの設定

まずはMetaMask の ChromeExtensionをインストールする。そして新規登録を行う。

これで自分用のウォレット(アカウント)ができたので、このウォレットアドレスとウォレットの秘密鍵を使ってデプロイを行なっていく事になる。先ほどのローカル環境ではAccount #0のものを使っていたが、これが自分のウォレットのものに代わるだけ。

ちなみに当然だけど 自分のウォレットの秘密鍵は公開したら詰み なので気をつけること。パスフレーズも同様。

ネットワークへの接続準備

※追記
以下過去記事なのでRopstenで説明していますが、今後はThe Mergeによってropstenは停止されます。なのでGoerliをテストネットとして使ってください。

テストネットワークや本番ネットワークへの接続方法について。ローカルであればノードは自分のマシンのみで、そのノード内でブロックチェーンもコントラクトもアカウントも完結していた。しかし外部のネットワークに接続するにはまず 接続したいネットワークに所属しているノード へと繋がないといけない。

ネットワークにすでに接続しているノードを提供してくれているサービスがいくつかあるのでこれを利用する。ここではalchemyを使う。INFURAというのもあるがどちらでも OK。

alchemy への登録方法などは割愛するが、欲しいのは適当にプロジェクトを作成してVIEW KEYというところから得られるhttps://eth-ropsten.alchemyapi.io/v2/abcddddaeeaonviabibviasiexampleのような HTTP URL。この URL に対して先ほどローカルで行ったようにデプロイを実行することでコントラクトがネットワークで使えるようになる。

alchemy では Ethereum と Polygon というブロックチェーンが選択できる。今回はテストネットワークへのデプロイまでしか行わない(というかガス代が高すぎて本番ネットワークへのデプロイは無理...)ので Ethereum の Ropsten ネットワーク を選択する前提で進む。

Ropsten というのは Ethereum に存在するテストネットワークのうちの一つ。他にも Kovan,Rinkeby,Goerli というテストネットワークが存在する。どれを使うかは自由。今回 Ropsten を選んだのは faucet がまだギリギリ機能しているから。テストネットワークでも当然ガス代が発生するが、現実の ETH を使う必要はない。faucet というのはテストネットワークでのみ使える暗号通貨を得るための場所。Ropsten Ethereum Faucetという場所を自分は利用した。自分のウォレットのアドレス(秘密鍵ではなく公開鍵の方)をフォームに入力して待つとテスト用の ETH がウォレットに送られてくる。ただ下手すると 1 日くらい待たないと送られてこないこともよくあるので辛抱強く待つか、他の faucet を探してみると良い。 秘密鍵の入力パスフレーズの入力ウォレットの権限を余計に要求する ようなサイトを使わなければ大丈夫なはず。

デプロイ設定

hardhat.config.jsを下記のように編集する。

require("@nomiclabs/hardhat-waffle");

task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
  networks: {
    ropsten: {
      url: "Alchemyで得たropstenのURL",
      accounts: "自分のウォレットの秘密鍵",
    },
    mainnet: {
      url: "Alchemyで得たEthereumのmainnetのURL",
      accounts: "自分のウォレットの秘密鍵",
    },
  },
};

これで下記のように実行すればデプロイできる。ただし十分なガス代がウォレットにあればの話。faucet で何も得られていなければデプロイすらできない。

npx hardhat run --network ropsten scripts/deploy.js

NFT の発行/確認

ローカル環境の時のスクリプト内にあるPUBLIC_KEYPRIVATE_KEYを自分のウォレットのアドレスにして、PROVIDER_URLを alchemy で得た URL にそれぞれ変える。あとは実行時に--networkの部分を実行したいネットワーク名に変更して叩けば良いだけ。

コンテンツのアップロードについて

先ほどhttps://enftxamplebucket.s3.amazonaws.com/my-tokens/42のような URL が ERC721 に準拠した JSON を返せば良いと書いた。なので実際はhttps://enftxamplebucket.s3.amazonaws.com/my-tokens/42が JSON を返すようにする必要もある。

例えば上記の例だとまず S3 に 42 というファイル名の JSON ファイルを設置する。重要なのは NFT 発行した時のトークン ID とファイル名が一致すること。

{
  "name": "Juice=Juice",
  "image": "https://enftxamplebucket.s3.amazonaws.com/assets/hoge.jpeg",
  "external_url: "http://www.helloproject.com/juicejuice/",
  "description": "We are J=J."
}

JSON ファイルの内容はとりあえずは上記で良いだろう。imageに記載されている画像も S3 にアップロードしておくのを忘れない。

これであとは発行した NFT の ID が1なら1という名前の JSON ファイルを S3 の同じ場所に設置すれば良いし、2なら2という名前の JSON ファイルを S3 の同じ場所に設置すれば良い。こうしてコントラクトで発行した NFT が JSON ファイルの指し示す画像 URL とリンクされるという仕組み。

画像はブロックチェーン上にないの??という疑問を持つ人もいると思うが、その通りで基本的にはチェーン上にはない。チェーン上にあるのは「誰がどの NFT を持っているのかという記録」のみ。https://enftxamplebucket.s3.amazonaws.com/my-tokens/42という URL が返す JSON ファイルが消えたらそれで終わり。虚無のトークンを保有する事になるはず。

なのでできるだけ消えないように IPFS(ブロックチェーンのように P2P で分散ノードにファイルを分割&分散させて保存しておく仕組み)を使うことが推奨されている。Brave というブラウザを使うとipfs://ipfs.io/Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiuという感じの ipfs プロトコルのURLでコンテンツにアクセスできる。ちなみにChrome ではまだ対応してない。

まとめ

主に Ethereum のスマートコントラクトと NFT に関する解説と独自コントラクトで NFT を発行する簡単な実装方法について書いた。コードは GitHub に公開してある。

https://github.com/YuheiNakasaka/minimal-nft

また、自分の勉強メモは Zenn のスクラップに雑にまとめてあるのでどういう順で勉強してたかは参考になるかもしれない。

https://zenn.dev/razokulover/scraps/b85e061d8c06fd

あとは Ethereum に関しては良い学習リソースがたくさんあるので下記に軽く紹介しておく。全て英語だが、BlockChain(特にスマートコントラクトや Web3.0) の情報は英語以外でまともな情報は得られないので英語を避けて通ることは不可能。Ethereum のドキュメントくらいのレベルで良いので読みこなせないと多分大変だと思う。

最後にもし記事が役に立ったら ETH,Polygon 等投げ銭してもらえたらありがたいです。
0xfB9AaE55f46F03a2FF53882b432Fbf52Fc6B668F

その他何かあれば@razokuloverに連絡してもらえれば気まぐれで対応できるかも。

Discussion