Santhosh Reddy Full-stack Web3 developer. Loves building applications in the Web3 space, and mostly works on Ethereum and Polygon. Core tech stack is JavaScript, TypeScript, and Solidity.

NFT access tokens: Build a crypto subscription model

10 min read 2920

Solidity Logo

NFTs, NFTs, NFTs… This Web3 word has become commonplace throughout the world. Creators and developers, alike, are interested in non-fungible tokens. Creators view NFTs as a kind of digital art, but developers and entrepreneurs see NFTs as a utility tool that can be used to address several issues and add value to many everyday applications.

As a Web3 developer and enthusiast, I’m interested in how NFTs can effectively act as access tokens to verify user identity. Have you ever subscribed to an over-the-top (OTT) platform? If you’re a developer, you’ll definitely be familiar with the mechanism behind this type of platform.

In this article, we’ll compare traditional and in-app crypto subscription models. Then, we’ll walk through a tutorial that will demonstrate how to create an ERC-1155 smart contract and build an in-app NFT subscription model using the smart contract, JavaScript, Web3.js, and Moralis.

Jump ahead:

Prerequisites

To follow along with the tutorial portion of this article, you’ll need the following:

Before we move into the tutorial, let’s compare traditional and in-app crypto subscription models.

Traditional subscription model

When a user registers and completes the transaction for a subscription plan, their login credentials are stored in the database along with details about their subscription plan. When the user logs in to the app, the app checks if they are subscribed to a plan and then loads the data accordingly.

In-app crypto subscription model

Now let’s look at how a subscription model would function in the Web3 world. When a user connects their Web3 wallet (for example, MetaMask) and subscribes to a plan by paying the subscription price in crypto (for example, ETH or MATIC), an NFT gets minted on-chain to the user’s address.

Wherever the user logs in to the DApp and connects their wallet, the DApp checks if the user owns an NFT of any of the subscription plans and then loads data accordingly. In this scenario, the NFT acts as an access or subscription token to the platform or DApp.

A crypto subscription model offers several benefits over a traditional subscription model. First, users can easily transfer ownership of their subscription in exchange for crypto by trading the NFT. The crypto subscription model is also more efficient and offers a more secure proof of ownership mechanism compared to a traditional model.

We made a custom demo for .
No really. Click here to check it out.

Let’s create our own in-app crypto subscription model.

In-app NFT subscription model tutorial

To develop an in-app NFT subscription model, we’ll start by building an ERC-1155 smart contract. Then, we’ll write some JavaScript functions to add the subscription model to a DApp and deploy the DApp on Polygon’s Mumbai testnet.

We’ll use the Web3.js API to interact with the network and our smart contract and the Moralis IPFS to store the NFT metadata.

Let’s dive in!

Creating an ERC-1155 smart contract

The first step in developing our subscription model is to write a smart contract in Solidity using the ERC-1155 multi-token standard.

To develop a smart contract for an OTT platform, we’ll use the OpenZeppelin library. Let’s try to write the code for our smart contract using solidity.

We first need to import the following required Solidity files from the OpenZeppelin library:

  • SafeMath.sol: performs maths functions on numbers or uint
  • ownable.sol: stores contract deployer’s address data and recognizes them as the contract owner
  • ERC1155.sol: helps us create the ERC-1155 contract and helps users mint NFTs whenever they purchase or subscribe to a plan
//SPDX-Licence-Identifier: MIT
pragma solidity ^0.8.7;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol";
import "github.com/OpenZeppelin/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol";
import "github.com/OpenZeppelin/openzeppelin-contracts/contracts/access/Ownable.sol";

Next, we need to name and initiate our smart contract and also initiate the SafeMath.sol file to the smart contract’s uints.

contract DFlix is ERC1155, Ownable {

    using SafeMath for uint256;

}

Now, we have a constructor that inputs and holds the name, symbol, and metadata uri of the DFlix contract and a uint variable, totalPlans, that returns the number of available subscription plans.

uint256 public totalPlans; 

constructor(string memory name, string memory symbol, string memory uri) ERC1155(uri) {}

The contract includes structs and mappings to store required data, such as the plan’s name, metadata uri, count of active subscribers, price (in Wei), and valid time period (in milliseconds) as well as the subscriber’s plan and subscribe date.

The struct plan is mapped with a uint and stores plan details in the mapping plans; these are the NFTs. Next, the struct subscriber is mapped with an address and stores the subscription details of a subscriber or user in the mapping subscribers.

struct plan {
        string name;
        string uri;
        uint256 subscribers;
        uint256 price;
        uint time;
    }

    struct subscriber {
        uint256 plan;
        uint256 date;
    }

    mapping(uint256 => plan) internal plans;

    mapping(address => subscriber) internal subscribers;

