コレクタブルNFTを作ってみる①事前にオフチェーンで画像を用意するパターン

ブロックチェーン界隈ではコレクタブルNFTというものが流行しています。CryptoPunks、Hashmasks、BAYCのように、一定数（100~10,000個くらい）の似た絵柄のNFTシリーズのことを指します。保有していることがデジタル上の一種のステータスになったり、コミュニティが形成されています。

コレクタブルNFTを技術的に見てみると、アイテムの表現方法で大きく分けて3パターンに分類できます。

①画像はオフチェーンで重ね合わせて事前に生成しておき、ipfs上に置くパターン
→ Bored Ape Yacht Club、Pudgy Penguinsなど
②generativeな表現をするスクリプトをipfs上に置くパターン
→ Generativemasksなど
③コントラクト上でsvgを生成するフルオンチェーンパターン
→ Blitmapなど

今回は一番シンプルな①の方法についてコントラクトとmetadataの作り方を中心に解説します。アジェンダは以下の通りです。

- 画像の用意
- IPFSへのアップロード
- メタデータの用意
- IPFSへのアップロード
- コントラクトの作成
- コントラクトのデプロイ

## 画像の用意
イラストを用意する方法は自由です。手書きで頑張って1枚1枚描いてもいいです。一般的には、パーツごとに複数種類の透過pngを用意して、pythonやjavascriptなどで自動的に重ね合わせて大量に画像を生成する方法をとると思います。

![スクリーンショット 2021-10-02 16.16.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505435/6d25e101-d487-9b2e-b50f-9000111409a3.png)

こちらのコードはかなりテキトーですが、僕の絵心が爆発したかわいいキャラクターたちが出来上がりました。

```javascript:index.js
const sharp = require("sharp");
const fs = require('fs');

const main = async ()  =>{
    let attributesForCheck = []

    for (let i = 0; i < 10; i++) {
        const rand = Math.floor(Math.random() * 100)
        let bodyType = ""
        if (rand < 20) {
            bodyType= "body0"
        } else if (rand < 50) {
            bodyType= "body1"
        } else if (rand < 70) {
            bodyType= "body2"
        } else {
            bodyType= "body3"
        };

        const earRand = Math.floor(Math.random() * 100)
        let earType = ""
        if (earRand < 30) {
            earType= "ear0"
        } else if (earRand < 60) {
            earType= "ear1"
        } else {
            earType= "ear2"
        };

        const glassesRand = Math.floor(Math.random() * 100)
        let glassesImage = ""
        if (glassesRand < 50) {
            glassesImage= "./images/glasses.png"
        } else {
            glassesImage= "./images/none.png"
        }

        const key = bodyType + earType + glassesImage
        const index = attributesForCheck.findIndex((element => element === key))
        if(index != -1) return

        await sharp( "./images/face.png" )
           .composite([ 
                  {
                    input: `./images/${earType}.png` ,
                    gravity:"northwest",
                }, {
                    input: `./images/${bodyType}.png` ,
                    gravity:"northeast",
                }, {
                    input: glassesImage ,
                    gravity:"northeast",
                },
           ] )
          .toFile( `./output/test${i}.png` );

          attributesForCheck.push(bodyType + earType + glassesImage)
        });
    }
}

main()
```

![スクリーンショット 2021-10-02 16.29.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505435/235519f9-1188-afe6-1687-9d7b97ef3417.png)

## IPFSへのアップロード
NFTの画像データは分散型ストレージに保存されることが多いです。中央管理のサーバーでも構いませんが、管理者が自由に変更したり管理をやめてしまう可能性もあるので、分散型ストレージで管理されるほうが価値が高くなる傾向があります。IPFSは分散型ストレージの一つです。ここはArweaveなどでも構いません。

IPFSにアップロードする方法はご自身で調べてください。ポイントとしてはFolderごとアップロードすることです。後述のメタデータの作成がラクになります。

![スクリーンショット 2021-10-02 16.44.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505435/75f3dd0b-97d0-67a8-33f6-64e836d25056.png)

https://ipfs.io/ipfs/QmcgHEHy1MwGK3xfQ6L7uPooNHG6uQEeqqbS9QSHNCJbKe



## メタデータの用意
NFT（ERC721）のコントラクトにはtokenURIという関数があり、その実行結果がNFTの画像などの情報を返します。 現在はOpneSeaが規格を定めている状況です。https://docs.opensea.io/docs/metadata-standards

必須の項目としては

- name: トークンの名前
- description： トークンの説明
- image: 画像のURL

くらいでしょうか。

imageには先ほどIPFSにアップロードした画像のURLが入ります。

