NFTを自分でつくってみる

# はじめに

今話題となっているNFTについて、自分が身につけた技術を活用しながら**軽め**に実装してみようと思います。
NFTの開発を通してブロックチェーン技術についての知見共有になれば幸いです。

<dl>
<dt>この記事の対象者</dt>
<dd>NFTがやたらと話題になっているけど、中身はどんなものが動いているの？というのが気になる方</dd>
<dt>やること</dt>
<dd>ブロックチェーンの開発言語やフレームワークを利用したスマートコントラクト開発(題材としてNFT)</dd>
<dt>やらないこと</dt>
<dd>ブロックチェーンやNFTの知識・仕組みの詳解(NFTの概要のみ記述)</dd>
</dl>

:::note warn
ブロックチェーンの各用語・各技術については最低限しか触れていないので、不明点は他ソースなどを参照ください。
:::

:::note info
スマートコントラクト
契約の自動化する仕組み。ブロックチェーンよりも前から提唱されている概念であり、有名な例として自動販売機がある。
(自動販売機におけるスマートコントラクト：機械による(複数人による合意のない)販売契約の自動化)
ブロックチェーンの中では狭義として、ブロックチェーン上に記録され、自動的に成立するトランザクション(取引記録)のことを表す。
:::

# NFTとは？

2021年のバズワードとも言われるように流行った「NFT」について軽く説明をします。
そもそもNFTとは「Non-Fungible Token」の略で非代替性トークンという意味です。
通貨が代替性のある(1つ1つのトークンに同一の価値のある)トークンとして存在する一方で、トークン同士の代替が不可能なものとしてNFTの仕組みがあります。
非代替性のトークンを利用することで、唯一無二のモノ(NFTでいうとデジタルコンテンツ)に価値をもたせることができます。

NFTを支える仕組みとして、ブロックチェーン技術があります。
ブロックチェーン技術の説明は割愛しますが、ブロックチェーン技術を活用することでNFTを実装することができます。

# 開発環境の準備

今回は下記の開発環境でNFTを作成してみる。
後々出てくる言語やフレームワークなどのversionもまとめて下記にまとめています。

```開発環境まとめ
OS
- Ubuntu-20.04LTS on WSL2(Windows11)

Truffle
- Truffle v5.4.7 (core: 5.4.7)
- Solidity - 0.8.11 (solc-js)
- Node v16.7.0
- Web3.js v1.5.2

Ganache (on Windows)
v2.5.4

VSCode

```

### プロジェクトフォルダの作成 (Truffleの利用)

