META KAWAII 🍭 を支える技術
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
概要
先日8/13,14に META KAWAII というNFTプロジェクトをローンチしました。
このスクラップでは、METAKAWAIIのローンチを支えた技術を紹介していきます。
※主にコントラクトやミントサイトの話
※CG周りは担当外です 🙏
質問は以下のスクラップでどうぞ!
関連URL
宣伝
イベント開催
この記事にちなんだイベントを開催しました!
solidity-jp
Solidityについてワイワイ学ぶDiscordコミュニティ「solidity-jp」を作りました!
いまから学んでみたい/学習中だけどの日本語の情報が少ない/古くて時間がかかっているという方、一緒に学びましょう〜!!
また、TwitterにてSolidityに関する技術情報を発信しています!
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
ミントサイト
フロントエンド
Cloudflare Pages
Next.js onNext.jsで開発。選定理由は使い慣れている&知見が豊富
Web3関連の開発ではよく使われている印象。solidity + next.js + ether.js でdApps作る
みたいなやつ。
ホスティングはCloudflare Pagesを利用
Unlimited Bandwidth のおかげで運用コスト0円を実現できた。(クレカ登録すらしてない)
バックエンド
Firebase CloudFunctions
アドレスを渡すとMerkleProofが返ってくるAPIのみ実装 運用コストは2円
META KAWAIIコントラクトではMerkleTreeによるホワイトリストを採用している
function preSaleMint(bytes32[] calldata _merkleProof, uint256 _mintCount)
external
payable
nonReentrant
{
// ...省略
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(
_merkleProof,
whiteListRoot[currentPhase()],
leaf
),
"MerkleProof: Invalid proof."
);
// ...省略
}
PresaleMintメソッドを呼び出す際に、引数として _merkleProof
を渡す必要がある。
この値を取得するAPIをCloudFunctionsにて生やしている
API仕様
# example request
https://us-central1-YOUR_PROJECT_ID.cloudfunctions.net/fetchProof?address=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
# example response
{
"has_whitelist": true,
"proof":["0xc3aa80aae859d498615d9afb9a7d81c79b29855bf44b2cff05d1e893f323bf6f","0x2e93b8fbddba0659245250d416c5f6b84a22fa0e45b15c4acf42d417b75e7d45","0x4a19039a8c6a78d283a913e6367eff86c3effa8d45420ecfece8d7a85ba97b54"]
}
トラブルシューティング
Presale, Publicsale当日、ミントできない問題が数人レベルで発生。
トラブルシューティングページを急遽作成し対応。
振り返り
- ThirdWebでどこまでできるか未調査だが、ノーコードでもミントサイト作れる時代になってきている。
- 今回はNext.jsで作ったが、オーバーエンジニアリングだったかも 🤔
- APIはFirebase CloudFunctionsで実装したが、デプロイやテストがめんどくさかったので次やるならNext.js on CloudRunにするかな〜⚡️
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
コントラクト
仕様
- ERC721A
- ホワイトリスト→MerkleTree
- ロイヤリティ→EIP2981、Rarible対応
- Upgradableは導入していない
開発環境
- hardhat
ライブラリ
- hardhat-gas-reporter おおよそのガス代を見積もれる
- hardhat-contract-sizer コントラクトサイズを計算できる
- hardhat-etherscan etherscanにてコードをverifyするために必要
- solidity-coverage テストカバレッジを簡単に生成できる
- erc721a mint時のガス代負担を減らすために採用
- merkletreejs, keccak256 merkletree関連のテスト用。テストコード内で使う
- mocha テスト用ライブラリ
- envchain 環境変数管理ライブラリ
BOT対策
BOT対策用にMintBOTコントラクトとテストコード作成
※MintBOTコントラクトを防げるのであって、全てのMintBOTを防げるわけではない。
MintBOTコントラクト
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract MintBot is IERC721Receiver {
address _mintContract;
constructor(address mintContract_) {
_mintContract = mintContract_;
}
receive() external payable {}
function mint() public payable {
bytes memory payload = abi.encodeWithSignature(
"publicSaleMint(uint256)",
5
);
(bool success, ) = _mintContract.call{value: 0.5 ether}(payload);
require(success, "error");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
}
振り返り
- 開発当初はERC721Aを継承せずに 開発していたが、ガス代等諸々考慮した結果、途中でERC721Aを継承する方針に変更した
- テストコードの変更で後戻りが若干発生したため、最初からERC721Aで実装していたらなーと反省。ジェネラティブNFTプロジェクトのコントラクトはとりあえずERC721Aで良いと思った。
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
メタデータとAPIサーバー
メタデータ運用
DROP'Sのメタデータは以下の通りとなっている
例: DROP'S #1
{
"name": "DROP'S #1",
"description": "DROP'S is the avatar collection of Tokyo-based brand META KAWAII.\n\nOur mission is to create a new brand for the Web3.0 era that crosses the borders of the real world and the digital world. DROP'S holders will benefit from exclusive drops, membership access, and much more. Check out [metakawaii.jp](https://metakawaii.jp) for more information. Click [here](https://metakawaii.jp/pfp) to download the PFP for DROP'S NFT\n\nWe build together. We grow together.",
"image": "https://firebasestorage.googleapis.com/v0/b/metakawaii-pj.appspot.com/o/drops-p2dJC3aIeHg7sQh7%2Fpng%2F1.png?alt=media",
"animation_url": "https://firebasestorage.googleapis.com/v0/b/metakawaii-pj.appspot.com/o/drops-p2dJC3aIeHg7sQh7%2Fmp4%2F1.mp4?alt=media",
"external_url": "https://firebasestorage.googleapis.com/v0/b/metakawaii-pj.appspot.com/o/drops-p2dJC3aIeHg7sQh7%2Fpng%2F1.png?alt=media",
"attributes": [
{ "trait_type": "DNA", "value": "Angel" },
{ "trait_type": "HAIR", "value": "LightBlue DoubleChignon" },
{ "trait_type": "EYE COLOR", "value": "Turquoise" },
{ "trait_type": "HEADWEAR", "value": "Pink Oni Mask" },
{ "trait_type": "CLOTHING", "value": "LimeGreen Hoodie" },
{ "trait_type": "MOUTH", "value": "White Controller" },
{ "trait_type": "BACKGROUND", "value": "White" },
{ "trait_type": "ACCESSORIES", "value": "Flame Earring" }
]
}
メタデータを記述するjsonファイル、表示用の画像、動画は全て Google Cloud Storage で管理している
IPFS上にpinすることも検討したが、画像や動画ファイルの容量が大きく取得が不安定になってしまうため採用を見送った
仕組み上はIPFS移行もできるようにしている
メタデータ取得用APIサーバー
メタデータ取得用のAPIサーバーも実装した。
構成は Next.js + Vercel
リビール前とリビール後でresponseが異なっている
(つまり、オンチェーントランザクション無しでAPIの返却値だけ変えることでリビールを実現)
リビール前
# request
curl https://api.metakawaii.jp/api/drop/1.json
# response
{
"name": "DROP'S",
"description": "DROP'S is the avatar collection of Tokyo-based brand META KAWAII.\n\nOur mission is to create a new brand for the Web3.0 era that crosses the borders of the real world and the digital world. DROP'S holders will benefit from exclusive drops, membership access, and much more. Check out [metakawaii.jp](https://metakawaii.jp) for more information. Click [here](https://metakawaii.jp/pfp) to download the PFP for DROP'S NFT\n\nWe build together. We grow together.",
"image": "リビール前の画像URL",
"animation_url": "リビール前の動画URL",
}
リビール後 GCSのファイルパスに307リダイレクトさせてる
# request
curl https://api.metakawaii.jp/api/drop/1.json --verbose
# ...省略
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 307
< age: 0
< cache-control: public, max-age=0, must-revalidate
< date: Tue, 06 Sep 2022 07:24:44 GMT
< location: https://firebasestorage.googleapis.com/v0/b/metakawaii-pj.appspot.com/o/drops-p2dJC3aIeHg7sQh7%2Fjson%2F1.json?alt=media
# ...省略
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
内製スクリプト、ツール等
メタデータの生成
メタデータはspreadsheetに入稿してもらい、csv形式でエクスポートしたデータを用いて自動生成した。
※NAME列は元データのファイル名
自動生成スクリプト
- node.js
- csv-parse
自動生成する際は余計な加工処理は行わず、spreadsheetをマスタデータとすることを意識した。
リリース前に画像動画とpropertyが間違えている部分がないかチェックすることになるが、ここを意識するだけで検証が楽になった
メタデータや動画像のアップロード
前述した通り、Google Cloud Storage
にメタデータ等を保管している。
今回ローンチした 3000体のメタデータだけで 合計20GB にもなるため、大量アップロードする手段が必要だった。
調べたところ、公式で提供されてる gsutilツールが良さげだったので使った。
- 一括アップロード
gsutil -m cp -r アップロードしたいフォルダ gs://YOUR_PROJECT.appspot.com/ファイルパス
- 一括削除
gsutil -m rm -r gs://YOUR_PROJECT.appspot.com/削除したいパス
色々な検証をするためにパターン別に変換してアップロードした動画像ファイル合計で最終的には 120GB
ほどアップロードした。(現在は検証用ファイルは削除済み)
CloudStorageではオペレーション課金、帯域幅課金もあり検証だけで 3万円
ほどかかってしまった 😱
NFT Viewer
動画像とメタデータのAttributeに間違いがないか検証するために検証用アプリを作成
これも Next.js + Vercelでホスティング
mp4 to gif
※ image
をgifにしようとして検証したときのコード。ファイルが大きく表示がカクつくので見送った
for f in inputs/mp4/*.mp4
do
FILENAME=${f##*/}
ffmpeg -i "$f" -r 24 -vf scale=350:-1 -f gif "outputs/gif/${FILENAME%.*}.gif"
done
mp4 to animation webp
※ image
をanimation webpにしようとして検証したときのコード。ファイルが大きく表示がカクつくので見送った
for f in inputs/mp4/*.mp4
do
FILENAME=${f##*/}
ffmpeg -i "$f" -vf scale=320:-1,fps=30 -lossless 0 -compression_level 6 -quality 100 -preset 0 "outputs/webp/${FILENAME%.*}.webp"
done
png to webp
※ image
をwebpにしようとして検証したときのコード。OpenSea側でキャッシュ変換されるときに画質が悪くなってしまうので見送った
for f in inputs/png/*.png
do
FILENAME=${f##*/}
ffmpeg -i "$f" -vf scale=1024:-1 -c:v libwebp -compression_level 6 -qscale 100 "outputs/webp/${FILENAME%.*}.webp"
done
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
スケジューリング
スケジューリングの取り方は仕様やエンジニアの人数よって変わるのでなんとも言えんのと、
その仕様もクリプト情勢や法的な面でころころ変わるので(しょうがない)とにかく柔軟な設計にできると良い
参考までにMETAKAWAIIのローンチまでの開発の流れを共有します
- 2022/03 週1でお手伝い
- 最初のヒアリング、仕様をある程度固める
- 2,3日でガッとコントラクト作成
- コントラクトのIFに合わせてミントサイト作成
- 2022/04-06 週1でお手伝い
- ミントサイトの開発を主にしながらコントラクト仕様を微調整
- 2022/06
- クリプト情勢が怪しくなってきたので一旦作業中断
- 2022/07 週2でお手伝い
- 情勢に合わせてコントラクト修正
- コントラクトのaudit依頼
- コントラクト修正に合わせてミントサイト修正
- 2022/08 週2でお手伝い
- ミントサイトの最終調整
- 内製ツールの開発
- 2022/08/13,14
- presale, publicsale!
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
テスト/デバッグ
- 自動テスト
- コントラクトのカバレッジは100%にした
- ミントサイト(フロントエンド)のテストコードはあまり書いてない
- 工数と優先順位の問題で切らざるを得なかったが書いたほうが良い
- ローカルで検証
- ローカルでブロックチェーン立ち上げ(hardhat)+ミントサイトの動作確認
- goerliテストネットで検証
- 運営メンバーにステージング環境共有
- ただ、状態を変えるのが大変だったため最終的には画面録画して共有することでUX面で問題ないかチェックしてもらう形での検証になった
- polygonチェーンで検証
- 主にOpenSeaでの表示確認 (propertyなど)
-
polygonで大量ミントするとOpenSeaでスパム扱いされて表示できなかった問題があった
- マジックナンバーは8
- 3000体のテストのため、1txあたり8mint * 375回実行した
- mainnetで検証
- OpenSeaでの画像・動画の画質確認
- OpenSeaでは画像と画像はOpenSea側でキャッシュされる
- どこまで画質を落とされてキャッシュされるかが検証したかった
- 本番ではOwnerMintを300体ほどする予定だった。大量ミントした場合にスパム判定のリスクがないか確認
- 3000体OwnerMintした。gas price 8gweiで0.04ETHかかった(1万円くらい) ERC721Aやすい!
- 3000体の大量mintでも問題なく全て表示確認できた。
- OpenSeaでの画像・動画の画質確認
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
検証/運用にかかった費用
- 検証
- コントラクトのガス代
- 検証/本番含め合計で約0.1ETH
- だいたい4~10gweiのときにやってた
- 検証/本番含め合計で約0.1ETH
- メタデータ・動画像ストレージ利用料
- 約3.5万円
- 検証に大量のgifやアニメーションwebp、avifをクラウドストレージにアップしたり削除しまくってたら思ったよりかかってしまった
- コントラクトのガス代
- 運用
- ミントサイト (Cloudflare Pages)
- 0円
- メタデータ・動画像 (Google Cloud Storage)
- 数百円〜数千円/月(その月のダウンロード量によって変動)
- APIサーバー (Vercel)
- 2000円/月
- ミントサイト (Cloudflare Pages)
![Ryo Takahashi](https://res.cloudinary.com/zenn/image/fetch/s--MaLS1XCN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/c5c5916b44.jpeg)
余談
もしいま作るとしたら、APIサーバーはHonoを使うかな。
Next.jsはオーバーエンジニアリングというか、Next.jsである必要があまりない(好きなフレームワークだけどね)