A NFT (Non-Fungible Token) is a unique token that cannot be replicated unlike fungible tokens like ERC-20, this means NFTs cannot be used for commercial transactions. Usually NFTs represent some kind of digital art or complement real-world items, like digital tickets, the ownership of intellectual property, patterns or sensitive data (e.g. medical records, real estate ownership, music), ensuring the authenticity of a product, tracking (e.g. supply chain) or gaming (e.g. trading of weapons).
ERC-721
A Non-Fungible Token is just another Ethereum standard, the EIP-721.
A NFT contract must implement the following interfaces:
ERC721
. Defines the following events and methods:event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved
function balanceOf(address _owner) external view returns (uint256)
function ownerOf(uint256 _tokenId) external view returns (address)
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable
function transferFrom(address _from, address _to, uint256 _tokenId) external payable
function approve(address _approved, uint256 _tokenId) external payable
function setApprovalForAll(address _operator, bool _approved) external
function getApproved(uint256 _tokenId) external view returns (address)
function isApprovedForAll(address _owner, address _operator) external view returns (bool)
- ERC165: This is just to identify what interface our smart contract is implementing, so we check whether the provided
interfaceID
matches the configured interface ID or not, and returntrue
orfalse
accordingly. In our casesupportsInterface()
should returntrue
when the providedinterfaceID
is0x80ac58cd
.function supportsInterface(bytes4 interfaceID) external view returns (bool)
ERC721TokenReceiver
(mandatory only when usingsafeTransferFrom
). This is an enquiry done by the smart contract caller to know if the ERC-721 implementation is aware of the necessity of implementingERC721
functionalities, so the token is safe and can be transferred out (it doesn’t become forever locked in the contract! 😱), however, this doesn’t mean the previous assurances are true, and only an awareness of the necessity. The receiving contract is therefore responsible to take proper actions upon receiving NFTs.function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4)
Optionally you can also implement the ERC721Metadata
interface to fetch information about the NFT:
function name() external view returns (string _name)
: returns the name of the collection of NFT tokens.function symbol() external view returns (string _symbol)
: returns the abbreviated name of the collection of NFTs.function tokenURI(uint256 _tokenId) external view returns (string)
: returns the asset linked to a particular NFT within the collection of NFTs.
Implementing a Non-Fungible Token
We are going to use the OpenZeppelin ERC721 interface.
Firstly, install OpenZeppelin contracts library:
yarn add --dev @openzeppelin/contracts
The OpenZeppelin implementation provides a set of interfaces and implementation for creating NFTs. We are going to use _safeMint(address to, uint256 tokenId)
to create a new NFT and accepts an address and a token ID.
contracts/MyNFT.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MyNFT is ERC721 {
uint256 private s_tokenCounter; // if you have a collection of tokens on the same smart contract each of them needs their own unique token ID
constructor() ERC721("MyNFT", "MNFT") {
s_tokenCounter = 0;
}
function mintNFT() public returns (uint256) {
_safeMint(msg.sender, s_tokenCounter); // this
s_tokenCounter++; // each time we mint an NFT we increase the token counter
return s_tokenCounter;
}
function getTokenCounter() public view returns (uint256) {
return s_tokenCounter;
}
}
As you can see above every time a new token is created we need to increase the token counter, and this is different to what we had to do for the ERC20 tokens, where we initialize an initial supply of tokens when minting the ERC-20. For the ERC721
we have to mint tokens one by one.
The OpenZeppelin implementation of the ERC721
is storing the token ID and owner address when calling _safeMint()
, and finally it emits a Transfer
event.
The previous definition of the NFT is not linked to a particular asset, so that’s what we are going to do next.
Linking an Asset to an NFT
Uploading NFT Asset
First of all we are going to upload an asset to a NFT storage provider. We are going to use NFT.STORAGE.
There are 3 options for uploading assets to NFT.STORAGE:
- Via the web app. Create an account and upload an asset by clicking on “+Upload”. Now you can click on “Actions” and copy the IPFS (Interplanetary File System) URL or the HTTPS URL.
- Via the NFT.STORAGE app..
- Programmatically! 🤓.
Since this is a coding tutorial let’s go for the programmatic approach.
- Get an API token. Once logged in, go to API Keys and generate a new one.
- Create an environment variable
NFT_STORAGE_KEY=<key>
in your.env
file and paste the API key. - Install the NPM NFT.STORAGE library.
yarn add --dev nft.storage mime
- Create a folder for the image:
mkdir images
and put the image inside the folder. - Create the upload script. NFT.STORAGE docs.
utils/upload.mjs
:
const { NFTStorage, File } = require("nft.storage")
const mime = require("mime")
const fs = require("fs")
const path = require("path")
require("dotenv").config()
const NFT_STORAGE_KEY = process.env.NFT_STORAGE_KEY || "key"
async function storeNFT(imagePath, name, description) {
const image = await fileFromPath(imagePath)
const nftstorage = new NFTStorage({ token: NFT_STORAGE_KEY })
const response = await nftstorage.store({
image,
name,
description,
})
console.log(`NFT image ${response.data.name} uploaded: ${response.url}`)
return response
}
async function fileFromPath(filePath) {
const content = await fs.promises.readFile(filePath)
const type = mime.getType(filePath)
return new File([content], path.basename(filePath), { type })
}
module.exports = { storeNFT }
We will run the previous script as part of the NFT contract deployment script.
Adding the Token URL to Your NFT Contract
Now we can update our NFT smart contract to inject the NFT image URL.
contracts/MyNFT.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
error MyNFT__AlreadyInitialized();
contract MyNFT is ERC721 {
uint256 private s_tokenCounter; // if you have a collection of tokens on the same smart contract each of them needs their own unique token ID
string internal s_tokeUri;
bool private s_initialized;
constructor(string memory tokenUri) ERC721("MyNFT", "MNFT") {
s_tokenCounter = 0;
_initializeContract(tokenUri);
}
function mintNFT() public returns (uint256) {
_safeMint(msg.sender, s_tokenCounter); // this
s_tokenCounter++; // each time we mint an NFT we increase the token counter
return s_tokenCounter;
}
function tokenURI(
uint256 /*tokenId*/
) public view override returns (string memory) {
return s_tokeUri; // using the same image for all minted tokens
}
function _initializeContract(string memory tokenUri) private {
if (s_initialized) {
revert MyNFT__AlreadyInitialized();
}
s_tokeUri = tokenUri;
s_initialized = true;
}
function getTokenCounter() public view returns (uint256) {
return s_tokenCounter;
}
}
Deployment
Now we can write our deployment script to deploy the NFT to your local network, a testnet or mainnet.
deploy/01-deploy-my-nft.js
:
const { network } = require("hardhat")
const {
developmentChains,
VERIFICATION_BLOCK_CONFIRMATIONS,
} = require("../helper-hardhat.config")
const { verify } = require("../utils/verify")
const { storeNFT } = require("../utils/upload")
let tokenUri =
"ipfs://bafkreia6pu2v77h6qdvgssaw5uljatm5hjdl3ec5d24k3isb2bna3nyfkq"
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, log } = deployments
const { deployer } = await getNamedAccounts()
const waitBlockConfirmations = developmentChains.includes(network.name)
? 1
: VERIFICATION_BLOCK_CONFIRMATIONS
log(`----------------------------------------------------`)
if (process.env.UPLOAD_TO_NFT_STORAGE == "true") {
tokenUri = await uploadTokenImage()
}
const arguments = [tokenUri]
const myNFT = await deploy("MyNFT", {
from: deployer,
args: arguments,
log: true,
waitConfirmations: waitBlockConfirmations,
})
// Verify the deployment
if (
!developmentChains.includes(network.name) &&
process.env.ETHERSCAN_API_KEY
) {
log("Verifying...")
await verify(myNFT.address, arguments)
}
log(`----------------------------------------------------`)
}
async function uploadTokenImage() {
const response = await storeNFT(
"./images/smr-logo.png",
"My NFT",
"This is an image for my NFT"
)
return response.url
}
module.exports.tags = ["all", "mynft", "main"]
To deploy to the Goerli testnet and upload the image to NFT.STORAGE:
UPLOAD_TO_NFT_STORAGE=true hh deploy --network goerli
The output should be something like:
----------------------------------------------------
NFT image My NFT uploaded: ipfs://bafyreies6zqyooxmvc7rnpnvhglc2dvkcflboedp6exqhbiavkfmlv5vz4/metadata.json
deploying "MyNFT" (tx: 0x4e33d8634bb9b91bc99834c94f7ca9ed20c70573480c47a4bdcd8d79d8e1f35a)...: deployed at 0xbe529C226d04bF1B44E44BDe175d422902cE8002 with 2248183 gas
Verifying...
Verifying contract...
Nothing to compile
Successfully submitted source code for contract
contracts/MyNFT.sol:MyNFT at 0xbe529C226d04bF1B44E44BDe175d422902cE8002
for verification on the block explorer. Waiting for verification result...
Successfully verified contract MyNFT on Etherscan.
https://goerli.etherscan.io/address/0xbe529C226d04bF1B44E44BDe175d422902cE8002#code
If you go to the Goerli Etherscan link you can the token URI which contains all the metadata associated to the NFT. See the example below:
ipfs://bafyreies6zqyooxmvc7rnpnvhglc2dvkcflboedp6exqhbiavkfmlv5vz4/metadata.json
:
{
"name": "My NFT",
"description": "This is an image for my NFT",
"image": "ipfs://bafybeigxde2t2koxbvj3xojtrmrwk2gxguivpvic7ujot55ptk4z6iefxy/smr-logo.png"
}
Testing
Finally we can create some unit tests to make sure the NFT is working as expected.
test/myNFT.test.js
:
const { expect } = require("chai")
const { network } = require("hardhat")
const { developmentChains } = require("../../helper-hardhat.config")
!developmentChains.includes(network.name)
? describe.skip
: describe("My NFT Unit Tests", () => {
const nftImageUri = "test"
let myNFTFactory
let myNFT
let deployer
beforeEach(async () => {
accounts = await ethers.getSigners()
deployer = accounts[0]
myNFTFactory = await ethers.getContractFactory("MyNFT")
myNFT = await myNFTFactory.deploy(nftImageUri)
await myNFT.deployed()
})
describe("Constructor", () => {
it("has correct name", async () => {
// WHEN
const name = await myNFT.name()
// THEN
expect(name).to.equal("MyNFT")
})
it("has correct symbol", async () => {
// WHEN
const symbol = await myNFT.symbol()
// THEN
expect(await myNFT.symbol()).to.equal("MNFT")
})
it("has counter value zero", async () => {
// WHEN
const tokenCounter = await myNFT.getTokenCounter()
// THEN
expect(tokenCounter.toString()).to.equal("0")
})
})
describe("Mint NFT", async () => {
beforeEach(async () => {
const tokenCounter = await myNFT.getTokenCounter()
expect(tokenCounter.toString()).to.equal("0")
const deployerBalance = await myNFT.balanceOf(
deployer.address
)
expect(deployerBalance.toString()).to.equal("0")
const response = await myNFT.mintNFT()
await response.wait(1)
})
it("bumps token counter", async () => {
// WHEN
const tokenCounter = await myNFT.getTokenCounter()
// THEN
expect(tokenCounter.toString()).to.equal("1")
})
it("increases owner balance", async () => {
// WHEN
const deployerBalance = await myNFT.balanceOf(
deployer.address
)
// THEN
expect(deployerBalance.toString()).to.equal("1")
})
})
describe("Token URI", () => {
it("return IPFS URI", async () => {
// WHEN
const tokeURI = await myNFT.tokenURI(0)
// THEN
expect(tokeURI.toString()).contains(nftImageUri)
})
})
})
Congratulations you were able to create an NFT with a decentralized image 🚀.