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:
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.
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.
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.
Let’s create our own in-app crypto subscription model.
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!
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 ownerERC1155.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 uint
s.
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.
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)
With Web3.js, we can call important functions inside our contract. Let’s see how we can interact with the subscription model to:
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.
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(), }, ], }); };
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(); };
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!
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!
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 — Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.