Welcome to the Complete Guide on How to Create and Deploy an NFT Smart Contract on RSK.
In this tutorial, we will take you through a step by step process on how to write a ERC-721 smart contract, and deploy to the RSK testnet using the following tools: Solidity, Hardhat, Ether.js, RSK Testnet, IPFS, Pinata, and Test RBTC from a faucet. Not to worry, if these terms are new to you — we will explain them!
To follow this tutorial, you should have knowledge in:
If you are not familiar with the above, it will be advisable to learn the basics by clicking the links above, before proceeding with this tutorial on how to create and deploy your NFT project to the RSK Testnet.
Fungibility, is about whether something is interchangeable and divisible.
A fungible token has both of those properties, and behave in a manner similar to fiat cash;
whereas a non-fungible token is neither interchangeable nor divisible, and behave in a manner similar to many real world objects.
ERC-20 is the most commonly used technical standard for fungible tokens (FTs); and ERC-721 is the most commonly used technical standard for non-fungible tokens (NFTs).
Non-fungible is a term used to describe digital assets that represent real world objects like art, furniture, a song file, in-game items, your computer or even real estate. Unlike ERC-20 tokens, tokens that are created under ERC-721 are not interchangeable, this comes from the fact that while two NFTs may look identical to each other, they both hold unique information. Non-fungible tokens also lack another feature of their ERC-20 counterparts – they are not divisible, which means that you cannot own a portion of an NFT.
ERC-721 is a standard for representing ownership of non-fungible tokens, where each token is unique. It provides functionalities like to transfer tokens from one account to another, to get the current token balance of an account, to get the owner of a specific token and also the total supply of the token available on the network.
EIP-721 is a standard interface for non-fungible tokens.
Hardhat is a development environment that enables you to compile, deploy, test, and debug your RSK software. It helps to manage and automate the recurring tasks that are inherent to the process of building Blockchain applications.
Check out this step-by-step tutorial on how to set up a Hardhat project to connect to the RSK Testnet.
Ethers.js is a JavaScript library that allows developers to interact with the blockchain. The library includes utility functions in JavaScript and TypeScript, and can also support wallets.
The Test Smart Bitcoin (tRBTC) is the token used to pay for the execution of transactions in the RSK Testnet environment.
Watch this video tutorial on How to Get tRBTC from the RSK Testnet Faucet.
IPFS (Interplanetary File System) is a file system/protocol for storing and sharing content, it allows you to store files, keeping track of them on a distributed network. This storage system allows direct interaction through a secure and global P2P network.
IPFS files are content-addressable. This means that it uses content identifiers, or CIDs, as a label to point to material in IPFS. These CIDs are based on the contents of the files themselves, and may be thought of as hashes. It doesn’t indicate where the content is stored, but it forms a kind of address based on the content itself. This property makes IPFS a suitable platform for referencing images within smart contracts. We will use IPFS to host/store our NFT images.
To get our tokenURI
parameter, that should resolve to a JSON document describing our NFT’s metadata which will include properties such as name, description, image, and other attributes, we would need to set up Pinata, a convenient IPFS API and toolkit, to store our NFT asset and metadata.
To simply put, a tokenURI
on an NFT is a unique identifier of what the token “looks” like. A URI could be an API call over HTTPS, an IPFS hash, or anything else that is unique.
If you don’t have a Pinata account, sign up for a free account here and complete the steps to verify your email.
Once you’ve created an account:
https://gateway.pinata.cloud/ipfs/<CID>.
Now, let’s create and upload two other files to pinata, each containing details of two cats in JSON format.
In your root directory, make a new folder called nft- metadata and add the following json codes:
File one is named doerak.json
{
"attributes": [
{
"trait_type": "Breed",
"value": "European short hair"
},
{
"trait_type": "Parent",
"value": "Gino Osahon"
}
],
"description": "Doerak. Gray & white kitty",
"image": "ipfs://QmX7P1aswXLKLPd7RqbtwrNGD9CzGjvNFKmdEhWtBmoyiS",
"name": "Doerak"
}
File two is called luna.json
{
"attributes": [
{
"trait_type": "Breed",
"value": "European short hair"
},
{
"trait_type": "Parent",
"value": "Alex Shenshin"
}
],
"description": "Luna. Ginger kitty",
"image": "ipfs://QmZZfJcrppaRiq5dWtC6zpnGZRNoVwdyh69VoA89xGW5Yt",
"name": "Luna"
}
Feel free to change the data in the json. You can remove or add to the attributes section. Most importantly, make sure the image field points to the location of your IPFS image.
Once you’re done editing the JSON file, save it and upload it to Pinata, following the same steps we did for uploading the image.
In your root directory, create a file called secret.json
and add your seed phrase.
Seed phrases are a human-readable version of your private keys. You can sign transactions and recover lost accounts using part of your mnemonic phrase. Mnemonic or seed phrases can range from 12 - 24 words depending on the blockchain ecosystem you are dealing with. Any app can generate its mnemonic phrase for security purposes.
{
"mnemonic": "please put your twelve words long mnemonic phrase to this string now"
}
To interact with the RSK blockchain, you need an account, which consists of a private key, a public key, and an address.
BIP-39 is a technical standard that allows the generation of multiple accounts from a set of dictionary words, plus a derivation path.
Many software libraries and wallet software implement this technical standard, including both ethers.js and MetaMask, which we’ll be using in this tutorial.
Note that seed phrases should be treated as securely as private keys, so do not use the one in this tutorial on RSK Mainner - the usage here is sufficient for use on RSK Testnet only.
Let’s explain the hardhat.config.js
file, the configuration file is always executed on startup before anything else happens, and it has two main tasks. You can define a task as a JavaScript async function with some associated metadata.
This metadata is used by Hardhat to automate tasks, or as asynchronous JavaScript functions that get access to the Hardhat Runtime Environment, which exposes its configuration and parameters.
task('deploy', 'Deploys smart contract to a blockchain').setAction(async () => {
const meowContractFactory = await ethers.getContractFactory('Meow');
const meowNft = await meowContractFactory.deploy();
await meowNft.deployed();
console.log(
`Meow NFT deployed to: ${meowNft.address}\nCopy this address and paste to the 'mint' task in 'hardhat.config.js'`,
);
});
When thedeploy
task is called, hardhat will deploy your smart contract to the RSK Blockchain. The function takes two metadata, and the second line const
meowContractFactory = await ethers.getContractFactory('Meow');
gets a contractFactory
of the compiled source code.
ContractFactory
in ethers.js
is an abstraction used to deploy new smart contracts, so Meow
here is a factory for instances of our Meow contract.
The line const meowNft = await meowContractFactory.deploy();
sends a deploy transaction, and the next line await meowNft.deployed();
waits for the transaction to be mined. The last line in the deploy task uses console.log
print logging message that says NFT has been deployed to the contract variable address
.
task('mint', 'Mint new NFT collectibles').setAction(async () => {
const deployedAddress = '0xE360F4BFb74A1B2B1d102f40BE6c7D0f5C9d12C8';
const newCIDsToMint = [
'QmaXZxVGYcCY36seYTVuGeY9mWchC1WjMscV1FLNfZsM3f',
'QmR5mspowKw6B68QPSuYE9SGH1A6gPKxjdVRokhAZZh4LD',
];
const api = (await ethers.getContractFactory('Meow')).interface;
const [signer] = await ethers.getSigners();
const meowNft = new ethers.Contract(deployedAddress, api, signer);
async function mintSequentially() {
const cid = newCIDsToMint.shift();
if (cid) {
const tx = await meowNft.mintNFT(signer.address, `ipfs://${cid}`);
const receipt = await tx.wait();
const { tokenId } = receipt.events[0].args;
console.log(`Minted NFT ${deployedAddress} #${tokenId}`);
await mintSequentially();
}
}
await mintSequentially();
});
Task mint
when called will mint a new NFT. The line const deployedAddress = '0xE360F4BFb74A1B2B1d102f40BE6c7D0f5C9d12C8';
takes the address where the smart contract source code was deployed to in the deploy
task. This means that you need to first run the deploy task, then copy the address of the deployed source code and then assign it here to the deployedAddress constant.
The below lines contain the IPFS content identifier (CIDs) obtained from Pinata.
const newCIDsToMint = [
'QmaXZxVGYcCY36seYTVuGeY9mWchC1WjMscV1FLNfZsM3f',
'QmR5mspowKw6B68QPSuYE9SGH1A6gPKxjdVRokhAZZh4LD',
];
The lines below get the smart contract application programming interface, get the deployers account information, and instantiates the smart contract representation object.
const api = (await ethers.getContractFactory('Meow')).interface;
const [signer] = await ethers.getSigners();
const meowNft = new ethers.Contract(deployedAddress, api, signer);
The below mintSequentially
function mints all items from the newCIDsToMint
array one after another. The statement const cid = newCIDsToMint.shift();
removes the first CID from the newCIDsToMint
array, if the array is already empty (minted all items). The if
statement is called, if there are still items to be minted, it calls the smart contracts mintNFT
function thereby initiating a transaction. It then waits for the transaction to be mined, gets the transaction receipt, extracts the ID of the newly minted NFT from the transfer event emitted by the smart contract, and recursively calls itself until the newCIDsToMint
array is empty.
async function mintSequentially() {
const cid = newCIDsToMint.shift();
if (cid) {
const tx = await meowNft.mintNFT(signer.address, `ipfs://${cid}`);
const receipt = await tx.wait();
const { tokenId } = receipt.events[0].args;
console.log(`Minted NFT ${deployedAddress} #${tokenId}`);
await mintSequentially();
}
}
The module.export section has already been explained in the JSON-RPC base networks section of the "How to set up a Hardhat project for RSK Testnet " tutorial referred to in this article.
Your final configuration file should now look like this:
/* eslint-disable no-undef */
require('@nomiclabs/hardhat-waffle');
const { mnemonic } = require('./.secret.json');
task('deploy', 'Deploys smart contract to a blockchain').setAction(async () => {
const meowContractFactory = await ethers.getContractFactory('Meow');
const meowNft = await meowContractFactory.deploy();
await meowNft.deployed();
console.log(
`Meow NFT deployed to: ${meowNft.address}\nCopy this address and paste to the 'mint' task in 'hardhat.config.js'`,
);
});
task('mint', 'Mint new NFT collectibles').setAction(async () => {
const deployedAddress = '0xE360F4BFb74A1B2B1d102f40BE6c7D0f5C9d12C8';
const newCIDsToMint = [
'QmaXZxVGYcCY36seYTVuGeY9mWchC1WjMscV1FLNfZsM3f',
'QmR5mspowKw6B68QPSuYE9SGH1A6gPKxjdVRokhAZZh4LD',
];
const api = (await ethers.getContractFactory('Meow')).interface;
const [signer] = await ethers.getSigners();
const meowNft = new ethers.Contract(deployedAddress, api, signer);
async function mintSequentially() {
const cid = newCIDsToMint.shift();
if (cid) {
const tx = await meowNft.mintNFT(signer.address, `ipfs://${cid}`);
const receipt = await tx.wait();
const { tokenId } = receipt.events[0].args;
console.log(`Minted NFT ${deployedAddress} #${tokenId}`);
await mintSequentially();
}
}
await mintSequentially();
});
module.exports = {
solidity: '0.8.12',
defaultNetwork: 'rsktestnet',
networks: {
hardhat: {},
rsktestnet: {
chainId: 31,
url: 'https://public-node.testnet.rsk.co/',
accounts: {
mnemonic,
path: "m/44'/60'/0'/0",
},
},
},
};
In your root directory, start by creating a new directory called contracts and create a file inside the directory called Meow.sol
.
mkdir contracts
touch contracts/Meow.sol
code contracts/Meow.sol
Below is our NFT smart contract code, which is based on the OpenZeppelin library’s ERC-721 implementation. Copy and paste the contents below into your Meow.sol
file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// using OpenZeppelin libraries
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract Meow is ERC721URIStorage, Ownable {
/**
From the `Counters` docs:
Provides counters that can only be incremented, decremented or reset.
This can be used e.g. to track the number
of elements in a mapping, issuing ERC721 ids, or counting request ids.
Include with `using Counters for Counters.Counter;`
*/
using Counters for Counters.Counter;
// tracks the number of minted NFTs
Counters.Counter private _tokenIds;
// calling ERC721 constructor
constructor() ERC721("Meow NFT", "MEO") {}
// mints new NFTs. Can be called only by the deployer (s/c owner)
function mintNFT(address recipient, string memory tokenURI)
public onlyOwner
returns (uint256)
{
// increment counter
_tokenIds.increment();
// get new NFT id
uint256 newItemId = _tokenIds.current();
// call internal ERC721 mint function
_mint(recipient, newItemId);
// write token URI to newly minted NFT
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
We are inheriting classes from the OpenZeppelin contracts library, in the command line run npminstall @openzeppelin/contracts
to install the library into our folder.
So, what does this code do exactly? Let’s break it down, line-by-line.
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
At the top of our smart contract, we import threeOpenZeppelin smart contract classes and one extension:
@openzeppelin/contracts/token/ERC721/ERC721.sol
contains the implementation of the ERC-721 standard, which our NFT smart contract will inherit. To be a valid NFT, your smart contract must implement all the methods of the ERC-721 standard. To learn more about the inherited ERC-721 functions, see the full ERC-721 specification.
@openzeppelin/contracts/utils/Counters.sol
provides counters that can only be incremented or decremented by one. Our smart contract uses a counter to keep track of the total number of NFTs minted and set the unique ID on our new NFT. (Each NFT minted using a smart contract must be assigned a unique ID—here our unique ID is just determined by the total number of NFTs in existence. For example, the first NFT we mint with our smart contract has an ID of 1, our second NFT has an ID of 2, etc.)
@openzeppelin/contracts/access/Ownable.sol
sets up access control on our smart contract, so only the owner of the smart contract (you) can mint NFTs. Note that including access control is entirely a preference. If you’d like anyone to be able to mint an NFT using your smart contract, remove the word Ownable
on line 10 and onlyOwner
on line 17.
@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol
- This is a more flexible but more expensive way of storing metadata.
using Counters for Counters.Counter;
// tracks the number of minted NFTs
Counters.Counter private _tokenIds;
// calling ERC721 constructor
constructor() ERC721("Meow NFT", "MEO") {}
After our import statements, we have our custom NFT smart contract, which is surprisingly short — it only contains a counter, a constructor, and single function! This is thanks to our inherited OpenZeppelin contracts, which implement most of the methods we need to create an NFT, such as ownerOf
which returns the owner of the NFT, and transferFrom
, which transfers ownership of the NFT from one account to another.
In our ERC-721 constructor, you’ll notice we passed 2 strings, Meow-NFT
, and MEO
. The first variable is the smart contract’s name, and the second is its symbol. You can name each of these variables whatever you wish!
// mints new NFTs. Can be called only by the deployer (s/c owner)
function mintNFT(address recipient, string memory tokenURI)
public onlyOwner
returns (uint256)
{
// increment counter
_tokenIds.increment();
// get new NFT id
uint256 newItemId = _tokenIds.current();
// call internal ERC721 mint function
_mint(recipient, newItemId);
// write token URI to newly minted NFT
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
Finally, we have our function mintNFT(address recipient, string memory tokenURI)
that allows us to mint an NFT! You’ll notice this function takes in two variables:
address recipient
specifies the address that will receive your freshly minted NFTstring memory tokenURI
is a string that should resolve to a JSON document that describes the NFT’s metadata. An NFT’s metadata is really what brings it to life, allowing it to have configurable properties, such as a name, description, image, and other attributes.
mintNFT
calls some methods from the inherited ERC-721 library, and ultimately returns a number that represents the ID of the freshly minted NFT.
We will do the following steps:
nvm use 12
npx hardhat compile
npx hardhat deploy --network rsktestnet
You will see a message:
Meow NFT deployed to: 0xE360F4BFb74A1B2B1d102f40BE6c7D0f5C9d12C8 Copy this address and paste to the 'mint' task in 'hardhat.config.js'
Paste the address to the mint task!
npx hardhat mint --network rsktestnet
Metamask is a kind of web wallet which facilitates transactions using yours accounts. It can be used with RSK networks too. It has versions for several browsers, like Chrome, Firefox, Opera and Brave.
Go to networks -> Custom RPC, and enter the following values:
Network NameRSK Testnet
New RPC URLhttps://public-node.testnet.rsk.co
ChainID (optional)31
Symbol (optional)tRBTC
Block Explorer URL (optional)https://explorer.testnet.rsk.co
After configuring it, select the RSK Testnet.
MetaMask screenshot before adding NFT collection.
You should now see an account connected to the RSK Testnet.
Not to worry if you see “No NFTs yet” under the NFTs tab,there is one more step before we’ll be able to see them!
Once you’re on the RSK network, select the “NFTs” tab on the right and add the NFT smart contract address and the ERC-721 token ID of your NFT — which you should be able to find on RSK Testnet Explorer based on the transaction hash from your NFT deployed to RSK Testnet.
You may need to refresh the page to see your newly minted NFT.
MetaMask screenshot after adding NFT collection
Congratulations! In this article, we learnt about NFTs, IPFS, Hardhat, and we have successfully created and deployed our NFT project to the RSK Testnet.
If you would like to delve deeper, here are some resources and tools that we recommend.
Resources