Closed9

META KAWAII 🍭 を支える技術

ピン留めされたアイテム
Ryo TakahashiRyo Takahashi

概要

先日8/13,14に META KAWAII というNFTプロジェクトをローンチしました。

このスクラップでは、METAKAWAIIのローンチを支えた技術を紹介していきます。

※主にコントラクトやミントサイトの話
※CG周りは担当外です 🙏

質問は以下のスクラップでどうぞ!
https://zenn.dev/ryo_takahashi/scraps/0e9b1466cbdafc

関連URL

宣伝

イベント開催

この記事にちなんだイベントを開催しました!

https://solidity-jp.connpass.com/event/259525/

https://www.youtube.com/watch?v=EI2HUzJx4PQ

solidity-jp

Solidityについてワイワイ学ぶDiscordコミュニティ「solidity-jp」を作りました!
いまから学んでみたい/学習中だけどの日本語の情報が少ない/古くて時間がかかっているという方、一緒に学びましょう〜!!

https://solidity-jp.dev/

また、TwitterにてSolidityに関する技術情報を発信しています!

https://twitter.com/k0uhashi

Ryo TakahashiRyo Takahashi

ミントサイト

フロントエンド

Next.js on Cloudflare Pages

Next.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 TakahashiRyo Takahashi

コントラクト

仕様

  • ERC721A
  • ホワイトリスト→MerkleTree
  • ロイヤリティ→EIP2981、Rarible対応
  • Upgradableは導入していない

開発環境

  • hardhat

ライブラリ

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 TakahashiRyo Takahashi

メタデータと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 TakahashiRyo Takahashi

内製スクリプト、ツール等

メタデータの生成

メタデータはspreadsheetに入稿してもらい、csv形式でエクスポートしたデータを用いて自動生成した。
※NAME列は元データのファイル名

自動生成スクリプト

自動生成する際は余計な加工処理は行わず、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 TakahashiRyo Takahashi

スケジューリング

スケジューリングの取り方は仕様やエンジニアの人数よって変わるのでなんとも言えんのと、
その仕様もクリプト情勢や法的な面でころころ変わるので(しょうがない)とにかく柔軟な設計にできると良い

参考までに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 TakahashiRyo Takahashi

テスト/デバッグ

  • 自動テスト
    • コントラクトのカバレッジは100%にした
    • ミントサイト(フロントエンド)のテストコードはあまり書いてない
      • 工数と優先順位の問題で切らざるを得なかったが書いたほうが良い
  • ローカルで検証
    • ローカルでブロックチェーン立ち上げ(hardhat)+ミントサイトの動作確認
  • goerliテストネットで検証
    • 運営メンバーにステージング環境共有
    • ただ、状態を変えるのが大変だったため最終的には画面録画して共有することでUX面で問題ないかチェックしてもらう形での検証になった
  • polygonチェーンで検証
  • mainnetで検証
    • OpenSeaでの画像・動画の画質確認
      • OpenSeaでは画像と画像はOpenSea側でキャッシュされる
      • どこまで画質を落とされてキャッシュされるかが検証したかった
    • 本番ではOwnerMintを300体ほどする予定だった。大量ミントした場合にスパム判定のリスクがないか確認
      • 3000体OwnerMintした。gas price 8gweiで0.04ETHかかった(1万円くらい) ERC721Aやすい!
      • 3000体の大量mintでも問題なく全て表示確認できた。
Ryo TakahashiRyo Takahashi

検証/運用にかかった費用

  • 検証
    • コントラクトのガス代
      • 検証/本番含め合計で約0.1ETH
        • だいたい4~10gweiのときにやってた
    • メタデータ・動画像ストレージ利用料
      • 約3.5万円
      • 検証に大量のgifやアニメーションwebp、avifをクラウドストレージにアップしたり削除しまくってたら思ったよりかかってしまった
  • 運用
    • ミントサイト (Cloudflare Pages)
      • 0円
    • メタデータ・動画像 (Google Cloud Storage)
      • 数百円〜数千円/月(その月のダウンロード量によって変動)
    • APIサーバー (Vercel)
      • 2000円/月
Ryo TakahashiRyo Takahashi

余談
もしいま作るとしたら、APIサーバーはHonoを使うかな。
Next.jsはオーバーエンジニアリングというか、Next.jsである必要があまりない(好きなフレームワークだけどね)

このスクラップは2023/07/18にクローズされました