Welcome to the Complete Guide on How to Create and Deploy an NFT Smart Contract on RSK.
Introduction
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!
We will do the following in this tutorial.
- A brief introduction to NFTs, and ERC-721
- Setting up your environment
- What is IPFS?
- How to set up your Pinata account
- Create NFT metadata (JSON File)
- Create ERC 721 smart contract
- Deploy smart contract on the RSK blockchain
Prerequisites
To follow this tutorial, you should have knowledge in:
- Smart contract fundamentals using Solidity
- JavaScript
- Command line
- Git
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.
What is a Non-fungible Token (NFT)?
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.
NFTs can be used in the following use cases:
- Medical Records and Identity Verification - NFT ledgers can store an individual's medical records without compromising confidentiality or risking tampering from external sources
- Real Estate - NFTs could be used to transfer land deeds, provide proof of ownership and even keep track of changes in property value over time using timestamped NFTs.
- Ensuring Authenticity of Products - NFTs can be used to ensure that the product you are purchasing is authentic. NFTs can also be used to store information about the manufacturing process, ensuring that everything is fair trade
- Academic Credentials - NFTs are also a good way to represent academic credentials. NFTs can provide proof of attendance, degree earned, and other important information which will be stored on the NFT chain that cannot be altered or hacked into.
- Gaming Industry - NFTs can be integrated into the gaming world by allowing NFT cross-platform playability.
What is ERC-721?
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, getting the current token balance of an account, getting 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.
**Setting up your Development Environment
What is Hardhat?**
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.
What is ethers.js?
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.
What is tRBTC (Smart Bitcoin in Testnet)?
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.
What is IPFS?
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.
Configure the metadata for your NFT using Pinata
To get our tokenURI parameter, which 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:
- Navigate to the "Files" page and click the blue "Upload" button at the top-left of the page.
- Upload two cat images to Pinata - this will be the image asset for your NFTs. Feel free to name the asset whatever you wish
- After you upload, you'll see the file info in the table on the "Files" page. You'll also see a CID column. You can copy the CID by clicking the copy button next to it. You can view your upload at: https://gateway.pinata.cloud/ipfs/.
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 named 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.
Generate a seed phrase
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 Mainnet- the usage here is sufficient for use on RSK Testnet only.
Hardhat Configuration
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 the deploy 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.
Complete Config File
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",
},
},
},
};
Create your Smart Contract
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 npm install @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 three OpenZeppelin 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 NFT
- string 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.
How to deploy your NFT to RSK
Steps
we will do the following steps;
- Switch to Node.js 12 for Hardhat to work nvm use 12
- compile Meow-NFT smart contract npx hardhat compile
- deploy Meow-NFT smart contract to RSK testnet. See other possible networks in hardhat.config.js 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!
- mint your NFTs from CIDs specified in the mint task of project.config.js
npx hardhat mint --network rsktestnet
View NFT using Metamask
Metamask
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 metamask.io and install it.
Create an account.
Write down your 12 word seed phrase. This is used to recover your account, in case you lose your password.
The seed phrase is the most important thing in a wallet / account!
Connect MetaMask to RSK testnet
Go to networks -> Custom RPC, and enter the following values:
- Network Name RSK Testnet
- New RPC URL https://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!
Add NFT to Metamask
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
Visit Our Developers Portal
This article was originally posted on the RSK Developer Portal.
Top comments (0)