Ethereumのスマートコントラクト開発用フレームワークである[Truffle](https://trufflesuite.com/docs/truffle/)を利用する。


作業用フォルダ作成後、truffleコマンドを利用してプロジェクトフォルダを作成する。

```shell-session
$ truffle init
```

`truffle init`を実行することで、コントラクト開発に必要な最低限のテンプレートが自動作成されます。

### ネットワーク準備 (Ganacheの利用)

Ethereumのプライベートネットワークを簡単に作成できる[Ganache](https://trufflesuite.com/docs/ganache/)を利用する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175966/7cf25ba7-cc8c-60bc-bae4-afe7f011c9f4.png)
「NEW WORKSPACE」を押して利用するワークスペースの名前を付ける(画像にある`NFT-Sample(ethereum)`は作成したワークスペース)

今回はWSL2環境で開発をすすめるので、SERVERの`HOSTNAME`では画像のようになる形で設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175966/c348947a-6352-c944-5e3d-30f250b920c0.png)

上記設定を元に先程`truffle init`で作成された`truffle-config.js`にネットワーク設定を記載する

```truffle-config.js
//　↑other settings ↑
  networks: {
    development: {
      host: "192.168.112.1",
      port: 7545,
      network_id: "5777",
    }
  }
//　↓other settings ↓
```

上記まで設定し、`truffle cosole`を実行してtruffleのコンソールが表示されればネットワークの準備完了。

```shell-session
$ truffle console
truffle(development)>
```

上記まで終了すれば、ローカル環境でプライベートネットワークを利用して、自分の開発したスマートコントラクトでトークンのやり取りなどができます。

# スマートコントラクトの実装


### ERC721の準備

開発環境が整ったので、実際にNFTを作成するためのスマートコントラクトを開発します。
NFTはERC721に準拠したトークンとなります。

https://ethereum.org/ja/developers/docs/standards/tokens/erc-721/

:::note info
ERC(Ethereum Request for Comments)
トークンの動作を実装するスマートコントラクトの共通の仕様を定めたもの。
:::

上記のように、ERCはあくまでトークンの動作の共通規格(= Interface, 以後IF)を定義したものなので、IFを参照しながら独自にスマートコントラクトを開発することも可能です。

IFを基に独自のスマートコントラクト開発も可能ですが、今回はOpenZeppelinが作ったライブラリを活用することで簡単にERC721に準拠したトークンを作っていきます。

https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721

npmでOpenZeppelinライブラリを導入する。
プロジェクトのルートディレクトリ(`truffle init`を実行したディレクトリ)で`npm install openzeppelin-solidity`を実行する。

```shell-session
$ npm install  openzeppelin-solidity
```

実行後、`node_module`フォルダ配下に`openzeppelin-solidity`フォルダが生成される。

### トークン作成用solidityファイルの作成

独自のNFTを作成するためのsolidityファイルを作成する。

:::note info
Solidity
スマートコントラクトを実装するためのオブジェクト指向プログラミング言語
:::

https://docs.soliditylang.org/en/v0.8.11/

contractフォルダ配下でNFT作成用のsolidityファイルを作成し、OpenZeppelinのERC721を継承する。
これだけでERC721に必要なステートやメソッド、イベントを持ったスマートコンストラクトに早変わりです。

```solidity:MyNFT.sol
pragma solidity 0.8.11;

import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol";

contract MyNFT is ERC721 {
    mapping(uint256 => string) public tokenName;

    constructor(string memory name, string memory symbol)
        ERC721(name, symbol)
    {}

    function createToken(uint256 tokenId, string memory name) external {
        _safeMint(msg.sender, tokenId);
        tokenName[tokenId] = name;
    }

    function getTokenName(uint256 tokenId) external view returns (string memory) {
        return tokenName[tokenId];
    }
}
```

継承元のERC721のコンストラクタの引数に`name_`と`symbol_`があるため、継承先のコンストラクタの引数で対応する引数を入れる必要があります。
ERC721で実装されているステートやメソッド、イベントについては補足として最下部にまとめました。
また、今回は独自の実装として以下のようなステートとメソッド、イベントを定義しました。

<dl>
<dt>tokenName</dt>
<dd>発行したNFTに紐づく名前ステート</dd>
<dt>IssueToken(uint256 tokenId, string tokenName)</dt>
<dd>NFTを発行する関数</dd>
<dt>function getTokenName(uint256 tokenId) external view returns (string memory)</dt>
<dd>対象のNFTの名前を取得する関数</dd>
</dl>

# 動作確認

上記で実装したスマートコントラクトを利用して実際にNFTを発行するところまでやってみます。

### 動作確認準備(Truffle, Ganacheの準備)

環境準備のときに利用したTruffleコンソールを利用していきます。

```shell-session
$ truffle console  
truffle(development)> 
```

まずはスマートコントラクトを新たに実装したので、`compile --all`を実行します。

```shell-session
truffle(development)> compile --all

Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/MyNFT.sol
> Compiling openzeppelin-solidity/contracts/token/ERC721/ERC721.sol
> Compiling openzeppelin-solidity/contracts/token/ERC721/IERC721.sol
> Compiling openzeppelin-solidity/contracts/token/ERC721/IERC721Receiver.sol
> Compiling openzeppelin-solidity/contracts/token/ERC721/extensions/IERC721Metadata.sol
> Compiling openzeppelin-solidity/contracts/utils/Address.sol
> Compiling openzeppelin-solidity/contracts/utils/Context.sol
> Compiling openzeppelin-solidity/contracts/utils/Strings.sol
> Compiling openzeppelin-solidity/contracts/utils/introspection/ERC165.sol
> Compiling openzeppelin-solidity/contracts/utils/introspection/IERC165.sol
> Compilation warnings encountered:

    Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.
--> project:/contracts/MyNFT.sol


> Artifacts written to /home/wataru/Workspace/NFT_Sample/build/contracts
> Compiled successfully using:
   - solc: 0.8.11+commit.d7f03943.Emscripten.clang


truffle(development)> 
```
※ライセンスの明記が無いため、Warningが発生していますが、本トピックでは問題無いため、割愛します。
次に、NFTを発行するために実装したスマートコントラクトをデプロイします。

```shell-session
truffle(development)> instance = await MyNFT.new('testName','testSymbol')
undefined
truffle(development)> instance.transactionHash
'0x8448614004ce5779e4b438e396688b384ee892137c4267008bab4fe63990e3b1'
truffle(development)> 
```

上記のように変数に格納する形でデプロイすることで、`transactionHash`関数を利用してデプロイ時のトランザクションハッシュを確認することが出来ます。

Ganacheの「TRANSACTION」タブを確認するとスマートコントラクトをデプロイしたトランザクションハッシュと一致するトランザクションがあることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175966/60671fff-cd09-d5da-c975-797ca6dc7a0d.png)

