Token Gated Rooms
In this guide we will create token gated room that only allows users with a specific ERC20
, ERC721
, ERC1155
etc. TOKENS
to join.
Overview
TOKEN
refers to the ethereum token that user must hold to join the room in the following steps.accessToken
is a JWT token that is generated by the server and is used to authenticate the user to join the room.- Your application server is responsible for verifying the user's wallet and generating the
accessToken
and only issuing it to users who hold the requiredTOKEN
. - The generation of the
accessToken
is done using the Huddle01 Server SDK.
Walkthrough
Create Room
Create a room using Create Room API. We add an API route - since we are using Next.JS, we add a custom Next.js API route.
app/createRoom.ts
"use server";
export const createRoom = async () => {
const response = await fetch("https://api.huddle01.com/api/v1/create-room", {
method: "POST",
body: JSON.stringify({
title: "Huddle01 Room",
metadata: { // Custom Data that you can enter
tokenGatingInfo: {
{
chain: "ETHEREUM",
contractAddress: "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85",
tokenType: "ERC721",
},
},
},
}
}),
headers: {
"Content-type": "application/json",
"x-api-key": process.env.API_KEY!,
},
cache: "no-cache",
});
const data = await response.json();
const { roomId } = await data.data;
return roomId;
};
Signing message to verify wallet
We will use wagmi (opens in a new tab) and viem (opens in a new tab) to sign message and verify wallet.
app/[roomId]/page.tsx
import { useSignMessage } from "wagmi";
import { config } from "@/utils/config";
import { useState } from "react";
const [expirationTime, setExpirationTime] = useState(0);
const [message, setMessage] = useState("");
const { signMessageAsync } = useSignMessage({
config: config,
mutation: {
onSuccess: (data) => {
// We will implement this function in later steps
authenticateUser(data);
},
},
});
<button
type="button"
className="bg-blue-500 p-2 mx-2 rounded-lg"
onClick={async () => {
const time = {
issuedAt: Date.now(),
expiresAt: Date.now() + 1000 * 60 * 5,
};
const msg = `Click "Sign" only means you have proved this wallet is owned by you.
We will use the public wallet address to fetch your NFTs.
This request will not trigger any blockchain transaction or cost of any gas fees.
Account: ${address}
Issued At: ${new Date(time.issuedAt).toLocaleString()}
Expires At: ${new Date(time.expiresAt).toLocaleString()}`;
setExpirationTime(time.expiresAt);
setMessage(msg);
await signMessageAsync({
message: msg,
});
}}
>
Sign In with Wallet
</button>;
Creating API to authenticate user
We will create an API that will verify the signature, get room-details
and verify if user holds the token or not.
If user holds the token then we will generate access token and allow user to join the room.
app/token/route.ts
import { publicClient } from "@/utils/config";
import { AccessToken, Role } from "@huddle01/server-sdk/auth";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
// Get data from request body
const { roomId, signature, address, expirationTime, message } =
await request.json();
if (!roomId || !signature || !address) {
return new Response("Incorrect request", { status: 400 });
}
// Verify if signature is expired or not
if (expirationTime < Date.now()) {
return new Response("Signature expired", { status: 400 });
}
// Call `room-details` API to get token gating details
const roomDetailsResponse = await fetch(
`https://api.huddle01.com/api/v1/room-details/${roomId}`,
{
headers: {
"x-api-key": process.env.API_KEY!,
},
}
);
const roomDetails = await roomDetailsResponse.json();
if (!roomDetails?.metadata?.tokenGatingInfo) {
return new Response("Room is not token gated", { status: 400 });
}
// Set public client based on chain
const publicClient =
roomDetails.metadata.tokenGatingInfo.tokenGatingConditions[0].chain === "ETHEREUM"
? ethereumPublicClient
: polygonPublicClient;
// Verify signature
const verify = await publicClient.verifyMessage({
address,
message,
signature,
});
// If signature is verified then we will check if user holds the token or not
if (verify) {
const contractResponse = await publicClient.readContract({
address:
roomDetails.metadata.tokenGatingInfo.tokenGatingConditions[0].contractAddress,
abi: [
{
inputs: [{ name: "owner", type: "address" }],
name: "balanceOf",
outputs: [{ name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
],
functionName: "balanceOf",
args: [address],
});
const balance = Number(contractResponse);
if (balance < 1) {
return new Response("You don't hold token to join this room", {
status: 400,
});
} else {
// If user holds the token then we will generate access token
const accessToken = new AccessToken({
apiKey: process.env.API_KEY!,
roomId: roomId as string,
role: Role.HOST,
permissions: {
admin: true,
canConsume: true,
canProduce: true,
canProduceSources: {
cam: true,
mic: true,
screen: true,
},
canRecvData: true,
canSendData: true,
canUpdateMetadata: true,
},
});
const token = await accessToken.toJwt();
return new Response(token, { status: 200 });
}
} else {
return new Response("Incorrect signature", { status: 400 });
}
Now, let's call this API from client side once user signs the message.
app/[roomId]/page.tsx
import { useRoom } from "@huddle01/react/hooks";
const { state } = useRoom();
// Call this function in `onSuccess` callback of `useSignMessage`
const authenticateUser = async (signature: string) => {
// Call `/token` API to verify signature and generate access token
const tokenResponse = await fetch("/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
signature,
address,
message,
expirationTime,
roomId: params.roomId,
}),
});
const token = await tokenResponse.text();
// Join room if `state` is `idle` which we get from `useRoom`.
if (state === "idle")
await joinRoom({
roomId: params.roomId,
token,
});
};
You're all set! Happy Hacking! 🎉
Thank you for following this guide till end. You can find the full source code here (opens in a new tab).