So you’re starting a new NFT project and you want to align with current best practices. Where should you start?

Let’s take a look at some of the current NFT standards we have in place:

You can check out the NFT-Standards Github repository to follow along with the code samples and try out the code locally to mint your own NFTs on the Rinkeby testnet.

By the end of this article and workshop, you will be able to mint NFTs using multiple standards and see the minted NFTs on OpenSea on the Rinkeby testnet with all the associated metadata.

Multiple types of Tokens Minted in a Single Transaction with a ERC-1155 Contract

ERC-721 Non-Fungible Token Standard

_ERC-721 defines a minimum interface a smart contract must implement to allow unique tokens to be managed, owned, and traded. It does not mandate a standard for token metadata or restrict adding supplemental functions._

ERC-721 is one of the most common implementations for NFTs.  A very common way to implement a ERC-721 smart contract is to extend the OpenZeppelin implementation.

The specification allows for the following behavior:

  • Transfer NFTs between accounts
  • Access control limiting who can transfer NFTs
  • Trade NFTs for other currencies
  • Identify the total supply of NFTs on a network
  • Query for the owners of a specific asset

The specification can also be extended with the following optional behavior:

Some limitations of the specification:

  • When transferring multiple NFTs, each one requires a separate transaction
  • Each contract can only represent a single type of NFT
  • Smart contract must implement IERC721Receiver to receive NFTs
  • No support for semi-fungible tokens

The basic steps to implement a NFT smart contract and mint an image-based NFT on testnet are as follows:

  • Create a smart contract
  • Deploy the smart contract
  • Upload and pin NFT image data to IPFS
  • Create JSON metadata for NFT
  • Invoke the smart contract to mint an NFT
  • Verify NFT on Opensea and Etherscan on testnet

You can create a free account on Infura to store your image data on IPFS!

Creating an ERC-721 Compliant Smart Contract

Let’s build a smart contract that extends the OpenZeppelin implementation.  This smart contract will not implement pausable, burnable or royalty extensions and will use a static metadata file.

Basic implementation Example

Install the OpenZeppelin package with npm (if needed):

npm i @openzeppelin/contracts

Check out the source code on Github for the Infura721NFT.sol contract or review the code locally after cloning the NFT-Standards Github repository.

Infura721NFT.sol

pragma solidity ^0.8.13;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Infura721NFT is ERC721URIStorage, Ownable {
   using Counters for Counters.Counter;
   Counters.Counter private _tokenIds;

   constructor() ERC721("Infura721NFT", "INFURA721") {}

   function mintNFT(address recipient)
       public
       returns (uint256)
   {
       _tokenIds.increment();

       uint256 newItemId = _tokenIds.current();
       _safeMint(recipient, newItemId);

       return newItemId;
   }

   function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
       _requireMinted(tokenId);

       return "https://raw.githubusercontent.com/anataliocs/NFT-Standards/main/metadata/InfuraNFT.json";
   }

   function contractURI() public view returns (string memory) {
       return "https://raw.githubusercontent.com/anataliocs/NFT-Standards/main/metadata/opensea-contract-721.json";
   }

   function _requireMinted(uint256 tokenId) internal view virtual {
       require(_exists(tokenId), "ERC721: invalid token ID");
   }
}

This basic example extends ERC721URIStorage and Ownable.

This basic contract implementation has the following features:

  • Extends the OpenZeppelin ERC721 implementation
  • Tracks itemId of the NFT using a OpenZeppelin Counter
  • Exposes a function to mint a new NFT
  • Stores the URI to the JSON metadata
  • Overriding the tokenURI() function helps NFT metadata display correctly on OpenSea
  • Overriding the contractURI() function helps to display storefront level data on OpenSea

Example NFT Metadata JSON

This metadata follows the OpenSea standard allowing the inclusion of attributes and traits.

{
 "description": "Infura NFT 721 Demo",
 "external_url": "ipfs://QmaBm1F7vUujz5ZURDXS3yqeTsMFUEVW5M7bGQbTQr7vJK",
 "image": "ipfs://QmaBm1F7vUujz5ZURDXS3yqeTsMFUEVW5M7bGQbTQr7vJK",
 "name": "Infura721NFT",
 "background_color": "#FFF"
}

Example Contract-Level Metadata JSON

This metadata follows the OpenSea standard allowing the inclusion of attributes and traits.  Check out the OpenSea page on storefront-level contract metadata.