### NFT発行と軽いトークン操作

上記まで完了したら実際にNFTを発行できます。上記手順同様にtruffleコンソールにて独自のトークン発行関数`createToken`を利用してNFTを発行します。

```shell-session
truffle(development)> instance.createToken(3141592, 'Hello World!!!')
{
  tx: '0xcd126bad6bcaf558ea57cff55452aeea4b2f8e86d233180c46163452c4b1949b',
  receipt: {
    transactionHash: '0xcd126bad6bcaf558ea57cff55452aeea4b2f8e86d233180c46163452c4b1949b',
    transactionIndex: 0,
    blockHash: '0x07395322279eca89f3aaf978af17e26593d47e9e15a783b04d9ad946ec17052a',
    blockNumber: 2,
    from: '0x516423dcb54d1091d5f035238e4cbccbd620a092',
    to: '0xbcc3c99c160c447488f8f27e42baf866b714697e',
    gasUsed: 92741,
    cumulativeGasUsed: 92741,
    contractAddress: null,
    logs: [ [Object], [Object] ],
    status: true,
    logsBloom: '0x00000000000000000000000000000000000000000200000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000080000000000000000000020000000000400000000800000002000000000000000010000000001000000000080000000000000000000000000000000800000000000001000000000000000000000000000000000000000000000000000000004000000000000000001002000000000000000000000002000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: [ [Object], [Object] ]
  },
  logs: [
    {
      logIndex: 0,
      transactionIndex: 0,
      transactionHash: '0xcd126bad6bcaf558ea57cff55452aeea4b2f8e86d233180c46163452c4b1949b',
      blockHash: '0x07395322279eca89f3aaf978af17e26593d47e9e15a783b04d9ad946ec17052a',
      blockNumber: 2,
      address: '0xbcC3C99C160C447488f8f27e42bAF866b714697E',
      type: 'mined',
      id: 'log_4cb86583',
      event: 'Transfer',
      args: [Result]
    },
    {
      logIndex: 1,
      transactionIndex: 0,
      transactionHash: '0xcd126bad6bcaf558ea57cff55452aeea4b2f8e86d233180c46163452c4b1949b',
      blockHash: '0x07395322279eca89f3aaf978af17e26593d47e9e15a783b04d9ad946ec17052a',
      blockNumber: 2,
      address: '0xbcC3C99C160C447488f8f27e42bAF866b714697E',
      type: 'mined',
      id: 'log_fd8d15ab',
      event: 'IssueToken',
      args: [Result]
    }
  ]
}
```

上記のように`createToken`実行後に沢山のログが流れますが、一番最初の`tx`を参考にGanacheを確認してみると対象のトランザクションログを確認することが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175966/49acad6b-b5fa-8154-e23d-ad7e682f160f.png)

これでNFTの発行まで完了です。

独自のスマートコントラクトで作った`getTokenName`メソッドを利用して発行したトークンの名前を取得することもできます。

```shell-session
truffle(development)> instance.getTokenName(3141592)
'Hello World!!!'
truffle(development)> 
```

また、ERC721ライブラリで実装されているメソッドも利用してトークン操作も可能です。
例えば、`ownerOf`を利用すると指定したNFTの所有者を確認できます。

```shell-session
truffle(development)> instance.ownerOf(3141592)
'0x516423dCb54D1091d5f035238e4CbccBD620a092'
truffle(development)> 
```

今回は独自メソッドでトークン発行者を所有者とする形で実装したので、トークンを発行したアカウントのアドレスが返ってきます。
アカウントのアドレスもGanacheで確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175966/0b58f521-61b6-c8d9-6be8-1cd3f07fb373.png)

