Gated content access by NFT ownership - Cloud Function example

Hey,

I’ve been experimenting with Moralis and created a cloud function to serve gated content by NFT ownership. I just wanted to share it, since I saw a lot of conversation around this topic but not a full code example (maybe I am missing it). I am new to Moralis and React/Javascript. So I am not sure if this is the correct way or if I am following the best practices. So please let me know if you see any issues with the implementation or the whole logic around it.

I’ve used two Testnet OpenSea NFTs for this that are in the below collection. Make sure that NFTs are not lazy-minted.
https://testnets.opensea.io/collection/testingthisstuff
The gated content in this example is just a string; “Gated content for NFT 1”
Each NFT in our example have a seperate gated content.

NFT to Gated content mapping is stored in the nftToPageMapping variable within the Cloud function itself. I thought this would be the best place to store the gated content in terms of security. (you are trusting Moralis to store your gated content, but we need start trusting from somewhere right :D) This can also be modified to store it in the database.
<token_address>_<token_id> maps to <gated content>
<token_address>_<token_id> points to the NFT that we want to have gated content with.

Here is the Cloud function with explanations:
Please change the nftToPageMapping variable in the code for your NFTs.

Moralis.Cloud.define("nftGatedContent", async (request) => {
    // To ensure user has logged-in.
    // The user object is passed by Moralis for logged in users.
    if (request.user) {

      // Mapping for NFT to gated content
      // This is where we store the gated content that NFT relates to.
      // You can expand this list for other NFTs
      // <token_address>_<token_id> -> <gated content>
      let nftToPageMapping = {
        /* Tesst */"0x0ea82ca03aa355941271efbe1b0ee66ef3a74ea3_112310352153202421499958867797769186319214905582065839985351320021994009788417": 
"Gated content 1",
        /* Test2 */"0x0ea82ca03aa355941271efbe1b0ee66ef3a74ea3_112310352153202421499958867797769186319214905582065839985351320023093521416193": 
"Gated content 2"

      };

      // Get logged in user's ETH address.
      const userEthAddress = request.user.get("ethAddress");

      // EthNFTTransfers is a table that stores user's NFT transactions. This table will automatically be updated by Moralis when a new user joins or an existing user makes an NFT transfer.
      const query = new Moralis.Query("EthNFTTransfers");

      // Filter the NFT transfters from EthNFTTransfers table to get the NFT transfers only for the current logged-in user.
      query.equalTo("to_address", userEthAddress);
      // Check if transaction is confirmed
      query.equalTo("confirmed", true);

      // Run the query
      const nftTransters = await query.find();

      // This is used to return the gated content for each NFT.
      // This is what we will return as output.
      let resultToReturn = {}

      // Iterate over NFTs of the logged-in user to get the token address and token id of NFT's
      for (let nftTransfer of nftTransters) {

        // Key for nftToPageMapping, <token_address>_<token_id> -> col#
        let key = nftTransfer.get("token_address") + "_" + nftTransfer.get("token_id");

        // Get the gated content this NFT points to.
        let currGatedContent = nftToPageMapping[key];
        resultToReturn[key] = currGatedContent;
      }
      return JSON.stringify(resultToReturn)
    }

    else {
      return "You are not authorized"
    }

  });

If the logged-in user owns the NFTs it will return the gated NFT content.
Example: {"Gated content 2":"0x0ea82ca0......","Gated content 1":"0x0ea82c....."}
If user doesn’t have any nfts with gated content it will return empty JSON; {}

Then in client-side. You can call this cloud function like below. You need

import { useEffect } from "react";
import { useMoralis, useMoralisCloudFunction } from "react-moralis";

function Cloudcall() {
  const { isAuthenticated, logout, authenticate } = useMoralis();

  // Here the User object passed to the request.
  const { data, error, isLoading } = useMoralisCloudFunction("nftGatedContent");

  useEffect(() => {
    //if (!isAuthenticated) router.replace("/");
  }, [isAuthenticated]);

  if (isAuthenticated) {
    return (
      <div className="h-screen w-screen flex flex-col items-center justify-center">
        <p>Here is the gated content: {data}</p>
        <button
          onClick={logout}
          className="px-7 py-4 text-xl rounded-xl bg-yellow-300"
        >
          Logout
        </button>
      </div>
    );
  }
  return (
    <div className="h-screen w-screen flex flex-col items-center justify-center">
      <p>You are not logged in.</p>
      <button
        onClick={() => authenticate({ signingMessage: "Authorize linking of your wallet to NFT Project" })
        }
        className="px-7 py-4 text-xl rounded-xl bg-yellow-300"
      >
        {!isAuthenticated ? "Login using Metamask" : "Logout"}
      </button>
    </div>
  );

}

export default Cloudcall;

And in app.js you need something like below;

import { MoralisProvider } from "react-moralis";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
  return (
    <MoralisProvider
      appId={process.env.NEXT_PUBLIC_APP_ID}
      serverUrl={process.env.NEXT_PUBLIC_SERVER_URL}
    >
      <Component {...pageProps} />
    </MoralisProvider>
  );
}
export default MyApp;

Hope it helps others as well.