{
 "name": "Infura 721 NFT Items",
 "description": "Infura 721 NFT Items used in workshops, tutorials and reference applications.",
 "image": "https://raw.githubusercontent.com/anataliocs/NFT-Standards/main/images/infura_lockup_red.png",
 "external_link": "https://infura.io/",
 "seller_fee_basis_points": 0
}

Calling the Deployed ERC-721 Compliant Smart Contract

After deploying this contract to testnet or locally, you can invoke the mintNFT() function with the following script:

mintSingleNFT.js

require("dotenv").config();

const { CONTRACT_ADDRESS, PUBLIC_ADDRESS } = process.env;

// Loading the compiled contract Json
const contractJson = require("../build/contracts/Infura721NFT.json");

module.exports = async function (callback) {
 // web3 is injected by Truffle
 const contract = new web3.eth.Contract(
   contractJson.abi,
   CONTRACT_ADDRESS // this is the address generated when running migrate
 );

 // get the current network name to display in the log
 const network = await web3.eth.net.getNetworkType();

 // Generate a transaction to calls the `mintNFT` method
 const tx = contract.methods.mintNFT(PUBLIC_ADDRESS);
 // Send the transaction to the network
 const receipt = await tx
   .send({
     from: (await web3.eth.getAccounts())[0], // uses the first account in the HD wallet
     gas: await tx.estimateGas(),
   })
   .on("transactionHash", (txhash) => {
     console.log(`Mining ERC-721 transaction for a single NFT ...`);
     console.log(`https://${network}.etherscan.io/tx/${txhash}`);
   })
   .on("error", function (error) {
     console.error(`An error happened: ${error}`);
     callback();
   })
   .then(function (receipt) {
     let batchGasCost = receipt.gasUsed * 3;
     // Success, you've minted the NFT. The transaction is now on chain!
     console.log(
       `Success: The single ERC-721 NFT has been minted and mined in block ${receipt.blockNumber} which cost ${receipt.gasUsed} gas`
         + `\n If you were to execute this script 3 times to perform a batch mint, the gas cost would be ${batchGasCost} \n`
     );
     callback();
   });
};

This script does the following:

  • Pulls deployed contract address and destination wallet from the .env file
  • Uses web3.js to load the existing deployed smart contract
  • Invokes the mintNFT() function of the contract, passing in the wallet address of the recipient
  • Several listeners are attached to handle success and error cases
  • A link to the transaction on Etherscan is printed out to the console
  • Then, the block number and gas cost is printed out to console

This script relies on the HDWalletProvider library using an Infura Ethereum Rinkeby Testnet node and the mnemonic stored in your local .env file to sign the transaction.

Network config in truffle-config.js

...

rinkeby: {
 provider: () => new HDWalletProvider(mnemonic, infuraURL),
 network_id: 4, // Rinkeby's id
 gas: 15500000, // Rinkeby has a lower block limit than mainnet
 confirmations: 2, // # of confs to wait between deployments. (default: 0)
 timeoutBlocks: 200, // # of blocks before a deployment times out  (minimum/default: 50)
 skipDryRun: true, // Skip dry run before migrations? (default: false for public nets )
},

...

Additional ERC-721 Specification Details

Transfers may be only be initiated by (by default):

  • The owner of an NFT
  • The approved address of an NFT
  • An authorized operator of the current owner of an NFT

The specification is very open-ended, un-opinionated and allows flexible behavior such as:

  • Read-only NFT registries
  • Disallow transfers if the contract is paused
  • Blacklist certain addresses from receiving the NFT
  • Charging a fee to both parties of a transaction

Also, receiving contracts must implement IERC721Receiver to safely receive ERC721 NFTs.  Be sure to use the safeTransferFrom() function to ensure the receiving address can handle ERC721 NFTs by implementing the onERC721Received() function.

ERC-721a Non-Fungible Token Standard with Batch-Minting

ERC-721a is an extension of ERC-721 that provides significant gas-savings for batch-minting of NFTs while still being ERC-721 compliant.

This is accomplished primarily through 3 optimizations:

  • Removing duplicate storage from OpenZeppelin’s (OZ) ERC721Enumerable
  • Updating the owner’s balance once per batch-mint request, instead of per minted NFT
  • Updating the owner data once per batch-mint request, instead of per minted NFT

ERC-721a Characteristics:

  • Moves initialization of token ownership from the minting stage to transferring stage
  • Is heavily optimized for generative artwork NFT collections
  • Gas-savings increase for more popular projects
  • Best used for NFTs with a busy mint phase
  • Prioritizes gas savings for the minting phase