上記のようにERC721の標準規格を利用することで、対象アカウントがいくつトークンを所有しているのかを確認したり、トークンの移譲をするなどのトークン操作を簡単に実施出来ます。

以上で簡単にNFTの発行から操作まで実践してみました。

# さいごに
話題になってる波に乗ってブロックチェーンの勉強がてらNFTの中身に触れてみました。
ERC721にはこの記事をきっかけで触っていること、まだまだブロックチェーン技術に半人前なので分かりづらい箇所もあったと思いますが、
少しでもNFTやブロックチェーン技術について知るきっかけになれば嬉しいです。

ご指摘事項などあれば優しく教えて下さい。。

### 参考文献

文献中に出てきていない参考文献となります。

- [TruffleフレームワークとOpenZeppelinライブラリでERC20準拠の独自トークンを実装してみる](https://qiita.com/watanabe_kentaro/items/4709874c6ac18d605609)
- [【solidity】スマートコントラクトで複雑なNFTを作るまで](https://cam-inc.co.jp/p/techblog/603394496142509097)
- [ERC-721におけるsetApprovalForAllを理解する](https://qiita.com/rmanzoku/items/a90f265ae499dc21b9cb)
 

# 【補足】ERC721の中身

もともとERC721で持っているステートやメソッド、イベントについても上述したOpenZeppelinライブラリを参考に軽くまとめます。

https://ethereum.org/ja/developers/docs/standards/tokens/erc-721/

### ステート
※変数名などに具体的な規則はないため、変数名は[OpenZeppelinのERC721](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol)より引用

<dl>
<dt>_name</dt>
<dd>トークンコレクションの名前</dd>
<dt>_symbol</dt>
<dd>トークンのシンボル</dd>
<dt>_owners</dt>
<dd>トークンの所有者</dd>
<dt>_balances</dt>
<dd>各トークン所有者が所持しているトークンの数</dd>
<dt>_tokenApprovals</dt>
<dd>トークンのやり取りについて承認されているアドレス</dd>
<dt>_operatorApprovals</dt>
<dd>トークン所有者として許可するオペレータのアドレス</dd>
</dl>

#### メソッド

<dl>
<dt>balanceOf(address _owner) external view returns (uint256)</dt>
<dd>指定したアドレス(_owner)が所持しているトークンの数を返却する</dd>
<dt>ownerOf(uint256 _tokenId) external view returns (address)</dt>
<dd>指定したトークン(_tokenId)の所有者のアドレスを返却する</dd>
<dt>safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable</dt>
<dd>安全に移転元(_From)から移転先(_to)に指定したトークン(_tokenId)を移転する</dd>
<dt>safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable</dt>
<dd>安全に移転元(_From)から移転先(_to)に指定したトークン(_tokenId)を移転する</dd>
<dt>transferFrom(address _from, address _to, uint256 _tokenId) external payable</dt>
<dd>指定したトークン(_tokenId)の所有権を別のアドレス(_to)に譲渡する(基本的に利用非推奨)</dd>
<dt>approve(address _approved, uint256 _tokenId) external payable</dt>
<dd>指定したトークン(_tokenId)の所有権を指定したアドレス(_approved)に移転する</dd>
<dt>setApprovalForAll(address _operator, bool _approved) external</dt>
<dd>オペレータ(_operator)の承認設定を変更する</dd>
<dt>getApproved(uint256 _tokenId) external view returns (address)</dt>
<dd>トークン(_tokenId)の所有者として承認されているアドレスを返却する</dd>
<dt>isApprovedForAll(address _owner, address _operator) external view returns (bool)</dt>
<dd>オペレータ(_operator)がトークン所有者(_owner)によって承認されているかどうかを返却する</dd>
</dl>

#### イベント

<dl>
<dt>Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId)</dt>
<dd>移転元(_form)が移転先(_to)にどのトークン(_tokenId)を移転したのかをトランザクションログに書き込む</dd>
<dt>Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId)</dt>
<dd>トークン所有者(_owner)が承認先(_approved)に指定したトークン(_tokenId)を移譲の承認することをトランザクションログに書き込む</dd>
<dt>ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved)</dt>
<dd>トークン所有者(_owner)に対応したオペレータ(_operator)がトークン操作可能であるかをトランザクションログに書き込む</dd>
</dl>