企画趣旨説明
今日から新しい企画を始めます。
その名も『まいにちDapps』です。僕が可能な限り毎日、Dapps(分散型アプリケーション)を作り、その作り方を公開していく企画です。
Ethereumを中心にNFT・DeFiなどの基本的なDappsを改めて調査しながら理解して作れるようになっていきます。新しいDappsを発明するのではなく、既存のDappsを真似していくイメージです。カバー予定のリストは以下になります。
この企画を通して、
- 弊社が受託開発でご提供できるサービスの例をご紹介すること
- 既存の素晴らしいツール・サービスを紹介して業界として車輪の再発明を防ぐこと
- 逆に簡単なサービスの割に高い料金を取っているところはめくっていこう
という裏テーマがあります!
Thirdwebで作るNFTのmintサイト徹底解説
『まいにちDapps』の1日目では、NFTの基本中の基本、mintサイトを作るところから始めます。
色々なやり方は存在しますが、今回は簡単で実用的なThirdwebを使ってmintサイトを作る方法をご紹介します。Thirdwebは非常に便利なので、これからも頻繁に登場する予定です。ただし、今回提供する情報は2023年7月時点のものなので、最新のバージョンや情報と異なる可能性がある点、ご了承ください。
まずは、ThirdwebのテンプレートからNFT Dropを選択し、デプロイします。
これでコントラクトのデプロイが完了しました。なお、Thirdwebのダッシュボードは誰でもアクセス可能ですが(←便利ですね!)、特定のmethodはその権限を持つウォレットからのみ実行できます。
次に、ExtensionsタブからNFTsのページに移動し、NFTの情報を登録します。
コレクタブルを発行する際は、Batch Upload機能を活用すると便利です。csvまたはjsonファイルを事前に準備しておく必要があります。また、画像等を先にIPFSにアップロードする場合も、ThirdwebのGUIを使用するか、あるいは自身でSDK経由でアップロードするスクリプトを書いて用意しておくといいですよ。
https://thirdweb.com/dashboard/storage
ここでのUpload操作で、NFTのLazy mintの準備が整います。
なお、一度登録したNFTはClaim(取得)される前であっても編集はできなくなるので、誤ったデータをアップロードしないよう注意しましょう。
次に、Claim Condition(取得条件)の設定に移ります。
ThirdwebのClaim Conditionsでは開始日時や価格、Allowlistなど様々な設ThirdwebのClaim Conditions機能では、開始日時、価格、Allowlist(許可リスト)等、多岐に渡る設定が可能です。複数の条件を設定できるので、例えば「この期間はAllowlistのユーザーだけが0.01ETHで購入できる」や「この期間は全体公開で0.02ETHで購入できる」といったフェーズごとの設定も可能ですよ。
現在のverで設定可能な項目を列挙します。
- フェーズの種類
- Only Owner
- Allowlist Only
- Public with Allowlist
- 誰でもmintできるが、Allowlistのウォレットは特別な条件でmintできる
- Public
- フェーズの開始日時
- フェーズのmint総数上限
- フェーズの1ウォレット当たりmint数上限
- mintあたりの価格と支払い通貨(ERC20であれば対応可能)
- Allowlistの登録
コントラクト全体についての設定可能項目も以下のものがあります。
- コントラクト自体のメタデータ
- 画像、名前、SNSのLinkなど、Etherscanなど一部Dappsで参照されます
- Primary SalesのRecipient Address
- 初期販売の売り上げが送付されるアドレス
- RoyaltiesのRecipient Addressと%
- オンチェーンのロイヤリティ分配標準に準拠しているマケプレで参照されて二次流通のロイヤリティが分配されます
- Platform fee
- 発行代理店みたいなサービスが入る時に売り上げの%を分配する設定ができます。Primary SalesのRecipient AddressをSplitコントラクトにする方法もありますが、こちらの方が便利ですね(参照:what is platform fee?)
- Admin
- 上記などの設定を変更できるアドレス、複数設定可能
- Minter / Creator
- Adminの権限はないが、NFTを作成できるアドレス、複数設定可能
- Transfer
- コントラクト全体のトークンを誰がTransferできるかを決定します。基本はTransferableで全員がTransferできるようになっています。
- Not Transferableにすることで指定のアドレスだけがTransfer可能な状態にしたり、全アドレスを不可にすることでいわゆるSBTを実現できます。
これでコントラクトの準備はひとまず完了です。
次に、フロントエンドの作成に進みます。まずは、Next.jsのプロジェクトを立ち上げましょう。
yarn create-next-app
最近ではNext.js 13がリリースされ、ディレクトリ構成等が変わりました。まだ新しいバージョンに慣れていないところもありますが、今回は新しいバージョンに準拠して進めていきます。また、CSSのライブラリについては個人的にはChakra UIに慣れていますが、Tailwindが主流になりつつある(もしくはすでになっている)感があるので、これを機にTailwindを使ってみることにします。
基本的には、Thirdwebのbuildページの指示に従って進めていきます。
まず、以下のThirdwebのライブラリをインストールします。
@thirdweb-dev/react @thirdweb-dev/sdk ethers@5
次に、ThirdwebProviderを設定しますが、これにはNext.js 13では少し手間がかかります。具体的には、Providersというfunction componentを作成し、それをlayout.tsxで読み込むように設定します。
providers/Providers.tsx
"use client";
import { ThirdwebProvider } from "@thirdweb-dev/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <ThirdwebProvider activeChain="mumbai">{children}</ThirdwebProvider>;
}
Layout.tsx
import { Providers } from "./providers/Providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}
次に、Page.tsxを編集していきます。こちらの公式リンクには、全てのパターンを網羅した例が出ています→ https://github.com/thirdweb-example/nft-drop/blob/main/src/App.tsx
今回は基本的な部分のみを取り出して実装していきます。
在庫量と価格を取得し表示する機能と、Connect WalletボタンおよびClaimボタンを作成しました。本来であれば、「Sold Out」の表示やエラーハンドリングも必要になるのですが、今回はこれらの部分は省略します。
"use client";
import {
ConnectWallet,
useActiveClaimConditionForWallet,
useAddress,
useClaimedNFTSupply,
useContract,
useContractMetadata,
useNFT,
useUnclaimedNFTSupply,
useClaimNFT,
} from "@thirdweb-dev/react";
import { useMemo } from "react";
import { BigNumber, utils } from "ethers";
export default function Home() {
const contractAddress = "0xE71eE93Ad57c8b355e1bCDFe435B05673d8B4930";
const contractQuery = useContract(contractAddress);
const contractMetadata = useContractMetadata(contractQuery.contract);
const { data: firstNft, isLoading: firstNftLoading } = useNFT(
contractQuery.contract,
0
);
const address = useAddress();
const activeClaimCondition = useActiveClaimConditionForWallet(
contractQuery.contract,
address
);
const unclaimedSupply = useUnclaimedNFTSupply(contractQuery.contract);
const claimedSupply = useClaimedNFTSupply(contractQuery.contract);
const numberClaimed = useMemo(() => {
return BigNumber.from(claimedSupply.data || 0).toString();
}, [claimedSupply]);
const numberTotal = useMemo(() => {
return BigNumber.from(claimedSupply.data || 0)
.add(BigNumber.from(unclaimedSupply.data || 0))
.toString();
}, [claimedSupply.data, unclaimedSupply.data]);
const priceToMint = useMemo(() => {
const bnPrice = BigNumber.from(
activeClaimCondition.data?.currencyMetadata.value || 0
);
return `${utils.formatUnits(
bnPrice.mul(1).toString(),
activeClaimCondition.data?.currencyMetadata.decimals || 18
)} ${activeClaimCondition.data?.currencyMetadata.symbol}`;
}, [
activeClaimCondition.data?.currencyMetadata.decimals,
activeClaimCondition.data?.currencyMetadata.symbol,
activeClaimCondition.data?.currencyMetadata.value,
]);
const {
mutate: claimNFT,
isLoading,
error,
} = useClaimNFT(contractQuery.contract);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
{firstNftLoading ||
!unclaimedSupply ||
!claimedSupply ||
!activeClaimCondition ? (
<>
<div className="px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200">
loading...
</div>
</>
) : (
<>
<img
src={contractMetadata.data?.image || firstNft?.metadata.image}
className="box-border border rounded-xl"
></img>
<p className="mt-4">
{numberClaimed} / {numberTotal} claimed
</p>
<p className="mt-4">{priceToMint}</p>
{address ? (
<>
{isLoading ? (
<>loading...</>
) : (
<button
onClick={() => claimNFT({ to: address, quantity: 1 })}
type="button"
className="text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2"
>
Claim
</button>
)}
</>
) : (
<ConnectWallet />
)}
</>
)}
</main>
);
}
これで完成です!vercelにデプロイしてください。
コードはこちらに公開しておきます。
https://github.com/ksuhara/dapps-everyday/tree/main/001-mint-site
まとめ
今日の連載では、NFTの基本となるmint(発行)サイトの作成について解説しました。
明日は、今日作成したmintサイトを一歩進め、クレジットカード決済機能を付け加えたサイトの作り方を解説します。より多機能なサービスの開発を可能にするためのノウハウを身につけていきましょう。
弊社Pontechはweb3に関わる開発を得意とするテック企業です。サービス開発に関するご相談はこちらのフォームからお願いいたします。
また、受託開発案件に共に取り組むメンバーを募集しています!ご興味のある方はぜひお話させてください!