Next.js、ReactJSエンジニアのための今使えるWeb3の話
この記事はWeb3 Dev Advent Calendar 2022 - Qiitaの25日目の記事です。
はじめに
この記事では、Next.js(ReactJS)のエンジニアの皆さんに、Web3(ブロックチェーン)とは何かを具体的に説明します。つまり、分散台帳がどうのこうのといったモヤっとした話はいっさいしません。
Web3アプリ
現実問題としてWeb3アプリと言えど、フロント側(Web)はNext.jsなどのWeb2の技術を使用する必要があります。幸いなことにWeb3のバックエンド側はThirdwebなどのサードパーティのサービスを利用することで、サーバ側(スマートコントラクト)の知識がなくてもカスタムフックを利用することで簡単にWeb3アプリが作れてしまいます。
Web2でもFirebaseやAWS Lambdaなどのサーバレス・サービスでサーバ側の知識がなくてもシステムを開発できますがそれに近い感覚です。
Web3のバックエンドとは何なのか?
乱暴なメタファーで言えば、ブロックチェーンとはデータベースであり、スマートコントラクトとはストアードプロシージャになります。更新系のトランザクションは有料であり、呼び出すごとにウォレットから手数料が徴収されます。
ThirdwebのReactJSのカスタムフックを利用すると、クライアントアプリからストアード(スマコン)を叩くことができます。レスポンスがJSONで返ります。
データベースにはNFTテーブルを作成します。1つのレコードが1つのNFTトークンに対応します。そして各レコードには所有者IDカラムや画像URLカラムがあります。レコードの数に制限を加えることで希少価値が生まれます。
仮にSQLが書けるとすると、自分自身が所有するNFTを取得するSQLは以下のようになります。
SELECT owner_id, nft_token_id, img_url FROM nft_table WHERE owner_id = :my_wallet_id
実際にやってみる
こちらのThirdweb(Solana)の公式ブログに基づいて説明していきます。
またこちらのチュートリアルビデオも参考にしてください。
全体の手順
- SolanaのウォレットPhantomをインストール
- ThirdwebのサイトでNFTの作成
- Next.jsアプリの作成
1. Phantomウォレット
ウォレットとは、Gmailアカウントにお財布機能が付いたようなものです。Web2ではログイン機能はGmailやFacebookのOAuth、決済機能はクレカと別々になっています。Web3ではそれが合体しています。メールアドレス間でお金を送金できるようなものです。
SolanaではPhantom
というChrome拡張のウォレットを使用します。イーサのMetamaskのようなものになります。インストール方法はこちらの動画をご覧ください。ウォレットはまだ空っぽですから入金しましょう。開発用のトークンがこちらのページから無料でもらえます。
2. Thirdwebのサイト
ThirdwebのサイトからNFTを作成します。先ほど作成したPhantomウォレットでThirdwebのページにログインします。手順はこちらのビデオを参考にしてください。
設定するときに、開発用のDevnet
を選択すること。アップロードした画像は誰でも見ることができること。販売価格を0.01と少額にすること。に注意してください。
この時点では全てのNFTはまだ誰にも販売されていない状態(unclaimed)になります。
こんな感じにNFTを作成します。この画像ではNFTが全て販売済みの状態になっています。実際にはTOKEN ID
がunclaimed
になります。
3. Next.jsのプロジェクトを作成
Next.jsプロジェクトを新規作成し、以下の機能を実装していきます。
- ウォレットでのログイン、ログアウト
- NFTの購入
- NFTの一覧表示
- NFTの譲渡
何の変哲もない、通常のNext.jsプロジェクトを作成します。プロジェクト名をsolana-thirdweb-nft-gated-app
とします。
npx create-next-app -e with-tailwindcss solana-thirdweb-nft-gated-app
thirdweb
とsolana
のライブラリをインストールします。
yarn add @thirdweb-dev/react @thirdweb-dev/sdk @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets
.env.local
ファイルにプライベートキーとプログラムIDを設定します。プログラムIDは外部に公開しても安全ですので、NEXT_PUBLIC_に設定しても大丈夫です。
RIVATE_KEY=3obL1P...
NEXT_PUBLIC_PROGRAM_ADDRESS=C7Q2ay7g...
_app.tsx
にウォレットのProviderを設置します。next-authのようにapiを使用して認証します。
import '../styles/globals.css'
import type {AppProps} from 'next/app'
import {ThirdwebProvider} from "@thirdweb-dev/react/solana"
import {Network} from "@thirdweb-dev/sdk/solana"
import {WalletProvider} from "@solana/wallet-adapter-react"
import {PhantomWalletAdapter} from "@solana/wallet-adapter-wallets";
export const network: Network = 'devnet' // 開発用のネットワーク
export const domain = "example.org"
export const wallet = new PhantomWalletAdapter()
function MyApp({Component, pageProps}: AppProps) {
return (
<ThirdwebProvider
authConfig={{
authUrl: "/api/auth",
domain: process.env.VERCEL_URL || domain
}}
network={network}
>
<WalletProvider wallets={[wallet]}>
<Component {...pageProps} />
</WalletProvider>
</ThirdwebProvider>
)
}
export default MyApp
authのライブラリをインストールします。
yarn add @thirdweb-dev/auth
import {ThirdwebAuth} from "@thirdweb-dev/auth/next/solana"
import {domain} from "./pages/_app"
export const {ThirdwebAuthHandler, getUser} = ThirdwebAuth({
privateKey: process.env.PRIVATE_KEY!,
domain,
})
import {ThirdwebAuthHandler} from "../../../auth.config"
export default ThirdwebAuthHandler();
ログインページを作成する
ウォレットにログイン(接続)するためのページ(login.tsx)を作成します。カスタムフックで提供されていますのでボタンのイベントハンドラで呼び出します。ウォレットでログインするとuseUser()
にユーザ情報がセットされます。
import {
useLogin,
useLogout,
useUser,
} from "@thirdweb-dev/react/solana";
function LoginPage() {
const login = useLogin()
const logout = useLogout()
const {user} = useUser() // ログインするとセットされます。
const handleLogin = async () => {
await login() // Phantomウォレットが起動します。
}
return (
<div>
<main>
<h1>Thirdweb Solanaのページ</h1>
{user ? (
<div>
ログインユーザ: {user.address}
<button className="border-2" onClick={logout}>ログアウト</button>
</div>
) : (
<div>
<button className="border-2" onClick={handleLogin}>ログイン</button>
</div>
)}
</main>
</div>
)
}
パブリックにデプロイしなくても、Next.jsをyarn devでローカルホストで起動、実行することができます。
NFT購入ボタンを実装する
NFTを購入するボタンを設置します。
<div>
<p>ログインユーザ: {user.address.slice(0, 10)}***</p>
<button className="border-2" onClick={handlePurchase}>NFTを購入する</button>
<button className="border-2" onClick={logout}>ログアウト</button>
</div>
NFTを購入するカスタムフックuseClaimNFT
を設置します。mutateAsyncの名の通り、更新処理になります。NFTの所有者をウォレットのユーザに変更します。
const {program} = useProgram(
process.env.NEXT_PUBLIC_PROGRAM_ADDRESS,
'nft-drop'
)
const {mutateAsync: claim} = useClaimNFT(program)
イベントハンドラーでカスタムフックを呼び出します。
const handlePurchase = async () => {
await claim({
amount: 1 // NFTを1つ購入
})
}
購入ボタンを押すとウォレットがポップオーバーします。Approve
をクリックすると、決済トランザクションが走ります。NFTの料金と手数料がウオレットから引かれます。
Thirdwebダッシュボードから処理が正常終了したか確認します。成功すると一番右側のカラムに購入者のウォレットのIDがセットされます。
作成したNFTはSolanaのパブリックチェーンにありますので、Solanaのコンソール(SOLSCAN)から確認することができます。つまり、作成したNFTの所有者は自分自身であり、Thirdwebのものではありません。もちろん、Thirdwebのスマートコントラクトやライブラリに不正やセキュリティホールが全くないとは言い切れません。
購入したNFTを一覧表示する
何の変哲もない、リスト型をmapで処理するコードです。usersNft
にNFT一覧を保持します。
<ul>
{usersNft.map(nft => {
const {id, name, image} = nft.metadata
return <li key={id}>{id.slice(0, 5)} : {name} : {image}</li>
})}
</ul>
</main>
usersNft
にNFT一覧をセットするコードを書きます。カスタムフックuseNFTsを呼び出すと、NFT一覧が取得できます。
function LoginPage() {
const [usersNft, setUsersNft] = useState<NFT[]>([]);
const {data: nfts} = useNFTs(program)
useEffect(() => {
const myNfts = nfts?.filter((nft) => nft.owner === user?.address) ?? [] // ウォレットのユーザが所有するNFTのみにフィルタリング
setUsersNft(myNfts)
}, [nfts, user])
ウォレットで接続し、NFTの購入が正常終了すると購入したNFT一覧が表示されます。画像ファイル自体はSolanaブロックチェーンとは別のシステムに保存されます。IPFSという分散ファイルシステムに格納されます。
ウォレットから購入したNFTを確認することもできます。
NFTでページのアクセスコントロール
NFTを購入した人だけ閲覧できるページを作ります。SSRでgetServerSideProps
にアクセスコントロールを実装します。
フックは使用できませんので、ダイレクトにSDKの関数getAllClaimed
を呼び出します。
NFTを所有してない場合は、/login
にリダイレクトさせます。所有していればそのまま会員ページをレンダリングします。
import type {GetServerSideProps, NextPage} from 'next'
import {ThirdwebSDK} from "@thirdweb-dev/sdk/solana";
import {getUser} from "../auth.config"
import {network} from "./_app"
// SSRでのアクセスコントロール実装
export const getServerSideProps: GetServerSideProps = async ({req, res}) => {
const sdk = ThirdwebSDK.fromNetwork(network)
const user = await getUser(req)
if (!user) return { // ウォレットに接続してるか?
redirect: {
destination: "/login",
permanent: false
}
}
const program = await sdk.getNFTDrop(
process.env.NEXT_PUBLIC_PROGRAM_ADDRESS!
)
const nfts = await program.getAllClaimed()
const hasNft = nfts.find((nft: any) => nft.owner === user.address)
if (!hasNft) return { // NFTを所有してるか?
redirect: {
destination: "/login",
permanent: false
}
}
return {props: {}} // クラサバ間でJSONデータが飛ぶので機密情報を渡さないように。
}
// NFTを所有しているとレンダリングされる。
const Home: NextPage = () => {
return (
<div>会員専用ページだよ!</div>
)
}
このようなNFTの使い方をユーティリティNFTと言います。メンバーシップ(会員制)のサービスに利用します。Zennの本もNFTとして購入できるようになると良いですね。
NFTを譲渡する
NFTを他人のウォレットに譲渡するにはカスタムフックuseTransferNFT(送信先アドレス、NFTトークンID)
を使います。以下はNFTを一覧表示と、送信先テキストボックス、送信ボタンをつけたReactコンポーネントです。
const NFTs = (): JSX.Element => {
const {user} = useUser()
const inputEl = useRef<HTMLInputElement>(null);
const {program} = useProgram(
process.env.NEXT_PUBLIC_PROGRAM_ADDRESS,
'nft-drop'
)
const [usersNft, setUsersNft] = useState<NFT[]>([]);
const {data: nfts, isLoading} = useNFTs(program)
const {mutateAsync: transfer} = useTransferNFT(program);
useEffect(() => {
const myNfts = nfts?.filter((nft) => nft.owner === user?.address) ?? []
setUsersNft(myNfts)
}, [nfts, user])
const handleTransfer = async (id: string) => {
if (inputEl.current) {
await transfer({ // NFTを譲渡します
receiverAddress: inputEl.current.value, // 譲渡先ウォレット
tokenAddress: id, // 譲渡するNFTのID
})
}
}
if (isLoading) {
return <div>loading...</div>
}
return (
<div>
<input ref={inputEl} type="text"/>
<ul>
{usersNft.map(nft => {
const {id, name, image} = nft.metadata
return (
<li key={id}>{id.slice(0, 5)} : {name} : {image}
<button className="border-2" onClick={() => handleTransfer(id)}>NFTを譲渡</button>
</li>
)
})}
</ul>
</div>
)
}
SOLSCAN
からトランザクション履歴を見ます。正常にNFTトークンが別のウォレットに移動したことを確認します。
以上で説明を終わります。
次回は
バックエンド部分のRustスマートコントラクトやイーサなどの他のThirdwebサービスを説明したいと思います。
Discussion