Next, we have some modifiers, like correctId which verifies if the planId given by the function caller as a parameter exists. ifExpired is another function that acts as a modifier; it checks the user’s data and communicates if they are eligible to subscribe by returning a Boolean value.

    modifier correctId(uint id) {
        require(id <= totalPlans && id>0, "provide a correct planID");
        _;
    }

    function ifExpired(uint id) internal view returns(bool) {
        if(subscribers[msg.sender].plan == id) {
            if((block.timestamp).sub(subscribers[msg.sender].date) < plans[id].time) {
                return false;
            } else {
                return true;
            }
        } else {
            return true;
        }
    }

Here, we also have an infrequently used (but useful!) function, setURI, which can set the metadata uri for the whole ERC1155 contract.

    function setURI(string memory uri) external onlyOwner {
        _setURI(uri);
    }

Next, we have the main functions: addPlan and updatePlan. These functions can only be called by the contract’s owner. addPlan and updatePlan help the owner add a subscription plan to their platform; the plan’s data can be updated later. These functions deal with data stored in the mapping plans; their function parameters represent required data that must be provided by the owner.

    function addPlan(string memory _name, string memory uri, uint256 price, uint time) external onlyOwner {
        totalPlans = totalPlans.add(1);
        uint256 id = totalPlans.add(1);
        plans[id] = plan(_name, uri, 0, price, time);
    }

    function updatePlan(uint id, string memory _name, string memory uri, uint256 price, uint time) external onlyOwner {
        plans[id] = plan(_name, uri, plans[id].subscribers, price, time);
    }

This brings us to the most important contract function: subscribe. Let’s break it down. The subscribe function checks if the user has provided a correct planId using the correctId modifier, uses the ifExpired function to verify that they are eligible to subscribe, and checks if they are paying the correct price in crypto (for example, ETH or MATIC) as it is a payable function.

After performing these checks, the subscribe function increments the subscription count of the plan and updates the subscriber’s data with the planId and the current timestamp. Then, it clears all of the user’s previous data by decrementing the subscription count of the previous plan (or the same plan if it’s expired). It also destroys the user’s NFTs for any inactive plans using the _burn function.

Lastly, the function mints an NFT for the user’s subscription plan and collects the subscription fee in crypto (ETH/MATIC) as it’s a payable function.

The DFlix smart contract specifies that a user can hold only one NFT at a time; when the user subscribes to a new plan, the NFT of their prior plans gets burned.

    function subscribe(uint256 planId) external correctId(planId) payable {
        require(ifExpired(planId) == true, "your current plan hasn't expired yet");
        require(msg.value == plans[planId].price, "please send correct amount of ether");
        plans[planId].subscribers = (plans[planId].subscribers).add(1);
        subscribers[msg.sender] = subscriber(planId, block.timestamp);
        _burn(msg.sender, subscribers[msg.sender].plan, balanceOf(msg.sender, subscribers[msg.sender].plan));
        plans[subscribers[msg.sender].plan].subscribers = (plans[subscribers[msg.sender].plan].subscribers).sub(balanceOf(msg.sender, subscribers[msg.sender].plan));
        _mint(msg.sender, planId, 1, "");
        payable(msg.sender).transfer(msg.value);
    }

Now, let’s look at the remaining functions listed in the smart contract.

The withdraw function can only be called by the owner. The owner can call this function to collect their revenue by transferring the contract’s ETH or MATIC balance directly to their wallet.

    function withdraw(uint256 amount) external onlyOwner {
        (bool success, ) = payable(owner()).call{value: amount}("");
        require(success, "transfer failed");
    }

The remaining functions here are the read/call functions. The currentPlan checks if the user has an active plan and returns the user’s current active planId from the data stored in the subscribers mapping. If the user doesn’t have any active subscription plan, the function fails or reverts.

    function currentPlan(address user) public view returns(uint) {
        require((block.timestamp).sub(subscribers[msg.sender].date) < plans[subscribers[msg.sender].plan].time, "deosn't have any active plan");
        return subscribers[user].plan;
    }

The tokenURI function returns the uri of the plan’s NFT. tokenSupply returns the subscription count of a plan, tokenPrice returns a plan’s price, totalSupply returns the number of subscription plans available, and balance returns the contract’s balance which can only be called by the owner.

    function tokenURI(uint id) public correctId(id) view returns(string memory) {
        return plans[id].uri;
    }

    function tokenSupply(uint id) public correctId(id) view returns(uint) {
        return plans[id].subscribers;
    }

    function tokenPrice(uint id) public correctId(id) view returns(uint) {
        return plans[id].price;
    }


    function totalSupply() public view returns(uint) {
        return totalPlans;
    }

