こんにちは、たいきです(@taiki_16_k)
今回はSolidityを使って簡単なNFTのmintingページを作ってみたので解説します。
この記事では、下記の動画を参考にコードの解説をしています。
英語の動画ですが、すごくわかりやすい動画のため一度手を動かしてみることをおすすめします。
フロントエンドにはReactを使っています。
SolidityとReactでNFTのmintingページを作ってみる
今回の全体像としては以下の通り。
- 必要なものをインストール
- コントラクトを書く(Solidity)
- コントラクトをテストネットにデプロイする(Rinkeby Network)
- Mintingサイトのフロントエンドを作る(React.js)
- スタイリング(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をインストール
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みたいなもんですね。
※ファイルの場所を間違えると環境変数が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である必要があるのでしっかり確認してください。
[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.jsとMainMint.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
を使用します。
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を開発してみました。
「ブロックチェーンとやりとり」と聞くとすごく難しそうに思えますが、そこまで難しくないですね!
今回は以上です。