Limitations of ERC-721a:

  • More expensive for single NFT mints
  • Not ideal for pure utility NFTs that do not have a busy mint phase
  • Each contract can only represent a single type of NFT
  • No support for semi-fungible tokens

Creating an ERC-721a Compliant Smart Contract

Install the Azuki ERC-721a package with npm (if needed):

npm install --save-dev erc721a

Basic implementation Example

Check out the source code on Github for Infura721aNFT.sol** **or review the code locally after cloning the NFT-Standards Github repository.

Infura721aNFT.sol

pragma solidity ^0.8.13;

import "@openzeppelin/contracts/access/Ownable.sol";
import "erc721a/contracts/ERC721A.sol";

contract Infura721aNFT is ERC721A, Ownable {

   event Mint(uint256 _value, string tokenURI);

   constructor() ERC721A("Infura721aNFT", "INFURA721a") {}

   function mint(address recipient, uint256 quantity) public returns (uint256) {

       _safeMint(recipient, quantity);

       return 0;
   }

   function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {

       return "https://raw.githubusercontent.com/anataliocs/NFT-Standards/main/metadata/InfuraNFT.json";
   }

   function contractURI() public view returns (string memory) {
       return "https://raw.githubusercontent.com/anataliocs/NFT-Standards/main/metadata/opensea-contract-721a.json";
   }
}

This basic example extends ERC721A and Ownable.

This contract has the following features:

  • Extends the Azuki ERC721A implementation
  • Exposes mint() function to mint a batch of NFT
  • Maintains an internal mapping of address balances.
  • Overriding the tokenURI() function helps NFT metadata to display correctly on OpenSea
  • Overriding the contractURI() function helps to display storefront-level data on OpenSea

Calling the deployed ERC-721a Compliant Smart Contract

After deploying this contract to testnet or locally, you can invoke the mint() function to mint multiple NFTs with the following script:

mintBatchNFT.js

require("dotenv").config();

const {PUBLIC_ADDRESS, CONTRACT_ADDRESS_721A} = process.env;

// Loading the compiled contract Json
const contract721aJson = require("../build/contracts/Infura721aNFT.json");

module.exports = async function (callback) {
   // web3 is injected by Truffle
   const contract721a = new web3.eth.Contract(
       contract721aJson.abi,
       CONTRACT_ADDRESS_721A // this is the address generated when running migrate
   );

   // get the current network name to display in the log
   const network = await web3.eth.net.getNetworkType();

   // Generate a transaction to calls the `mint` function
   const tx721a = contract721a.methods.mint(PUBLIC_ADDRESS, 3);
   // Send the transaction to the network
   await tx721a
       .send({
           from: (await web3.eth.getAccounts())[0], // uses the first account in the HD wallet
           gas: await tx721a.estimateGas(),
       })
       .on("transactionHash", (txhash) => {
           console.log(`Mining ERC-721a transaction for a batch of 3 NFTs ...`);
           console.log(`https://${network}.etherscan.io/tx/${txhash}`);
       })
       .on("error", function (error) {
           console.error(`An error happened: ${error}`);
           callback();
       })
       .then(function (receipt) {
           // Success, you've minted the NFT. The transaction is now on chain!
           console.log(
               `\n Success: 3 ERC-721a NFTs have been minted and mined in block ${receipt.blockNumber} which cost ${receipt.gasUsed} gas \n`
           );
           callback();
       });
};

This script does the following:

  • Pulls deployed contract address and destination wallet from the .env file
  • Uses web3.js to load the existing deployed smart contract
  • Invokes the mint() function of the contract, passing in the wallet address of the recipient and the number of NFTs to mint
  • Several listeners are attached to handle success and error cases
  • A link to the transaction on Etherscan is printed out to the console
  • Then the block number and gas cost is printed out to console


Additional Resources on the ERC-721a specification:

ERC-1155 Multi-Token Standard

ERC-1155 is a fungibility-agnostic standard that can use a single smart contract to represent multiple tokens at once.  This approach can lead to massive gas-savings for projects that require multiple tokens, since one contract can represent all your tokens and also because of batch transfers.

ERC-1155 Characteristics:

  • Can transfer multiple NFTs in a single transaction (safeBatchTransferFrom() function)
  • Contract can represent multiple types of fungibility-agnostic NFTs
  • Support for semi-fungible tokens
  • Generally requires less storage but stores less robust information
  • Multi-language localization of text is possible
  • Get the balances of multiple assets in a single call
  • Allow Batch Approval for an address (setApprovalForAll() function)
  • EIP-165 Support: Declare supported interfaces