Now, let’s wrap up all the code blocks and view our smart contract in its complete form:

//SPDX-Licence-Identifier: MIT
pragma solidity ^0.8.7;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol";
import "github.com/OpenZeppelin/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol";
import "github.com/OpenZeppelin/openzeppelin-contracts/contracts/access/Ownable.sol";

contract DFlix is ERC1155, Ownable {

    using SafeMath for uint256;

    struct plan {
        string name;
        string uri;
        uint256 subscribers;
        uint256 price;
        uint time;
    }

    struct subscriber {
        uint256 plan;
        uint256 date;
    }

    mapping(uint256 => plan) internal plans;

    mapping(address => subscriber) internal subscribers;

    uint256 public totalPlans;

    constructor(string memory name, string memory symbol, string memory uri) ERC1155(uri) {}


    modifier correctId(uint id) {
        require(id <= totalPlans && id>0, "provide a correct planID");
        _;
    }

    function setURI(string memory uri) external onlyOwner {
        _setURI(uri);
    }



    function ifExpired(uint id) internal view returns(bool) {
        if(subscribers[msg.sender].plan == id) {
            if((block.timestamp).sub(subscribers[msg.sender].date) < plans[id].time) {
                return false;
            } else {
                return true;
            }
        } else {
            return true;
        }
    }

    function addPlan(string memory _name, string memory uri, uint256 price, uint time) external onlyOwner {
        totalPlans = totalPlans.add(1);
        uint256 id = totalPlans.add(1);
        plans[id] = plan(_name, uri, 0, price, time);
    }

    function updatePlan(uint id, string memory _name, string memory uri, uint256 price, uint time) external onlyOwner {
        plans[id] = plan(_name, uri, plans[id].subscribers, price, time);
    }

    function subscribe(uint256 planId) external correctId(planId) payable {
        require(ifExpired(planId) == true, "your current plan hasn't expired yet");
        require(msg.value == plans[planId].price, "please send correct amount of ether");
        plans[planId].subscribers = (plans[planId].subscribers).add(1);
        subscribers[msg.sender] = subscriber(planId, block.timestamp);
        _burn(msg.sender, subscribers[msg.sender].plan, balanceOf(msg.sender, subscribers[msg.sender].plan));
        plans[subscribers[msg.sender].plan].subscribers = (plans[subscribers[msg.sender].plan].subscribers).sub(balanceOf(msg.sender, subscribers[msg.sender].plan));
        _mint(msg.sender, planId, 1, "");
        payable(msg.sender).transfer(msg.value);
    }

    function currentPlan(address user) public view returns(uint) {
        require((block.timestamp).sub(subscribers[msg.sender].date) < plans[subscribers[msg.sender].plan].time, "deosn't have any active plan");
        return subscribers[user].plan;
    }

    function tokenURI(uint id) public correctId(id) view returns(string memory) {
        return plans[id].uri;
    }

    function tokenSupply(uint id) public correctId(id) view returns(uint) {
        return plans[id].subscribers;
    }

    function tokenPrice(uint id) public correctId(id) view returns(uint) {
        return plans[id].price;
    }


    function totalSupply() public view returns(uint) {
        return totalPlans;
    }

    function balance() public view onlyOwner returns (uint) {
        return address(this).balance;
    }

    function withdraw(uint256 amount) external onlyOwner {
        (bool success, ) = payable(owner()).call{value: amount}("");
        require(success, "transfer failed");
    }


}

This smart contract demonstrates an example of how NFTs can act as access tokens.

Next, we need to compile the smart contract on the Remix IDE and then deploy it to Ethereum, or any other EVM-based network such as Polygon. If you are not familiar with compiling a contract on the Remix IDE, you may find this guide helpful.

Adding the subscription model to the DApp

We’ll build an in-app NFT subscription model in JavaScript and use the Web3.js API to add the subscription model to our DApp’s frontend.

First, install the Web3.js API using the following command:

npm i Web3

Then, initiate it with the RPC node of your choice. For this tutorial, we’ll use Moralis Speedy Nodes.

We’ll use the Moralis IPFS to store the NFT metadata.

First, create an account on Moralis. Next, create a server and install the Moralis SDK using its UNPKG CDN link.

Next, initiate the Moralis SDK with the addId and serverUrl using the Moralis.start() function.

We’ll initiate our smart contract using Web3.js with the contract’s ABI and address.

