MENU

SolidityとReactでNFTのmintingページを作ってみる【ethers.js, Hardhat】

こんにちは、たいきです(@taiki_16_k)

今回はSolidityを使って簡単なNFTのmintingページを作ってみたので解説します。

 

この記事では、下記の動画を参考にコードの解説をしています。

英語の動画ですが、すごくわかりやすい動画のため一度手を動かしてみることをおすすめします。

 

フロントエンドにはReactを使っています。

 

目次

SolidityとReactでNFTのmintingページを作ってみる

 

今回の全体像としては以下の通り。

 

  1. 必要なものをインストール
  2. コントラクトを書く(Solidity)
  3. コントラクトをテストネットにデプロイする(Rinkeby Network)
  4. Mintingサイトのフロントエンドを作る(React.js)
  5. スタイリング(CSS)

 

今回はコントラクトの書き方とコントラクトとのやりとりがメインになるため、⑤については深く解説しません。

それでは実際にやっていきましょう。

 

必要なものをインストール

 

ReactテンプレとHardhatをインストール

 

まずはReactのテンプレートをインストールします。

※実行するディレクトリに注意してください。プロジェクトフォルダに移動したのを確認してから実行してください。

npx create-react-app full-mint-website

 

作成されたフォルダに移動し、Hardhatをインストール。

Hardhatとは、Solidityの開発環境のことで、コントラクトのデプロイなどが簡単にできたりします。

cd full-mint-website

npm i -D hardhat

 

Hardhatのテンプレを入れます。

色々聞かれますがEnterキーを4回押せばOK

npx hardhat

 

これで開発環境はOKです。

 

ライブラリをインストール

 

