Token Gated Rooms

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 required TOKEN.
  • The generation of the accessToken is done using the Huddle01 Server SDK.
architecture

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).

Audio/Video Infrastructure designed for developers to empower them to ship simple yet powerful Audio/Video Apps.
support
company
Copyright © 2024 Graphene 01, Inc. All Rights Reserved.