Thanks,

5 Likes

This looks really close to what I was hoping to do. Thanks so much! Hopefully I can review this closer tonight and test an implementation.

Hey,

Thank you! You can also take look at this function; gettokenidowners

I’ve seen a couple of issues (logic-wise) with my implementation and now using an updated version of it. I ended up creating a new table to store nft owners using the afterSave trigger of EthTransactions. Additional logic is needed to only track specific NFTs (NFTs that you have gated content for). Otherwise, this table could be huge. I haven’t implemented this part yet and manually managing it using nft_name column.

Here is the Cloud function I am using now. You need to create a new Class called “NftOwners” and add the following columns; token_id, token_address, owner_address, nft_name. They are all strings. Here is what mine looks like;


Cloud Code:
syncNftOwners is used for the initial sync of NftOwners class from EthNFTTransfers. You can call it to automatically populate your NftOwners table. No need to call after the implementation as it will be automatically updated going forward by afterSave("EthNFTTransfers".

    // Catch the NFT transfer event when it is saved to the EthNFTTransfers by Moralis and use the request to store the latest owner of that NFT in our NftOwners table
    Moralis.Cloud.afterSave("EthNFTTransfers", async (request) => {
    const logger = Moralis.Cloud.getLogger();

    const query = new Moralis.Query("NftOwners");
    query.equalTo("token_id", request.object.get("token_id"));
    query.equalTo("token_address", request.object.get("token_address"));
    const row_object = await query.first({ useMasterKey: true });

    // If NFT already exists
    if (row_object) {
        // Update the owner to the laset owner
        row_object.set("owner_address", request.object.get("to_address"));
        // Save changes to the database
        try {
        await row_object.save({ useMasterKey: true });
        } catch (err) {
        logger.info(err);
        }
    }
    // If it's a new NFT.
    else {
        // Create and save it to the database
        var newNFT = new Moralis.Object("NftOwners");
        newNFT.set("token_id", request.object.get("token_id"));
        newNFT.set("token_address", request.object.get("token_address"));
        newNFT.set("owner_address", request.object.get("token_id"));
        newNFT.set("nft_name", "unnamed"); // TODO; not being used currently.
        await newNFT.save({ useMasterKey: true });
    }
    });

    Moralis.Cloud.define("getGatedContent", async (request) => {
    const logger = Moralis.Cloud.getLogger();

    // To ensure user has logged in. TODO: There are better approaches to this; https://docs.moralis.io/moralis-server/cloud-code/cloud-functions#more-advanced-validation
    if (request.user) {

        // Collection used for testing in Opensea; Testingthisstuff. https://testnets.opensea.io/collection/testingthisstuff
        // nftToGatedContent used for mapping from NFT to gated content.
        // NFT is identified by <token_address>_<token_id>
        // Mapping: <token_address>_<token_id> -> <gated content string>
        let nftToGatedContent = {
        "0x0ea82ca03aa355941271efbe1b0ee66ef3a74ea3_112310352153202421499958867797769186319214905582065839985351320021994009788417"
            : "Gated content for Tesst",

        "0x0ea82ca03aa355941271efbe1b0ee66ef3a74ea3_112310352153202421499958867797769186319214905582065839985351320023093521416193"
            : "Gated content for Test 2"
        };

        // Get logged in user's ETH address.
        const userEthAddress = request.user.get("ethAddress"); // Ex: returns "0xc74a9803cc566535672028b90ea32a6cce5064f0"

        // NftOwners is a table that we store NFT tokens and who owns them currently. This table will automatically updated when a user makes an NFT transfer.
        const query = new Moralis.Query("NftOwners");
        // Get only the logged-in user's NFT's
        query.equalTo("owner_address", userEthAddress);

        const nftsOwnedByUser = await query.find({ useMasterKey: true }); // Ex: [{"owner_address":"0xc74a9803cc566535672028b90ea32a6cce5064f0","createdAt":"2022-02-11T11:39:20.404Z","updatedAt":"2022-02-11T15:26:19.608Z","token_address":"0x0ea82ca03aa355941271efbe1b0ee66ef3a74ea3","token_id":"112310352153202421499958867797769186319214905582065839985351320021994009788417","nft_name":"Tesst","objectId":"nuUo1uZB0GgMEDfgknfExZpu"},{"owner_address":"0xc74a9803cc566535672028b90ea32a6cce5064f0","token_address":"0x0ea82ca03aa355941271efbe1b0ee66ef3a74ea3","nft_name":"Test2","token_id":"112310352153202421499958867797769186319214905582065839985351320023093521416193","createdAt":"2022-02-11T15:11:59.156Z","updatedAt":"2022-02-11T15:11:59.156Z","objectId":"vm8rFd6xUDkvtJkbKv5I4YkF"},{"owner_address":"0xc74a9803cc566535672028b90ea32a6cce5064f0","nft_name":"Test3","token_address":"0x88b48f654c30e99bc2e4a1559b4dcf1ad93fa656","token_id":"112310352153202421499958867797769186319214905582065839985351320021994009788417","createdAt":"2022-02-11T15:12:15.943Z","updatedAt":"2022-02-11T15:12:15.943Z","objectId":"N8h3ezP4jZTWgesulpCg2dT7"},{"nft_name":"Unlock Key","token_address":"0xb75c36fdfdf6f38363719dd3b1dd8ffe1ebc172f","token_id":"1","owner_address":"0xc74a9803cc566535672028b90ea32a6cce5064f0","createdAt":"2022-02-11T15:12:30.235Z","updatedAt":"2022-02-11T15:12:35.191Z","objectId":"jZreskE1VfGX87DegyLWRI4T"}]

        // If user does not have any NFTs
        if (nftsOwnedByUser.length == 0) { return "NotEnoughNFTs" }

        // Result to return by this method.
        let gatedContentToReturn = {};

        // Iterate over NFTs and add gated content to response
        for (let nft of nftsOwnedByUser) {
            var nftKEy = nft.get("token_address") + "_" + nft.get("token_id");
            gatedContentToReturn[nft.get("nft_name")] = nftToGatedContent[nftKEy]
        }


        return gatedContentToReturn; // returns {"":"Gated content for Tesst"}
    }
    else {
        return "Not authorized";
    }

    });

    // In case you need to sync the NftOwners table manually.
    // This can be called to initialize your NftOwners table.
    // Goes throug the EthNFTTransfers table and pick the latest transfers to add into NftOwners table.
    Moralis.Cloud.define("syncNftOwners", async (request) => {
    const logger = Moralis.Cloud.getLogger();

    const ethNFTTransfersQuery = new Moralis.Query("EthNFTTransfers");
    const ethNFTTransfers = await ethNFTTransfersQuery.find({ useMasterKey: true });

    // Sort Nft transactions by block_number ascending. Latest transaction is at the end.
    ethNFTTransfers.sort(function (a, b) {
        var x = a["block_number"]; var y = b["block_number"];
        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
    });

    // Iterate over each transfer starting from lowest block number (oldest transaction)
    for (let nftTransfer of ethNFTTransfers) {

        // Query NftOwners to find if NFT is already in the table.
        const getNFTQuery = new Moralis.Query("NftOwners");
        getNFTQuery.equalTo("token_id", nftTransfer.get("token_id"));
        getNFTQuery.equalTo("token_address", nftTransfer.get("token_address"));
        const row_object = await getNFTQuery.first({ useMasterKey: true });

        // If NFT already exists
        if (row_object) {
        // Update the owner to the laset owner
        row_object.set("owner_address", nftTransfer.get("to_address"));
        // Save changes in the database
        try {
            await row_object.save({ useMasterKey: true });
        } catch (err) {
            logger.info(err);
        }
        }
        // If it's a new NFT.
        else {
        // Create and save it to the database
        var newNFT = new Moralis.Object("NftOwners");
        newNFT.set("token_id", nftTransfer.get("token_id"));
        newNFT.set("token_address", nftTransfer.get("token_address"));
        newNFT.set("owner_address", nftTransfer.get("to_address"));
        newNFT.set("nft_name", "unnamed");
        await newNFT.save({ useMasterKey: true });
        }

    }

    return "success";
    });
2 Likes

@codeinpeace, so you would need to manually put in all your token_id, token_address, nft_name? (so owner_address can update accordingly?)

Hi, that’s not required in my case since I initially own all of my NFTs. So I just call syncNftOwners function to auto-populate the NftOwners table. The function goes through my transactions and puts the NFT information into the NftOwners table. Also, the account number I use for this has only my NFTs that I use for gated content. So no other NFT transactions are in my account which makes it possible to do it automatically.

gonna ask a bit of dump question, because I don’t know reactjs, how do you call the function in JavaScript?

async function getGatedContent() ? 
do you need to pass on any ethaddress ?

nvm I got that working

1 Like

Ah the solutions here is for specific NFT

Because I am trying to build dapp (learning) - where individual who hold that NFT able to see the gated content.

I was building a table with prefilled token_address and token_id (was filling myself, so I could use the above method also) and run a function to find who’s the current owner of the NFT.

I am running through each check (checking each role of current owner) for all of them using the aftersave on my ethaddress (since if I sold the NFT to person A or Person A sold to Person B [imagine I collect commission in opensea, I will get comission for every sale to that NFT]).

I was using web api to check the ownership of the NFT in the database at the moment,

I am thinking if there is a better way to do it so I won’t check say 10-20k row (doing 10-20k api call) each time.

@codeinpeace

I figure out, at your first post, you do not need to have "if (request.user) { "
you can do {requireUser: true} at the end

Has anyone had any luck coding a webpage where the user connects their wallet, then signs via metamask to get access to gated content? Here is an example of what I’m trying to do. I believe that this gating is a feature for shopify stores but if I wanted to build something like this would I use a solution similar to what is shown above (except, without a token id, just a contract address to give access to all nft holders)? Does anyone have any example where this has been implemented? Thanks!

It looks like in that example, routes are protected and users are allowed/denied access based on NFT ownership.

For a general check against the contract, you could probably use getNFTs and filter by the contract address with the token_addresses param.

1 Like

Thanks so much for that @alex! I’m going to play around with it