Understanding ERC1155

Understanding ERC1155

The Basics

ERC1155 tokens are semi-fungible tokens that have a single token but multiple owners, unlike ERC721 which are one-of-one and only have a single owner of a specific token. an example would be a certificate from Udemy. A single certificate has multiple owners. i.e multiple people can have the same certificate.

The code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC1155, Ownable {
    constructor() ERC1155("") {}

    function 
    mint(address account, uint256 id, uint256 amount, bytes memory data)
    public
    onlyOwner
    {
        _mint(account, id, amount, data);
    }

    function 
    mintBatch(address to, uint256[] memory ids, uint256[] memory amounts,     bytes memory data)
    public
    onlyOwner
    {
        _mintBatch(to, ids, amounts, data);
    }
}

Keep in mind this is just the outline of the code. we are going to modify it a lot.
So now we'll head over to the online Remix IDE, and paste the above code.

Now I've made some changes to the code and explained it there itself

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract UdemyCompletion is ERC1155, Ownable {

    //! define the name and symbol of the ERC1155 token
    string public name;
    string public symbol;

    //max supply for a type of NFT and how many of those are minted so far
    //The first token Id will have a supply of 10, the second tokenId will have a supply of 20, and the 3rd tokenId will have a supply of 30.
    //The benifit of using an array here is we can push new certificates/nft's over time. we could also use mapping
    uint256[] public supplies = [10,20,30];
    uint8[] public minted = [0,0,0];


    constructor() ERC1155("") {
        //! set the name and symbol
        name="UdemyCourse";
        symbol="UDC";
    }

    //Anyone can mint our nft
    function 
    mint(uint256 id)
    public 
    {
        require(id<=supplies.length,"id should be the index of the supplies array,i.e which NFT certificate you want");
        require(minted[id]<=supplies[id]+1,"minted certificate should be less than the supply of the given NFT");

        //what I'm saying here is, whoever calls this funtion can mint 1 NFT
        _mint(msg.sender, id, 1, "");
        minted[id]+=1;
    }
    //! what is "id"?
    /*
    * Id is the Id of the type of certificate
    */
    //new certificate/NFT added
    function newEdition(uint256 supply) public onlyOwner{
        supplies.push(supply);
        minted.push(0);
    }
}

The problem with the above code is if through my account I _mint a NFT of tokenId 1 multiple times, I'll end up getting only one NFT. but the count of minted NFT will be increased. So I guess I'll probably need a mapping to map out an address to a token Id and run a require() statement before minting to check if the NFT has been minted by that address or not.

Now if you just run and mint the above code the NFT that you'll receive will have no image/metadata associated with it. It'll look something like this.

So let's move ahead and associate some metadata to it.

Understanding Metadata

The metadata structure for ERC1155 is the same as ERC721, the only difference is the way we point it to a tokenId in the contract.

In ERC721 the NFTs are one-of-one so each one of them has a unique metadata and we can keep a JSON file generated for each NFT and host it on a server and just keep incrementing the tokenID at the end of the domain name or IPFS hash. you don't have to worry about the code. It's taken care of in the background. Just remember to use the ERC721URIStorage library, which takes care of it all.

In the case of ERC1155 irrespective of how many times a user mints a token, the token Id will be the same. Secondly, the problem arises if we try to add another NFT to the collection. That might be a problem because that would mean we'll have to change the data on the metadata.json file, which is problematic because if we host it on IPFS, we cannot change that metadata. you'll have to change the URI every time you add a new NFT(token_id) to that collection.

A solution to the above problem is to just reference each tokenId with its own URI, so for eg: metadata URI for tokenID 1 and 2 will be:

Adding URI to each token individually.

Since we'll be posting our metadata on IPFS, mutating it is not possible. so we'll be associating a new URI for each tokenId.

//for each tokenId associate it with a URI
mapping(uint => string) public tokenURI;

//reset URI, if required for a given token Id.
function setURI(uint _id, string memory _uri) external onlyOwner {
    //check if the ID exists
    require(_id<=supply.length,"The Id does not exist");

    tokenURI[_id]=_uri;
    //it is manditory as per standards to mention in an event if the URI has 
    //changed
    emit URI("_uri","_id");
}   

//manditory override
function uri(uint _id) public override view returns (string memory) {
    return tokenURI[_id];
}     

//updated function when adding a new Edition
function addNewEdition(uint256 supply, string memory _uri) public onlyOwner {
    supplies.push(supply);
    minted.push(0);
    tokenURI[supplies.length-1] = _uri;
    emit URI(_uri, supplies.length-1);
}

Now that we've understood how to add a separate URI for each token, lets move forward

Adding mint price to a specific edition

Just like how we've added the amount of NFT's were minted and the supply for the respective NFT. we can add the price of the NFT in a similar way.

//Initial Eth cost per NFT
uint256[] public prices = [0.002 ether, 0.01 ether, 0.020 ether];

//when addeding new Edition
function addNewEdition(uint256 supply, uint256 price, string memory _uri) public onlyOwner {
    supplies.push(supply);
    minted.push(0);
    //add the price to the respective array
    prices.push(price);
    tokenURI[supplies.length-1] = _uri;
    emit URI(_uri, supplies.length-1);
}

function mint(uint256 id) public payable {
    require(id < supplies.length, "Token doesn't exist");
    require(supplies[id] >= minted[id]+1, "Token supply finished");
    //check if you have appropriate funds to mint the NFT
    require(msg.value >= prices[id], "Not enough ether");

    _mint(msg.sender, id, 1, "");
    minted[id] += 1;
}

in the addNewEdition() the price is in uint256 type. we have to add it in Wei instead of ether.

Withdrawing funds

Now that people have paid us in ether to mint our NFTs. we should be able to withdraw the funds that are in the contract.

Let us create a function to withdraw the above-mentioned funds.

function withdraw() external payable{
require(address(this).balance>0,'Balance is 0');
(bool success,)=msg.sender.call{
    value:address(this).balance
}("");
require(success,"Tx. failed")
}

Execute the above code using the appropriate values. below is the contract that we created. As you can see for a given token Id we have 2 different addresses owning the given NFT.

I've used IPFS to upload the given metadata and used its URL to create the NFT
There are plenty of videos on how to do the above on youtube.

The final NFT after minting will look as the follows