```javascript:create-metadata.js
const fs = require('fs');

const createFile = () => {
        for (i = 0; i < 10; i++){
            const testObj = {
                name: `kawaii yatsu #${i}`,
                description: 'kawaii yatsu is a collectible made by @suhara_ponta',
                image: `https://gateway.pinata.cloud/ipfs/QmcgHEHy1MwGK3xfQ6L7uPooNHG6uQEeqqbS9QSHNCJbKe/test${i}.png`,
            };
            const toJSON = JSON.stringify(testObj);
            fs.writeFile(`./metadata/${i}`, toJSON, (err) => {
                if (err) console.log(err)
                if (!err) {
                console.log(`JSONファイルを生成しました${i}`);
                }
            });
        }
};

createFile();
```
こちらで生成したjsonのフォルダもIPFSにアップロードします。

https://ipfs.io/ipfs/QmVWcD2MSZcKRcaFpmVAbBqz7sw8eMpTFDyjt8ugZzGxwa


## コントラクトの作成

hardhatを使った基本的なコントラクト開発については以前書いたこちらの記事を参考にしてください。
https://qiita.com/ksuhara/items/55296e5098bc27061d13

ここではコントラクトのみ書きます。


```solidity:KawaiiYatsura.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Context.sol";

contract KawaiiYatsura is Context, ERC721, ERC721Enumerable, Ownable {
    using SafeMath for uint256;

    string private _baseTokenURI;
    uint256 public constant MAX_ELEMENTS = 10;
    uint256 public constant price = 1000000000000000000; //1 ETH / 1 MATIC

    constructor(
        string memory name,
        string memory symbol,
        string memory baseTokenURI
    ) ERC721(name, symbol) {
        _baseTokenURI = baseTokenURI;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    function buy() public payable {
        require(totalSupply() < MAX_ELEMENTS, "Purchase would exceed max supply of NFTs");
        require(price <= msg.value, "Ether value sent is not correct");
        uint256 mintIndex = totalSupply();
        _safeMint(msg.sender, mintIndex);
    }

    function withdraw() public onlyOwner {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }
}
```

OpenZeppelinのpresetを参考に必要なfunctionを実装しつつ、独自で実装しているのはbuyとwithdrawのみです。

buy関数では`msg.value`が価格より大きいかを確認し、NFTを`msg.sender`に対してmintします。

withdraw関数ではコントラクトに入ってる売り上げをコントラクトのownerが引き出せるようにしています。

## コントラクトのデプロイ
hardhat-deployを使ってデプロイしてみました。
https://hardhat.org/plugins/hardhat-deploy.html

```javascript:deploy/00_KawaiiYatsura.js
module.exports = async ({getNamedAccounts, deployments}) => {
    const {deploy} = deployments;
    const {deployer} = await getNamedAccounts();
    await deploy('KawaiiYatsura', {
      from: deployer,
      args: ['KawaiiYatsura', 'KY', "https://ipfs.io/ipfs/QmVWcD2MSZcKRcaFpmVAbBqz7sw8eMpTFDyjt8ugZzGxwa/"] ,
      log: true,
    });
  };
module.exports.tags = ['KawaiiYatsura'];
```
hardhat.config.jsに以下を追加

```javascript:hardhat.config.js
require('hardhat-deploy');

const privateKey = process.env.PRIVATE_KEY || "0x0000000000000000000000000000000000000000000000000000000000000000";

module.exports = {
  solidity: "0.8.4",
  namedAccounts: {
    deployer: 0,
  },
  networks: {
    localhost: {
      timeout: 50000,
    },
    polygon: {
      url: "https://polygon-mainnet.infura.io/v3/7495501b681645b0b80f955d4139add9",
      accounts: [privateKey],
      gas: 2100000,
      gasPrice: 8000000000,
    },
    mumbai: {
      url: "https://polygon-mumbai.infura.io/v3/7495501b681645b0b80f955d4139add9",
      accounts: [privateKey],
      gas: 2100000,
      gasPrice: 8000000000,
    },
  },
};
```
Polygonとそのテストネットのmumbaiにデプロイしていきます。
環境変数PRIVATE_KEYにMATIC、TMATICが入っているウォレットの秘密鍵を設定します。

`yarn hardhat deploy --network mumbai`
`yarn hardhat deploy --network polygon`

## フロントエンド
自由に作成してデプロイしてください。
https://kawaii-yatsura.web.app/

mintボタンを設置しているのでmintしてみてください。1個1MATICで10個限定です。

## OpenSeaで表示
https://opensea.io/assets/matic/0xea88d85599bedee5c52e0e2d90773b324d61945e/0
![スクリーンショット 2021-10-03 14.50.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505435/1cdbbf7f-92d6-1295-4584-71ce7db71ab8.png)

無事表示できました！

## おわりに
以上のようにsolidity開発を少しでもやったことがある人なら簡単にコレクティブルは作れます。
次回は②generativeな表現をするスクリプトをipfs上に置くパターン、③コントラクト上でsvgを生成するフルオンチェーンパターンについてもメモを残そうと思います。