Limitations of ERC-1155:

  • Cannot easily query the owner of NFTs on-chain
  • No way to easily enumerate tokens on-chain
  • 3rd-party support generally is not as robust as support for ERC-721 (as of July 2022)
  • Contract state contains ownership data per token ID rather than the token existing or not existing in a wallet
  • Not as easy for off-chain services to determine ownership of a specific NFT

Creating an ERC-1155 Compliant Smart Contract

Basic implementation Example

Check out the source code on Github for Infura1155NFT.sol or review the code locally after cloning the NFT-Standards Github repository.

Infura1155NFT.sol

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

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

contract Infura1155NFT is ERC1155, Ownable {
   uint256 public constant GOLD = 0;
   uint256 public constant RARE_ITEM = 1;
   uint256 public constant EPIC_ITEM = 2;

   constructor() ERC1155("https://raw.githubusercontent.com/anataliocs/NFT-Standards/main/metadata/Infura1155NFT-type{id}.json") {}

   function mint(address recipient) public returns (uint256)
   {
       _mint(recipient, GOLD, 10, "");
       _mint(recipient, RARE_ITEM, 1, "");
       _mint(recipient, EPIC_ITEM, 1, "");

       return 0;
   }

   function contractURI() public view returns (string memory) {
       return "https://raw.githubusercontent.com/anataliocs/NFT-Standards/main/metadata/opensea-contract-1155.json";
   }
}

This basic example extends ERC1155 and Ownable.

This contract has the following features:

  • Extends the OpenZeppelin ERC1155 implementation
  • Defines 3 types of tokens in a single contract
  • Exposes mint() function to mint multiple NFT types
  • Mints multiple tokens in a single transaction
  • Overriding the tokenURI() function helps NFT metadata display correctly on OpenSea
  • Overriding the contractURI() function helps to display storefront level data on OpenSea

Calling the deployed ERC-1155 Compliant Smart Contract

After deploying this contract to testnet or locally, you can invoke the mint() function to mint multiple NFTs with the following script:

mint1155NFT.js

require("dotenv").config();

const {PUBLIC_ADDRESS, CONTRACT_ADDRESS_1155} = process.env;

// Loading the compiled contract Json
const contract1155Json = require("../build/contracts/Infura1155NFT.json");

module.exports = async function (callback) {
   // web3 is injected by Truffle
   const contract1155 = new web3.eth.Contract(
       contract1155Json.abi,
       CONTRACT_ADDRESS_1155 // this is the address generated when running migrate
   );

   // get the current network name to display in the log
   const network = await web3.eth.net.getNetworkType();

   // Generate a transaction to calls the `mint` function
   const tx1155 = contract1155.methods.mint(PUBLIC_ADDRESS);
   // Send the transaction to the network
   await tx1155
       .send({
           from: (await web3.eth.getAccounts())[0], // uses the first account in the HD wallet
           gas: await tx1155.estimateGas(),
       })
       .on("transactionHash", (txhash) => {
           console.log(`Mining ERC-1155 transaction for 2 NFTs and fungible tokens ...`);
           console.log(`https://${network}.etherscan.io/tx/${txhash}`);
       })
       .on("error", function (error) {
           console.error(`An error happened: ${error}`);
           callback();
       })
       .then(function (receipt) {
           // Success, you've minted the NFT. The transaction is now on chain!
           console.log(
               `\n Success: ERC-1155 NFTs and tokens have been minted and mined in block ${receipt.blockNumber} which cost ${receipt.gasUsed} gas \n`
           );
           callback();
       });
};

This script does the following:

  • Pulls deployed contract address and destination wallet from the .env file
  • Uses web3.js to load the existing deployed smart contract
  • Invokes the mint() function of the contract, passing in the wallet address of the recipient which mints and transfers multiple NFTs
  • Several listeners are attached to handle success and error cases
  • A link to the transaction on Etherscan is printed out to the console
  • Then the block number and gas cost is printed out to console

Comparing the Implementations

Let’s stack up the different implementations side-by-side:

ERC-721 ERC-721a ERC-1155
Batch Transfers 🚫
Multiple types of tokens in single contract 🚫 🚫
Easily identify the total supply of NFTs on a network 🚫
Easily query for the owners of a specific asset 🚫
Support for semi-fungible tokens 🚫 🚫
Localization 🚫 🚫


Generally, ERC–1155 implementations are more robust, feature-rich and cheaper to deploy if you have multiple tokens.  ERC-1155 does, however, gain these abilities by making some trade-offs by storing less robust information and sacrificing some ease-of-use for certain functionality.

