HiÐΞClassic

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

ぽんた / Kenta Suhara
3 years ago
画像の用意IPFSへのアップロードメタデータの用意コントラクトの作成コントラクトのデプロイフロントエンドOpenSeaで表示おわりに

ブロックチェーン界隈ではコレクタブル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

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

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

IPFSへのアップロード

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

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

スクリーンショット 2021-10-02 16.44.52.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が入ります。

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

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

//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を使ってデプロイしてみました。

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に以下を追加

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

無事表示できました!

おわりに

以上のようにsolidity開発を少しでもやったことがある人なら簡単にコレクティブルは作れます。

次回は②generativeな表現をするスクリプトをipfs上に置くパターン、③コントラクト上でsvgを生成するフルオンチェーンパターンについてもメモを残そうと思います。

githubのレポジトリも置いておくので参考にしてみてください。 https://github.com/ksuhara/kawaii-yatsura

もし役に立っていたら投げ銭よろしくお願いします!作ったNFTをairdropしてくれても嬉しいです! 0xB6Ac3Fe610d1A4af359FE8078d4c350AB95E812b


コメント
いいね
投げ銭
最新順
人気順
ぽんた / Kenta Suhara
3 years ago
コメント
いいね
投げ銭
最新順
人気順
トピック
アプリ

Astar Network

DFINITY / ICP

NFT

DAO

DeFi

L2

メタバース

BCG

仮想通貨 / クリプト

ブロックチェーン別

プロジェクト

目次
Tweet
ログイン