import Web3 from "Web3"
import "https://unpkg.com/moralis/dist/moralis.js";

var NODE_URL = "https://speedy-nodes-nyc.moralis.io/<access-key>/polygon/mumbai";
var provider = new Web3.providers.HttpProvider(NODE_URL);
var Web3 = new Web3(provider);

const serverUrl = "https://<key>.usemoralis.com:2053/server";
const appId = "<app-id>";
Moralis.start({ serverUrl, appId });

// Initiate the contract
var contract = new Web3.eth.Contract(abi, address)

Interacting with the subscription model

With Web3.js, we can call important functions inside our contract. Let’s see how we can interact with the subscription model to:

How to add a subscription plan

The addPlan function converts the price from ETH to Wei using the Web3.js utils. Next, the uploadFiles function uploads the metadata object files, such as images, to IPFS. The file values are replaced by the IPFS url.

After the uploadFiles function has executed, the metadata is uploaded to IPFS as a JSON file. Then, the contract’s addPlan function (which includes parameters like the subscription plan’s name, time period, metadata url, and price converted to Wei) is called with Web3.js.

// this is my function to upload files like images inside the metadata to IPFS

var data;

async function uploadFiles() {

  for (var i = 0; i < Object.keys(data).length; i++) {

    if (Object.values(data)[i].type && Object.values(data)[i].lastModified) {

      const a = new Moralis.File(Object.values(data)[i].name,Object.values(data)[i]);

      await a.saveIPFS().then(() => {

        data[Object.keys(data)[i]] = "https://ipfs.io/ipfs/" + a.hash();

      });

    }

  }

}

const addPlan = async function (name, price, time, metadata) {
  const _price = Web3.utils.toWei(price.toString(), "ether");
  data = metadata;
  await uploadFiles();
  const a = new Moralis.File("metadata.json", {
    base64: btoa(JSON.stringify(data)),
  });
  a.saveIPFS().then(async () => {
    var url = "https://ipfs.io/ipfs/" + a.hash();
    await window.ethereum.request({ method: "eth_requestAccounts" });
    await window.ethereum.request({
      method: "eth_sendTransaction",
      params: [
        {
          from: user.address,
          to: contract._address,
          data: contract.methods.
                addPlan(name, url, _price, time).encodeABI(),
        },
      ],
    });
  });
};

Now let’s see other functions that call the functions in our contract using Web3.js.

How to subscribe to a plan

The subscribe function calls window.ethereum to request payment for the plan.

const subscribe = async function (id) {
   var price = await contract.methods.tokenPrice(id).call();
   await window.ethereum.request({ method: "eth_requestAccounts" });
   await window.ethereum.request({
     method: "eth_sendTransaction",
     params: [
       {
         from: user,
         to: contract._address,
         value: Web3.utils.toHex(price),
         data: contract.methods.subscribe(id).encodeABI(),
       },
     ],
  });
};

How to check if a user has an active plan

The read function, currentPlan is called using Web3.js. This function is used to determine if a user has an active subscription plan.

const currentPlan = async function (user) {
  return await contract.methods.currentPlan(user).call();
};

How to withdraw revenue from a contract

The withdraw function uses window.ethereum to withdraw the contract’s revenue in MATIC since the contract is deployed to Polygon’s Mumbai network.

const withdraw = async function (amount) {
  await window.ethereum.request({ method: "eth_requestAccounts" });
  await window.ethereum.request({
    method: "eth_sendTransaction",
    params: [
      {
        from: user,
        to: contract._address,
        data: contract.methods.withdraw(amount).encodeABI(),
      },
    ], 
  });
};

By now, you should have a better understanding of how to use NFTs as access tokens for a subscription model. I hope you found this exercise helpful!

Conclusion

In this article, we demonstrated how NFTs can act as access tokens. We walked through a demo showing how to set up an NFT subscription model and add it to a DApp using Web3.js in the frontend, Solidity at the backend, and Morlais for IPFS.

I hope you enjoyed this Solidity and Web3.js tutorial. If you have suggestions or feedback, or if you encounter any vulnerability or an error in the code, please use the comments below.

Meet you in the next article. Until then, keep learning, keep building, and keep exploring in the Web3 space.

WAGMI!

WazirX, Bitso, and Coinsquare use LogRocket to proactively monitor their Web3 apps

Client-side issues that impact users’ ability to activate and transact in your apps can drastically affect your bottom line. If you’re interested in monitoring UX issues, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — .

Santhosh Reddy Full-stack Web3 developer. Loves building applications in the Web3 space, and mostly works on Ethereum and Polygon. Core tech stack is JavaScript, TypeScript, and Solidity.

Leave a Reply