Ninja Dao web3エンジニア養成講座に掲載されている 「誰でもできる!ジェネラティブnft開発」の作業メモ。
絵心がないのであまり興味が向かなかったけど、Let’s try amyway!! (とにかくやってみよう!!)の精神でやってみることにした。
教材のURLは https://crypto-code.jp/materials/create-generative-nft
環境構築
HashLipsをGitからダウンロード
- Art Engine:https://github.com/HashLips/hashlips_art_engine
- NFT Contract:https://github.com/HashLips/hashlips_nft_contract
- NFT Minting Dapp:https://github.com/HashLips/hashlips_nft_minting_dapp
HashLipsの三つのモジュールを作業用ディレクトリにダウンロードする。
作業用のディレクトリは /Users/eibakatsu/Dev/appとした。こちらにダウンロードして解凍する。
ダウンロード・解凍後のディレクトリ構成はこんな感じ。
Art Engine
VSCodeでプロジェクトを開く
VSCodeの「フォルダを開く」で、最初にダウンロードした hashlips_art_engine-1.1.2_patch_v6 を指定する。
パッケージのインストール
表示 -> ターミナル を開き、nvmをインストール
* mac のM1だと特定のバージョンのnpmでないとエラーになるらしい
( https://github.com/HashLips/hashlips_art_engine/issues/812 )
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
ログがずらっと出力されて、コマンドラインに復帰したら終了。
.zshrcを再読み込みしてパスを通してから、現在のバージョンを確認してみる。
“system”と表示されれば、元々のOS環境にインストールされているものが使用されている
# source ~/.zshrc
# nvm current
system
v14.17.3 をインストール
# nvm install v14.17.3
Downloading and installing node v14.17.3...
Downloading https://nodejs.org/dist/v14.17.3/node-v14.17.3-linux-arm64.tar.xz...
########################################################################################## 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v14.17.3 (npm v6.14.13)
Creating default alias: default -> v14.17.3
次に実行する npm install でエラーにならないように、Canvas 2.8で使用するライブラリをインストールしておく。
ライブラリはbrewでインストールするため、先にbrewをインストールしてから、ライブラリをインストールする。
# arch -arm64 brew install pkg-config cairo pango jpeg giflib librsvg
ずらっとログが流れて、入力プロンプトが出てきたら終了。
次は、npm install する
# npm install
設定ファイルの修正
設定ファイルは、 /src/config.js
何箇所か自分のプロジェクトに合わせて修正
7行目付近
Openseaに掲載されるコレクションの情報
// General metadata for Ethereum
const namePrefix = " EibaKatsu First Collection"; // $$$$ 修正 $$$$
const description = "This is generative collection."; // $$$$ 修正 $$$$
const baseUri = "ipfs://NewUriToReplace"; // $$$$ 修正(後で書き換える) $$$$
24行目付近
ジェネラティブを構成する要素(レイヤー)の数と重ね順、シャッフルして作成するかの指定
// If you have selected Solana then the collection starts from 0 automatically
const layerConfigurations = [
{
growEditionSizeTo: 40,
layersOrder: [
{ name: "background" },
{ name: "body" },
{ name: "face" },
{ name: "glass" },
{ name: "footwear" },
],
},
];
const shuffleLayerConfigurations = true; // $$$$ 修正 $$$$
- 最初、growEdhitionsSizeTo に 500を指定したが、Pinataへのアップロード時にFreeプラン枠をオーバーしてしまったので 40 に減らした
画像の作成
今回はPowerPointで作成
/layers配下に前項で設定したジェネラティブを構成する要素と同じ名前のサブディレクトリを作成。
サブディレクトリの中に、名前#割合.png の画像ファイルを保存する
背景(background)
PowePointを開き、デザイン→ユーザ設定→スライドのサイズ→ページ設定 を開く
幅/高さを5.12cmに変更してOK
同じくユーザ設定から背景の書式設定を開く
単色、グラデーションなどから好きなものを指定する
ファイル→エクスポート。
保存先を先ほど作成した/layers/サブディレクトリ配下。ファイル形式はPNGとして、名前#割合.png のファイル名で保存(エクスポート)する
各パーツ(body, face, glass, footwear)
先ほどと同じく、縦横5.12cmのスライドを作成。
挿入→図形→正方形を選択
スライドいっぱいの大きさで描画
塗りつぶし/線の透明度を100%に変更する
挿入→図形や挿入→アイコン などで好きなデザイン要素を描く
最初に作成した透明の正方形を含む全ての図形を選択した状態で、右クリック→図形として保存
(背景画像と同じ大きさの正方形を含むことで、各パーツの相対地位が整う)
背景を保存したしたときと同じ様に、 /layers/サブディレクトリ配下に 名前#割合.png で保存する
今回作成した画像は以下。
ジェネラティブデータの生成
VS Codeでターミナル→新しいターミナル
以下のコマンドを実行する
% node index.js
Created edition: 95, with DNA: db3e4e785cd01ca570d4496f6ef61dc8292f08c3
Created edition: 11, with DNA: 33a7376d7f59e31006ab71f8feb1aa1201d87a71
Created edition: 478, with DNA: b45d5fb36f85cee7aab85bd076de9f3ac4348c24
Created edition: 480, with DNA: a775e6fe200acf97704b20f00045346547f6ff39
Created edition: 18, with DNA: 91cd679bfd3935268ecfd6711afe5a7f8e2f563d
Created edition: 91, with DNA: 26a6cc1fe02ec6503cf66268698e8ebed4896c28
(続く)
/build/images 配下に ジェネラティブのイメージファイルが作成される
/build/json 配下にジェネラティブのjsonファイルが作成される
Pinataへイメージアップロード
前項で作成したイメージファイルを Pinataへアップロードする
https://app.pinata.cloud/pinmanager
UPLOADボタンからFolderを選択し、/build/imagesフォルダを指定する
ファイル指定後、格納するフォルダ名を聞かれるので、imageであることがわかるよう “First_generative_images”と入力しておく
リストに指定したフォルダがアップロードされるので、CIDをコピーする
メタデータの更新
メタデータ内のイメージを指定するパス ( ipfs://~~~ ) を書き換える
config.js の 10行目付近の baseUrlを上記のCIDに変更する
//const baseUri = "ipfs://NewUriToReplace"; // $$$$ 修正 $$$$
const baseUri = "ipfs://QmWQGNSkZ2TsMgsRJL6kDUMzKyMfib1tAVpHpvE1o1KLEc"; // $$$$ 修正 $$$$
上書き保存後、以下のコマンドで jsonファイル内の パスを書き換える
% node utils/update_info.js
Updated baseUri for images to ===> ipfs://QmWQGNSkZ2TsMgsRJL6kDUMzKyMfib1tAVpHpvE1o1KLEc
Updated description for images to ===> This is generative collection.
Updated name prefix for images to ===> EibaKatsu First Collection
jsonファイルがちゃんと書き換えられていることを確認するため、サンプルで開いて確認する。
4行目のimageのパスが先ほどのCIDになっていればOK
イメージファイルのときと同じ様に、pinataへアップロードする
今度のフォルダ名は、 “First_generative_metadata”とする
NFT Contract
VSCodeでプロジェクトを開く
最初にダウンロードした 「hashlips_nft_contract-1.0.1」を VS Code で開き、 contract / SimpleNft_flat.sol を選択。
ソース全てを選択してコピーする。
Rinkebyへのデプロイ
準備
https://remix.ethereum.org/ へアクセスする
Workspaces 横の+マークをクリックしてワークスペースを作成する
contracts 配下にデフォルトで作成されているファイルは不要なため、削除する
三つのファイルを選択して、右クリック→ Delete All
Contractの作成
contractsフォルダをクリックして反転した状態でCreate New Fileボタンをクリック
新規ファイルが作成され、ファイル名が聞かれるので、 First_generative.sol と入力する
前項の contract / SimpleNft_flat.sol の中身をまるごと貼り付ける
1235行目付近。 contract NFT を contract First_generative(ファイル名と同じ)に変更する
続く変数定義値を以下のように変更する
- cost – NFTを1つミントするために必要な値段。今回は0.0001
- maxSupply – ジェネラティブの総数。今回は40
- maxMintAmount – 一度にmintできる数。今回は2
- paused – ミントできない状態はtrue。まずはtrueで試す
- revealed – 公開しているか。true
// contract NFT is ERC721Enumerable, Ownable {
contract First _generative is ERC721Enumerable, Ownable {
using Strings for uint256;
string baseURI;
string public baseExtension = ".json";
uint256 public cost = 0.0001 ether;
uint256 public maxSupply = 40;
uint256 public maxMintAmount = 2;
bool public paused = true;
bool public revealed = true;
string public notRevealedUri;
コンパイル
左側の緑のついたマークをクリックして、Compile first_generative.sol ボタンをクリック
コンパイルが問題なく完了すると下部に CONTRACTのエリアが表示される。
自分の環境ではソースを保存したタイミングで自動でコンパイルされていた
デプロイ(Rinkeby)
テスト用のRinkevyネットワークへデプロイするため、MetamaskのネットワークをRinkebyに接続させる
左ペイン一番下メニューボタンのデプロイを選択し、各項目に以下のように入力後、transact実行
_INITBASEURIは、 “ipfs://” + [PinataにアップロードしたメタデータのCID] + “/”
Metamaskが立ち上がり、ガス代を確認して確認ボタンをクリック
成功すると画面下部に Deployed Contract の情報が表示されるので、コピーボタンでコントラクトのアドレスをコピーしておく。
0x90f347F2E4b8059c3e0eC845829Da93D218763ab
pause右の入力欄にfalseを入力して、pauseをクリックすることで、mint可能にする
下部のpaused をクリックして false になったら成功
mint
試しにmintしてみる。mint横の欄に 1 を入力して、mintをクリック
OpenSeaのテストネットでmintされていることを確認する
https://testnets.opensea.io/
デプロイ時に取得したコントラクトアドレスで検索するとCollectionsにコントラクトが表示されるので選択
mintしたNFTが表示される(一つ目は失敗したもの)
Polygonへのデプロイ
価格の修正
Polygonに合わせて価格を修正する
// uint256 public cost = 0.001 ether;
uint256 public cost = 0.5 ether;
コンパイル
Rinkebyのときと同じ
ネットワークの切り替え
MetamaskのネットワークをPolygonへ切り替える
デプロイ(Polygon)
Rinkebyのときと同じ様にデプロイする
ENVIRONMENT下の Custom(137) network の数字が137になっていることを確認しておく。137はPolygonのネットワーク番号。
何回かデプロイしても、以下のエラーが発生
[ethjs-query] while formatting outputs from RPC ‘{“value”:{“code”:-32603,”data”:{“code”:-32000,”message”:”transaction underpriced”
ガス代リミットが低い場合に発生するらしいので、リミットを上げて試すと、無事デプロイできた。
RIMIX画面 左下のDeployed Contracts下のコピーボタンをクリックして、コントラクトアドレスを取得しておく
0xa049bdfE04C2a6C677DAf1D4BFF7F43B0575A9a9
Verify
Polygon scan を開き、前項で取得したコントラクトアドレスを検索する
contract タブを開き、「Verify and Publish」をクリック
入力項目を埋める
OptimizationにNo (Yesだとコンパイルエラー、Noだと成功した)
*実際にコンパイルしたときと同じ設定にすべきとのこと。今回のコンパイルでは指定する箇所が見当たらなかったが、きっと No でコンパイルしたものと思う。
Contract Code below に Rimixから First_generative.solの中身を貼り付け
下部にスクロール、私はロボットではありませんをチェックして、Velify and Publish
Successfly generated… が表示されればOK
最初の Contaract タブを開くと、コントラクトの詳細が見える様になっている。
まず、デプロイ時のウォレットを接続
下にスクロールして、write コントラクトの Pause に falseを設定して、Writeを実行。ガス代確認するとミント可能になる。
フロントエンド
パッケージインストール
VSCode で hashlips_nft_minting_dapp-1.0.1 を開く
「Terminal」>「New Terminal」でターミナルを開き、 npm install
% npm install
インストールが完了したら、開発用サーバを起動してみる
% npm run start
http://localhost:3000 にアクセスして、以下の画面が起動したらOK
設定値の変更
public/config/config.json
WEBアプリの基本設定。前項で作成したNFTに合わせて定義する
{
"CONTRACT_ADDRESS": "0xa049bdfe04c2a6c677daf1d4bff7f43b0575a9a9",
"SCAN_LINK": "https://polygonscan.com/address/0xa049bdfe04c2a6c677daf1d4bff7f43b0575a9a9",
"NETWORK": {
"NAME": "Polygon",
"SYMBOL": "Matic",
"ID": 137
},
"NFT_NAME": "EibaKatsu First Generative",
"SYMBOL": "EKFG",
"MAX_SUPPLY": 40,
"WEI_COST": 500000000000000000,
"DISPLAY_COST": 0.5,
"GAS_LIMIT": 300000,
"MARKETPLACE": "Opeansea",
"MARKETPLACE_LINK": "https://opensea.io/collection/eibakatsu-first-generative",
"SHOW_BACKGROUND": true
}
public/config/abi.json
コントラクトのインタフェースを定義する
Polygon scanの contractタブから code を選択し、下部の「Contract ABI」をコピーして貼り付ける
[{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"string","name":"_initBaseURI","type":"string"},{"internalType":"string","name":"_initNotRevealedUri","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseExtension","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"cost","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxMintAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_mintAmount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"notRevealedUri","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"_state","type":"bool"}],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"reveal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"revealed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_newBaseExtension","type":"string"}],"name":"setBaseExtension","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_newBaseURI","type":"string"}],"name":"setBaseURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newCost","type":"uint256"}],"name":"setCost","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_notRevealedURI","type":"string"}],"name":"setNotRevealedURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newmaxMintAmount","type":"uint256"}],"name":"setmaxMintAmount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"walletOfOwner","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"payable","type":"function"}]
public/index.html
画面表示される内容。<title>と <meta name>だけ修正しておく
<title>EibaKatsu First Generative</title>
<meta name="description" content="EibaKatsu First Generative" />
public/manifest.json
short_name と name だけ修正しておく
"short_name": "EKFG",
"name": "EibaKatsu First Generative",
src/App.js
mintできる最大数を設定する。
160行目付近の incrementMintAmount 内の数字(元は10)を変更する
const incrementMintAmount = () => {
let newMintAmount = mintAmount + 1;
if (newMintAmount > 2) {
newMintAmount = 2;
}
setMintAmount(newMintAmount);
};
適切なガス代の設定
デフォルトだと失敗する可能性が高いため、直近の中央値を参照できるよう設定する
src/redux/blockchain/blockchainActions.js
64,74行目付近
if (networkId == CONFIG.NETWORK.ID) {
const gasPrice = await web3.eth.getGasPrice() // 追加
const SmartContractObj = new Web3EthContract(
abi,
CONFIG.CONTRACT_ADDRESS
);
dispatch(
connectSuccess({
account: accounts[0],
smartContract: SmartContractObj,
web3: web3,
gasPrice: gasPrice, // 追加
})
);
src/redux/blockchain/blockchainReducer.js
23行目付近
const blockchainReducer = (state = initialState, action) => {
switch (action.type) {
case "CONNECTION_REQUEST":
return {
...initialState,
loading: true,
};
case "CONNECTION_SUCCESS":
return {
...state,
loading: false,
account: action.payload.account,
smartContract: action.payload.smartContract,
web3: action.payload.web3,
gasPrice: action.payload.gasPrice, // 追加
};
src/App.js
139行目付近
const claimNFTs = () => {
let cost = CONFIG.WEI_COST;
let gasLimit = CONFIG.GAS_LIMIT;
let totalCostWei = String(cost * mintAmount);
let totalGasLimit = String(gasLimit * mintAmount);
console.log("Cost: ", totalCostWei);
console.log("Gas limit: ", totalGasLimit);
setFeedback(`Minting your ${CONFIG.NFT_NAME}...`);
setClaimingNft(true);
blockchain.smartContract.methods
.mint(mintAmount)
.send({
gasLimit: String(totalGasLimit),
to: CONFIG.CONTRACT_ADDRESS,
from: blockchain.account,
value: totalCostWei,
gasPrice: blockchain.gasPrice * mintAmount // 追加
})
.once("error", (err) => {
console.log(err);
setFeedback("Sorry, something went wrong please try again later.");
setClaimingNft(false);
})
.then((receipt) => {
console.log(receipt);
setFeedback(
`WOW, the ${CONFIG.NFT_NAME} is yours! go visit Opensea.io to view it.`
);
setClaimingNft(false);
dispatch(fetchData(blockchain.account));
});
};
UIのカスタマイズ
画像作ったりが手間なので割愛。対応するときは以下を参照。
https://crypto-code.jp/chapters/create-mint-dapp/update-ui
サーバへのアップロード
こちらも割愛。
https://crypto-code.jp/chapters/create-mint-dapp/upload
ローカルでお試し
http://localhost:3000 にアクセス
CONNECTでウォレット接続するとMINT数を選択できる様になる。
一つ購入。Metamaskで支払うMaticとガス代を確認
何度かやっても失敗したようだったので、何回かMINTした結果、四つも買ってしまった。。。
コントラクトには、0.5*4=2Matic溜まった
コメント