特定のNFTを持っている人だけ見られるページを作る
特定のNFTを持っている人だけ見られるページを作りたい。
実装
- バックエンド
- NFTコントラクト
- 限定させたいのでERC-1155でNFTの個数制限かける
- Wallet AddressからNFTの保有状況を検索できるインターフェースを追加
- NFTコントラクト
- フロントエンド
- Walletログイン
- 取得したWallet AddressからコントラクトのABI経由でメソッドコール
- NFTの保有者だったらページを表示、そうでなければ404
自作したERC721のテンプレをERC1155用に改造する。
ひとまずは動かす。
$ git clone git@github.com:YuheiNakasaka/minimal-nft-ts.git minimal-nft-1155-ts
$ yarn
$ cd contracts
$ yarn dev:node
$ yarn dev:deploy
$ yarn dev:view
yarn run v1.22.17
hardhat run scripts/view.ts --network localhost
No need to generate any newer typings.
0
✨ Done in 9.36s.
よし。
ERC721をERC1155に改造してみる。下記のOpenZeppelinのドキュメントを読めば実装に関しては十分そう。
まずは特にカスタムせずERC1155PresetMinterPauserでコントラクトを作成してみる。
// contracts/NFT.ts
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol";
contract NFT is ERC1155PresetMinterPauser {
constructor() ERC1155PresetMinterPauser("http://localhost:3000/{id}.json") {}
}
一旦nodeを落としてから再度起動しデプロイ。
$ yarn dev:deploy
mintしてみる。ERC721と違ってmint(from, id, amount, data)
と指定する引数が増えてる。TOKEN_ID:1
のトークンを10個だけPUBLIC_KEY
のアドレスに対してmintしてる。気をつけないといけないのはmint
の4爪の引数はArrayだということ。byte型なので空文字で""
とか指定してるとエラーが出る。
// scripts/mint.ts
import Web3 from "web3";
// ADDRESS, KEY and URL are examples.
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const PUBLIC_KEY = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
const PROVIDER_URL = "http://localhost:8545";
const TOKEN_ID = 1;
async function mintNFT() {
const web3 = new Web3(PROVIDER_URL);
const contract = require("../artifacts/contracts/NFT.sol/NFT.json");
const nftContract = new web3.eth.Contract(contract.abi, CONTRACT_ADDRESS);
const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest");
const tx = {
from: PUBLIC_KEY,
to: CONTRACT_ADDRESS,
nonce: nonce,
gas: 500000,
// Mint id:1 token which is amount of 10 to PUBLIC_KEY.
data: nftContract.methods.mint(PUBLIC_KEY, TOKEN_ID, 10, []).encodeABI(),
};
const signPromise = web3.eth.accounts.signTransaction(
tx,
// Test account's fixed private key in hardhat local node
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
);
signPromise
.then((signedTx) => {
const tx = signedTx.rawTransaction;
if (tx !== undefined) {
web3.eth.sendSignedTransaction(tx, function (err, hash) {
if (!err) {
console.log("The hash of your transaction is: ", hash);
} else {
console.log(
"Something went wrong when submitting your transaction:",
err
);
}
});
}
})
.catch((err) => {
console.log("Promise failed:", err);
});
}
mintNFT();
実際にmintされたかを確認するスクリプト。
// scripts/view.ts
import Web3 from "web3";
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const PUBLIC_KEY = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
const TOKEN_ID = 1;
async function viewNFT() {
const web3 = new Web3("http://localhost:8545");
const contract = require("../artifacts/contracts/NFT.sol/NFT.json");
const nftContract = new web3.eth.Contract(contract.abi, CONTRACT_ADDRESS);
// Display the PUBLIC_KEY's balance of id:1 token.
nftContract.methods.balanceOf(PUBLIC_KEY, TOKEN_ID).call().then(console.log);
}
viewNFT();
実行してみる。
$ yarn dev:view
yarn run v1.22.17
hardhat run scripts/view.ts --network localhost
No need to generate any newer typings.
10
よし。
AdminのPUBLIC_KEYで持ってるトークン(id:1のやつ)を別の人に分け与えるスクリプトも書いてみる。safeTransferFromを使えば良い。
// scripts/transfer.ts
import Web3 from "web3";
// ADDRESS, KEY and URL are examples.
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const PUBLIC_KEY = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
const PROVIDER_URL = "http://localhost:8545";
const TOKEN_ID = 1;
const RECEPIENT_ADDRESS = "0x70997970c51812dc3a010c7d01b50e0d17dc79c8";
async function transferNFT() {
const web3 = new Web3(PROVIDER_URL);
const contract = require("../artifacts/contracts/NFT.sol/NFT.json");
const nftContract = new web3.eth.Contract(contract.abi, CONTRACT_ADDRESS);
const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest");
const tx = {
from: PUBLIC_KEY,
to: CONTRACT_ADDRESS,
nonce: nonce,
gas: 500000,
// Transfer three of PUBLIC_KEY's id:1 token to RECEPIENT_ADDRESS.
data: nftContract.methods
.safeTransferFrom(PUBLIC_KEY, RECEPIENT_ADDRESS, TOKEN_ID, 1, [])
.encodeABI(),
};
const signPromise = web3.eth.accounts.signTransaction(
tx,
// Test account's fixed private key in hardhat local node
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
);
signPromise
.then((signedTx) => {
const tx = signedTx.rawTransaction;
if (tx !== undefined) {
web3.eth.sendSignedTransaction(tx, function (err, hash) {
if (!err) {
console.log("The hash of your transaction is: ", hash);
} else {
console.log(
"Something went wrong when submitting your transaction:",
err
);
}
});
}
})
.catch((err) => {
console.log("Promise failed:", err);
});
}
transferNFT();
実行する。
$ yarn dev:transfer
発行されてるかを確認する。さっきのscripts/view.ts
を改造してRECEPIENT_ADDRESS
のbalanceも表示する。
$ yarn dev:view
yarn run v1.22.17
hardhat run scripts/view.ts --network localhost
No need to generate any newer typings.
9
1
大丈夫そう。
フロントエンドを作成する。まずはwalletログイン。usedappを使う。
$ yarn add @usedapp/core ethers
usedappの公式Docsに倣ってconnectボタンを設置する。
import { Head, BlitzPage } from "blitz"
import Layout from "app/core/layouts/Layout"
import { ChainId, DAppProvider, useEthers, Config } from "@usedapp/core"
import { Box, Flex, Text } from "@chakra-ui/layout"
import { Button } from "@chakra-ui/button"
const config: Config = {
readOnlyChainId: ChainId.Localhost,
readOnlyUrls: {
[ChainId.Localhost]: "http://localhost:8545",
},
}
const MainContent = () => {
const { activateBrowserWallet, account } = useEthers()
return (
<>
<Flex alignItems="center" justifyContent="center" w="100vw" minH="100vh">
<Box textAlign="center">
<Box my="1rem">
<Text fontSize="lg" fontWeight="600">
Status
</Text>
{account ? <Text>Connected</Text> : <Text>Not connected</Text>}
</Box>
{!account && (
<Button
onClick={() => {
activateBrowserWallet()
}}
>
Connect Wallet!
</Button>
)}
</Box>
</Flex>
</>
)
}
const NFTLoginPage: BlitzPage = () => {
return (
<>
<Head>
<title>NFTLogin</title>
</Head>
<DAppProvider config={config}>
<MainContent />
</DAppProvider>
</>
)
}
NFTLoginPage.authenticate = false
NFTLoginPage.getLayout = (page) => <Layout>{page}</Layout>
export default NFTLoginPage
実際にログインしてみる。ローカルで動かすのでノードを立ち上げておくのを忘れない。
yarn dev:node
ボタンを押すとMetaMaskが立ち上がり許可するとStatusがConnectedになった。よし。
実際にNFTを保持しているか確認するhooksを書く。Contractのコードをコンパイルしてartifact
配下に生成されたABIファイル(json)を適当なディレクトリに設置しておく。
import { useEthers } from "@usedapp/core"
import { utils, Contract } from "ethers"
import ABI from "../resources/nft-login-abi.json"
const TOKEN_ID = 1
const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
export const useCallNFTLogin = () => {
const { library } = useEthers()
const inteface = new utils.Interface(ABI.abi)
const contract = new Contract(CONTRACT_ADDRESS, inteface, library?.getSigner())
return async (address: string): Promise<boolean> => {
const balance = await contract.balanceOf(address, TOKEN_ID)
return parseInt(balance.toString()) > 0 // NFTを持っているとtrueを返す
}
}
まずはNFTの発行
yarn dev:deploy
yarn dev:mint
次にNFTを自分のMetamaskのwalletアドレスに送る。さっき書いたtransfer.ts
のRECEPIENT_ADDRESS
を自分のアドレスに変えればおけ。
yarn dev:transfer
これで準備完了。UI側にtokenを持っている人だけが見られるテキストを表示させる。うまくいけばYou are a special user!
という文字列が表示されるはず。
import { Head, BlitzPage } from "blitz"
import Layout from "app/core/layouts/Layout"
import { ChainId, DAppProvider, useEthers, Config } from "@usedapp/core"
import { Box, Flex, Text } from "@chakra-ui/layout"
import { Button } from "@chakra-ui/button"
import { useCallNFTLogin } from "app/playgrounds/hooks/useCallNFTLogin"
import { useState } from "react"
const config: Config = {
readOnlyChainId: ChainId.Localhost,
readOnlyUrls: {
[ChainId.Localhost]: "http://localhost:8545",
},
}
const MainContent = () => {
const [authenticated, setAuthenticated] = useState(false)
const { activateBrowserWallet, account } = useEthers()
const hasValidNFT = useCallNFTLogin()
return (
<>
<Flex alignItems="center" justifyContent="center" w="100vw" minH="100vh">
<Box textAlign="center">
<Box my="1rem">
<Text fontSize="lg" fontWeight="600">
Status
</Text>
{account ? <Text>Connected</Text> : <Text>Not connected</Text>}
</Box>
{!account && (
<Button
onClick={() => {
activateBrowserWallet()
}}
>
Connect Wallet!
</Button>
)}
{account && (
<Box>
<Button
onClick={async () => {
setAuthenticated(await hasValidNFT(account))
}}
>
Show the limited contents?
</Button>
</Box>
)}
{account && authenticated && (
<Box mt="1rem">
<Text fontSize="2xl" fontWeight="600">
You are a special user!
</Text>
</Box>
)}
</Box>
</Flex>
</>
)
}
const NFTLoginPage: BlitzPage = () => {
return (
<>
<Head>
<title>NFTLogin</title>
</Head>
<DAppProvider config={config}>
<MainContent />
</DAppProvider>
</>
)
}
NFTLoginPage.authenticate = false
NFTLoginPage.getLayout = (page) => <Layout>{page}</Layout>
export default NFTLoginPage
出来た!これを応用すれば特定のNFTを持っている人だけ見られるコンテンツを表示するとかが出来そう。
おわり!
コードはここに置いた。
コントラクトとか↓
フロント側↓。個人サイトの一角に配置したのでごちゃごちゃしてる。
参照リンク