[jin_icon_check color=”#28467A” size=”20px”]OpenZeppelin

OpenZeppelinは、NFTコントラクトを作る規格、ERC721などのテンプレを提供してくれるライブラリです。Ethereum上の開発では必須です。

npm i @openzeppelin/contracts

 

[jin_icon_check color=”#28467A” size=”20px”]Chakra

Chakraは、Reactのフロントエンド開発を簡単にしてくれるキットです。

React版のBootstrap的な感じ。

npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6

 

[jin_icon_check color=”#28467A” size=”20px”]dotenv

dotenvはプライベートキーなど隠したい重要なキーなどを管理することのできるライブラリです。

npm i -D dotenv

 

dependencyをインストール

 

Hardhatのプラグインです。

npm install -D @nomiclabs/hardhat-waffle ethereum-waffle

 

後ほどもう少しインストールするものが出てきますが、一気にやってしまうとなんのためにインストールしているのかわかりづらいため都度紹介していきます。

 

テンプレートの整理

 

上記がインストールできたら一旦コード・ファイルを整理しましょう。

  • srcフォルダlogo.svg, reportWebVitals.js, setupTests.jsは使わないので削除
  • index.jsのimport reportWebVitals…を削除。ついでに下のコメントとreportWebVitals()も削除
  • App.jsのimport logo, <header>を削除
  • contractsのGreeter.solも削除→新しくRoboPunksNFT.solというコントラクトを作成

こんな感じのフォルダ構成になるはずです。

※assetフォルダの中身はこちらからダウンロードできます

それでは早速RoboPunksNFT.solにコントラクトを書いていきましょう。

 

①Solidityでコントラクトを書く

 

ソースコードはこちら。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

//必要なライブラリをインポート
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract RoboPunksNFT is ERC721, Ownable {
    uint256 public mintPrice;
    uint256 public totalSupply;
    uint256 public maxSupply;
    uint256 public maxPerWallet;
    bool public isPublicMintEnabled;
    string internal baseTokenUri;
    address payable public withdrawWallet;
    mapping(address => uint256) public walletMints;

//ミント時のプライスや最大供給量をデプロイと共に定義
    constructor() payable ERC721("RoboPunksNFT", "RP") { //ERC721に名前とシンボルを渡してあげる
        mintPrice = 0.02 ether;
        totalSupply = 0;
        maxSupply = 1000;
        maxPerWallet = 3;
        //set withdraw wallet address
    }

        //この変数がtrueでなければmintできないようにする(etherscanから操作できます)
    function setIsPublicMintEnable(bool isPublicMintEnabled_) external onlyOwner {
        isPublicMintEnabled = isPublicMintEnabled_;
    }

    //NFTのメタデータが読み込まれるベースURL(このURL以下にjsonファイルが置かれます)
    function setBaseTokenUri(string calldata baseTokenUri_) external onlyOwner {
        baseTokenUri = baseTokenUri_;
    }

    //それぞれのNFTのメタデータが入ったjsonファイルをtoken_idごとに返す
    function tokenURI(uint256 tokenId_) public view override returns (string memory) {
        require(_exists(tokenId_), "Token does not exist");
        //solidityでの文字列の連結はabi.encodePackedを使う
        return string(abi.encodePacked(baseTokenUri, Strings.toString(tokenId_), ".json"));
    }

    //ETHを引き出すときの関数
    function withdraw() external onlyOwner {
        (bool success,) = withdrawWallet.call{ value: address(this).balance }("");
        require(success, "withdraw failed");
    }

    //NFTをMint
    function mint(uint256 quantity_) public payable {
        //mintできるようにトグルがオンになっているかチェック
        require(isPublicMintEnabled, "minting not enabled");

                //mintしようとしているウォレットの送金額はmintPriceと同じか
        require(msg.value == quantity_ * mintPrice, "wrong mint value");

                //最大供給量に達していないか(売り切れていないか)
        require(totalSupply + quantity_ <= maxSupply, "sold out");

        //既にウォレットにあるこのNFT量が、一つのウォレットにもてる量(maxPerWallet)を超えていないか
        require(walletMints[msg.sender] + quantity_ <= maxPerWallet, "exceed max wallet");

               //新しいtoken_idを現在の供給量(totalSupply)に1を足して生成
       //_safeMint関数でMintする
        for (uint256 i  = 0; i < quantity_; i++) {
            uint newTokenId = totalSupply + 1;
            totalSupply++;
            _safeMint(msg.sender, newTokenId);
        }

   }
}

 

それぞれに注釈をつけたので大体何をやっているかわかると思います。

mintやtokenURIなどの関数はOpenZeppelinのtoken/erc721.solという世界標準規格から来ています。

 

スマートコントラクトを書くのはこれだけです。

続いては、コントラクトをテストネットにデプロイするための各種設定ファイルを書いていきましょう。

 

②コントラクトをテストネットにデプロイする

 

上記で作成したスマートコントラクトをブロックチェーン上にデプロイするために設定ファイル(configファイルなど)を少しいじる必要があります。

今回は、コントラクトのデプロイにHardhatを使用しています(始めにインストールしましたね)

 

デプロイ用のスクリプトを編集

 

Src<Scripts<Simple-script.jsのファイル名を”deployRoboPunksNFT”に変更

“Greeter”と書かれているような部分をRoboPunksNFTに変更します

 

こんな感じのコードになります↓

const hre = require("hardhat");

async function main() {
  
  //プロジェクト名に変更
  const RoboPunksNFT = await hre.ethers.getContractFactory("RoboPunksNFT");

  //Constructorの引数に何も取ってないのでdeploy()の中身を削除してください
  const roboPunksNFT = await RoboPunksNFT.deploy();

  await roboPunksNFT.deployed();

  console.log("RoboPunksNFT deployed to:", roboPunksNFT.address);
}


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

 

これでdeployのためのスクリプトは完了です。

 

.envファイルを作成

 

始めにインストールしたdotenvを使って.envファイルに環境変数を書いていきます。

githubに大事な秘密鍵などをアップしないように、.envファイルに書くわけです。

.gitignoreみたいなもんですね。

 

まずはプロジェクトルートに.envを追加しましょう。

※ファイルの場所を間違えると環境変数がundefindになってしまうので注意です

このファイルに秘密鍵やApiKeyなどを書いていきます。

 

// Infraからコピー
REACT_APP_RINKEBY_RPC_URL='https://rinkeby.infura.io/v3/YOURRPCURL'
// Ethscanからコピー
REACT_APP_ETHSCAN_KEY='YOURAPIKEY'
// メタマスクからコピー
REACT_APP_PRIVATE_KEY='YOURPRIVATEKEY'

 

[jin_icon_check color=”#6B9FCE” size=”20px”]Infra

今回はInfraというホスティングサービスを使用しています。

Infraを使えば自分でノードを構築することなく開発者が簡単にdappsなどを作ることができます。

 

公式サイトから登録してRinkebyネットワークでプロジェクトを作成してください。*無料です

その後、RPCサーバーのURLを.envファイルに記述します。

 

それぞれ自分のものに編集してください。

 

[jin_icon_check color=”#6B9FCE” size=”20px”]Ethscan

EthscanのAPIKeyを入力してください。Rinkeby ネットのEthscanである必要があるのでしっかり確認してください。

Ethscan Rinkeby

[jin_icon_check color=”#6B9FCE” size=”20px”]Private Key

メタマスクのプライベートキーを入力します。

メタマスクを開いて右上の点が3つあるところを開きます。

その後「アカウントの詳細」というところを開きます。すると下記画面が表示されるので「秘密鍵のエクスポート」をすると秘密鍵が表示されます。

[2col-box]
[2-left]

[/2-left]
[2-right]

[/2-right]
[/2col-box]

 

これで.envファイルはOKです。

 

hardhatのconfigファイル

 

hardhat.config.jsファイルを下記のように編集します。

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

const dotenv = require("dotenv");

dotenv.config();
console.log(process.env.REACT_APP_RINKEBY_RPC_URL) // remove this after you've confirmed it working


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);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.15",
  networks: {
    rinkeby: {
      url: process.env.REACT_APP_RINKEBY_RPC_URL,
      accounts: [process.env.REACT_APP_PRIVATE_KEY]
    },
  },
  etherscan: {
    apiKey: process.env.REACT_APP_ETHSCAN_KEY
  },
};

 

envファイルを使ってそれぞれのKEYなどを渡します。

 

③コントラクトをデプロイ!

 

ようやくコントラクトをデプロイできます!

まずはクリアコマンド

npx hardhat clear

 

※このコマンドの実行には色々dependencyが必要なようです。。僕の場合は下記コマンドで解決しました。

npm install --save-dev "@nomiclabs/hardhat-ethers@^2.0.0"

 

コンパイル

npx hardhat compile

 

Rinkebyテストネットにデプロイ!

npx hardhat run scripts/deployRoboPunksNFT.js --network rinkeby

 

コンソールに表示されたコントラクトアドレスをEthscanで見てみましょう。

Rinkeby.etherscanです

https://rinkeby.etherscan.io/address/0x37D9b01Aaf1eD7632E124993c129c4C7d5A5597a

 

Verifyするとコントラクトアドレス内の関数の詳細がContractタブ内で表示されるようになります

Verifyの仕方は下記コマンドで自分のコントラクトアドレスを入力してあげてください。

npx hardhat verify --network rinkeby CONTRACTADDRESS

 

このコマンドが通ると、Ethscan上でコントラクトの中身が見えるようになります。

また、デプロイ後artifactsというファイルがプロジェクトルートに生成されているのがわかりましたでしょうか?

Artifacts<contracts<RoboPunksNFT.jsonに行ってコントラクトのabiを見てみましょう。

ABIとは、Application Binary Interfaceの略で、EVM(ethereum vertual machine)というEthreum上のコンピューターに読みやすいようにプログラムをコンパイルしてあげたものです。

Solidityで書かれたコードは人間には読みやすいですが、それをコンピュータに読みやすいように変換してあげたものです。

 

このABIをsrcフォルダ内にコピーしましょう。

SrcフォルダにRoboPunksNFT.jsonというファイルを作ってABIをコピペしましょう。

 

これでコントラクト関連は完了です!

続いてはフロントエンドをReactで書いていきます。

 

④Mintingサイトのフロントエンドを作る(React.js)

 

Reactを使ってフロントエンドを作っていきます。

まずは、App.jsにコードを記述していきます。

その前に、NavBar.jsMainMint.jsコンポーネントを追加しておきましょう。

 

App.js

App.jsのコードはこんな感じです。

import { useState } from "react";
import './App.css';
import MainMint from "./MainMint";
import NavBar from "./NavBar";

function App() {
  //フックを作成
  const [accounts, setAccounts] = useState([]);

  return (
    <div className="overlay">
      <div className="App">
      <NavBar accounts={accounts} setAccounts={setAccounts} />
      <MainMint accounts={accounts} setAccounts={setAccounts} />
    </div>
    <div className="moving-background"></div>
    </div>
    
  );
}; 

export default App;

 

MainMint.js

NFTをMintするためのメインの実装はこちらのファイルに記述していきます。

import { useState } from 'react';
// ethers.jsはweb3.jsに代わるスタンダード
import { ethers, BigNumber } from 'ethers';
//abiを使えるようにjsonをインポート
import roboPunksNFT from './RoboPunksNFT.json';
import { Button, Box, Flex, Input, Text } from '@chakra-ui/react';


const roboPunksNFTAddress = "CONTRACT_ADDRESS";

//変数の引き渡し
const MainMint = ({ accounts, setAccounts }) => {
    //ミントの数を定義。デフォルトは1
    const [mintAmount, setMintAmount] = useState(1);
    const isConnected = Boolean(accounts[0]);

    //Mintをコントロールする関数
    async function handleMint() {
        if (window.ethereum) {
            const provider = new ethers.providers.Web3Provider(window.ethereum);
            const signer = provider.getSigner();
            const contract = new ethers.Contract(
                roboPunksNFTAddress,
                roboPunksNFT.abi,
                signer
            );
            try {
                //コントラクト内のmint()を実行
                //solidityで使うためにBigNumberに変換
                const response = await contract.mint(BigNumber.from(mintAmount), {
                    value: ethers.utils.parseEther((0.02 * mintAmount).toString()),
                });
                console.log('response: ', response);
            } catch (err) {
                console.log("error: ", err)
            }
        }
    }

    const handleDecrement = () => {
        if (mintAmount <= 1) return;
        setMintAmount(mintAmount - 1);
    };

    const handleIncrement = () => {
        //3がMAX
        if (mintAmount >= 3) return;
        setMintAmount(mintAmount + 1);
    };

    return (
        <div>
            <Text fontSize="48px" textShadow="0 5px #000000">RoboPunks</Text>
            <Text
                fontSize="30px"
                letterSpacing="-5.5%"
                fontFamily="VT323"
                textShadow="0 2px 2px #000000"
            >
                It's 2078. Can the RoboPunks NFT save humans from destructice rampant NFT speculation? Mint Robopunks to find out.
            </Text>
            {isConnected ? (
                <div>
                    <Flex align="center" justify="center">
                        <Button
                            backgroundColor="#D6517D"
                            borderRadius="5px"
                            boxShadow="0px 2px 2px 1px #0F0F0F"
                            color="white"
                            cursor="pointer"
                            fontFamily="inherit"
                            padding="15px"
                            marginTop="10px"
                            onClick={handleDecrement}
                        >
                            -
                        </Button>
                        <Input
                            readOnly
                            fontFamily="inherit"
                            width="100px"
                            height="40px"
                            textAlign="center"
                            paddingLeft="19px"
                            marginTop="10px"
                            type="number"
                            value={mintAmount}
                        />
                        <Button
                            backgroundColor="#D6517D"
                            borderRadius="5px"
                            boxShadow="0px 2px 2px 1px #0F0F0F"
                            color="white"
                            cursor="pointer"
                            fontFamily="inherit"
                            padding="15px"
                            marginTop="10px"
                            onClick={handleIncrement}
                        >
                            +
                        </Button>
                    </Flex>
                    <Button
                        backgroundColor="#D6517D"
                        borderRadius="5px"
                        boxShadow="0px 2px 2px 1px #0F0F0F"
                        color="white"
                        cursor="pointer"
                        fontFamily="inherit"
                        padding="15px"
                        marginTop="10px"
                        onClick={handleMint}
                    >
                        Mint Now
                    </Button>
                </div>
            ) : (
                <p>You must be connected to Mint.</p>
            )}
        </div>
    );
};

export default MainMint;

 

大事なコードはhandleMintという関数で、ブロックチェーンとやりとりしているこの部分です。

async function handleMint() {
        if (window.ethereum) {
            const provider = new ethers.providers.Web3Provider(window.ethereum);
            const signer = provider.getSigner();
            const contract = new ethers.Contract(
                roboPunksNFTAddress,
                roboPunksNFT.abi,
                signer
            );
            try {
                //コントラクト内のmint()を実行
                //solidityで使うためにBigNumberに変換
                const response = await contract.mint(BigNumber.from(mintAmount), {
                    value: ethers.utils.parseEther((0.02 * mintAmount).toString()),
                });
                console.log('response: ', response);
            } catch (err) {
                console.log("error: ", err)
            }
        }
    }

 

“Provider”はEthereumへの接続の抽象化です。

“window.ethereum””で、ユーザーがメタマスクを経由してブロックチェーンとやりとりできるようにします。

if(window.ethereum)で、ユーザーがメタマスクにログインしているかどうかを確認しているわけですね。

 

“Signer”とは署名のことです。Ethereum上でトランザクションを行うときにそれを承認したりするときに署名をします。

 

既にブロックチェーン上にデプロイされているコントラクトに関してはContractを使用します。

あわせて読みたい
Contract Documentation for ethers, a complete, tiny and simple Ethereum library.

 

NavBar.js

import React from 'react';
import { Button, Box, Flex, Image, Link, Spacer } from '@chakra-ui/react';
import Facebook from "./assets/social-media-icons/facebook_32x32.png";
import Twitter from "./assets/social-media-icons/twitter_32x32.png";
import Email from "./assets/social-media-icons/email_32x32.png";


//App.jsからの変数の引き渡し
const NavBar = ({ accounts, setAccounts }) => {
    //ログインしているかどうかの確認
    const isConnected = Boolean(accounts[0]);

    //メタマスク内のアカウントとコネクト
    async function connectAccount() {
        if(window.ethereum) {
            const accounts = await window.ethereum.request({
                //このメソッドでメタマスクに存在するアカウントを全て渡してくれる
                method: "eth_requestAccounts",
            });
            //アカウントをアップデート
            setAccounts(accounts);
        }
    }

    return (
        <Flex justify="space-between" align="center" padding="30px">
            {/* Left Side - Social Media Icons */}
            <Flex justify="space-around" width="40%" padding="0 75px">
                <Link href="https://facebook.com">
                    <Image src={Facebook} boxSize="42px" margin="0 15px"/>
                </Link>
                <Link href="https://twitter.com">
                    <Image src={Twitter} boxSize="42px" margin="0 15px"/>
                </Link>
                <Link href="https://gmail.com">
                    <Image src={Email} boxSize="42px" margin="0 15px"/>
                </Link>
            </Flex>

            {/* Right Side - Sections and Connect */}
            <Flex justify="space-around" align="center" width="40%" padding="30px">
            <Box margin="0 15px">About</Box>
            <Spacer />
            <Box margin="0 15px">Mint</Box>
            <Spacer />
            <Box margin="0 15px">Team</Box>
            <Spacer />
            </Flex>
            {/* Connect */}
            {/* //ログインしているならConnected, してないならメタマスクを立ち上げる */}
            {isConnected ? (
                <Box margin="0 15px">Connected</Box>
            ) : (
                <Button
                backgroundColor="#D6517D"
                borderRadius="5px"
                boxShadow="0px 2px 2px 1px #0F0F0F"
                color="white"
                cursor="pointer"
                fontFamily="inherit"
                padding="15px"
                margin="0 15px"
                onClick={connectAccount}
                >Connect</Button>
            )}
        </Flex>
    )
}

export default NavBar;

 

⑤CSSでスタイリング

 

既にある程度のスタイリングについてはApp.jsやコンポーネントファイルに記述してあります。

App.cssは以下の通り。

@import url("https://fonts.googleapis.com/css?family=Press+Start+2P");
@import url("https://fonts.googleapis.com/css?family=VT323");

.App {
  text-align: center;
  font-family: "Press Start 2P", "VT323";
  color: white;
}

* {
  box-sizing: border-box;
}

body {
  overflow: hidden;
}

.overlay {
  opacity: 0.85;
  width: 100%;
  height: 100%;
  z-index:10;
  top: 0;
  left: 0;
  position: fixed;
}

.moving-background {
  z-index: -1;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: url("./assets/background/parallax-bg.gif");
  background-size: cover;
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-position: 40% 40%;

}

 

index.cssは以下の通り。

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

 

これでNFTをMintできるwebsiteが完成です!

 

[jin_icon_check color=”#6B9FCE” size=”20px”]ライブプレビューを見るには?

VSCodeでライブプレビューを見るには、Live Serverというextentionが便利です。

これをインストールすると、右下に”Go Live”というボタンが出現するのでそれをクリックすれば簡単にプレビューが見れます。

なお、npm run startコマンドでサイトを実行できます。

終わりに

 

今回は前述してYouTube動画に沿ってNFT Minting Websiteを開発してみました。

「ブロックチェーンとやりとり」と聞くとすごく難しそうに思えますが、そこまで難しくないですね!

 

今回は以上です。

僕のTwitterはこちら >>

 

 

 

 

 

 

 

 

 

 

 

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次