If you only have a single NFT and will never add another type, the ERC-721 and ERC-721a implementations might be a bit simpler to use and allow you to query collection-wide data more easily but ERC-1155 is generally going to be a better, more future-proof choice.

Workshop:  Deploying and Minting NFTs

Clone the NFT-Standards Github repository.

Follow the instructions in the README to setup your local development environment, your .env file and your Infura project id and secret.

If you don’t have a Infura account yet, sign up here!

Use the following command to deploy all the smart contracts.

truffle migrate --network rinkeby

Mint a Single ERC-721 NFT

Run the following script to mint a single NFT with the ERC-721 contract:

truffle exec scripts/mintNFT.js --network rinkeby

Here is an example transaction of a ERC-721 transaction minting and transferring a single NFTs:  https://rinkeby.etherscan.io/tx/0x671742fd51599c21d49a2d4e37722309e9be34d5df92b69a9107d596b3fe3cc7

Here is an example success message after the ERC-721 NFT has been minted.

Success: The single ERC-721 NFT has been minted and mined in block 11025087 which cost 91869 gas
 If you were to execute this script 3 times to perform a batch mint, the gas cost would be 275607 

Screenshot from Etherscan of transferred ERC-721 token:

Mint a Batch of Three ERC-721a NFTs

Run the following script to mint a single NFT with the ERC-721a contract:

truffle exec scripts/mintBatchNFT.js --network rinkeby

Here is an example transaction of a ERC-721a transaction minting and transferring a batch of 3 NFTs:  https://rinkeby.etherscan.io/tx/0xceff593ef56a4040f0d1a4fe2f7aa8ae4b95165c899895a2dfb5c33a3dd6411f

Here is an example success message after the ERC-721a batch of 3 NFTs has been minted.

Success: 3 ERC-721a NFTs have been minted and mined in block 11025098 which cost 95608 gas 

As you can see, the cost of batch-minting 3 ERC-721a NFTs is about ⅓ the gas cost to mint 3 ERC-721 NFTs in 3 different transactions.

Screenshot from Etherscan of transferred ERC-721a tokens:

Mint a Collection of ERC-1155 NFTs

Run the following script to mint a single NFT with the ERC-1155 contract:

truffle exec scripts/mint1155NFT.js --network rinkeby

Here is an example transaction of a ERC-1155 transaction minting and transferring a collection of NFTs:

https://rinkeby.etherscan.io/tx/0x0216c0eb01b415a70f0e35952787bb27c70971180ced63dc02d207dbe986ca82

Here is an example success message after the ERC-1155 collection of NFTs has been minted.

Success: ERC-1155 NFTs and tokens have been minted and mined in block 11025109 which cost 100951 gas

Screenshot from Etherscan of transferred ERC-1155 tokens:

Interacting with On-Chain NFTs

You can view minted NFTs on testnet at https://testnets.opensea.io/ and validate metadata at https://rinkeby-api.opensea.io/asset/{tokenID}/{chainID}/validate/.

View your Minted ERC-1155 NFTs on Opensea

Navigate to Opensea and sign in with Metamask.  Click “Live on a testnet”, select Rinkeby and enter your deployed contract address for the ERC-1155 Smart Contract.

We can now see storefront contract-level metadata being loaded into Opensea successfully.

We can also see NFT level metadata being loaded into Opensea successfully:  https://testnets.opensea.io/assets/rinkeby/0xc9cf27a09ccf9e1b8a1a9ebaee140e32bf5bc99a/2

Conclusion

There are many options for implementing a NFT smart contract. Which standard to choose is dependent on the requirements of your project.  All three standards allow you to produce NFTs with rich metadata and implement Non-Fungible Tokens for a vast variety of use-cases.  The ERC-1155 standard however generally is a much more flexible and efficient standard in many multi-NFT use-cases where you need to define multiple token types.

Additional Reading

Infura NFT API

Interested in integrating NFTs into your platform while leveraging standard web2 technologies such as REST APIs?  Check out the Infura NFT API here!

Live coding with the NFT API & SDK

NFT Crash Course

Interested in learning more about NFTs in general?  Check out NFT Crash Course, featured at Microsoft Inspire 2022.

Inspired to join the revolutionary world of blockchain? Check out https://consensys.net/careers/ today!

Want to get started on your journey as a Web3 developer? https://infura.io/register

Read more stories like this at: https://blog.infura.io/

Follow Infura: