Cronos developer series: create your NFT collection with Solidity, Typescript, and IPFS
As requested by many on the Discord server, in this blog post we are going to create and deploy a NFT collection on Cronos.
As requested by many on the Discord server, in this blog post we are going to create and deploy a NFT collection on Cronos.
If you are a developer, there are many ways to create a NFT collection. There is no way that we can cover all the possible languages and storage options in one blog post. Some developer tools even make it possible to abstract the whole process, for example Minty (see here) and NFTport (see here).
Here, we are going to rely on as few dependencies as possible, so that you can understand the all the basics and customize them to your own needs. We have made the following choices:
We use Hardhat to compile the NFT smart contract and to work locally. Alternatively you can use the excellent Truffle suite.
We use custom Typescript scripts to deploy the smart contracts to a public blockchain network and to mint the NFTs. This enables us to fully customize the deployment logic and pause at each step to check that the deployment steps are executing correctly (compared with using the standard deployment scripts of Hardhat and Truffle).
The NFT images and metadata are stored in IPFS, a decentralized storage protocol. IPFS is great for decentralized storage, however it causes some latency when you need to retrieve the data. Alternatively you can use a centralized storage server on AWS or Azure.
Prerequisites
You need to have some knowledge of Javascript / Typescript and have NodeJS installed on your computer. This tutorial uses NodeJS version 16+.
Please clone the following repository to your local machine: repo link. We are going to review each component together.
The repository is mainly based on the standard Hardhat development framework, so please refer to the Hardhat documentation if you’d like to better understand the directory structure. Note that this particular repository has been customized to use Typescript rather than plain Javascript.
In the local directory, you need to create a new .env file, following the format of the .env.example file. For this you need:
A free Infura account with a IPFS project set-up. On the Infura settings page, you will be able to find a IPFS project ID and a secret, that are needed to access the Infura IPFS endpoint.
A wallet account address and the corresponding private key, that is provisioned with CRO on the Cronos testnet and/or mainnet.
Finally, we recommend to install Truffle’s Ganache application in order to run a test blockchain on your computer. Download it here. After installation, you can launch an Ethereum blockchain network on your computer, simply by pressing the quick start button. The local RPC endpoint should be HTTP://127.0.0.1:7545.
Now, in the local repository, install the dependencies with the Terminal command: npm install
.
First, the smart contract
The smart contract is in the /contracts directory and it is called “MyERC721.sol”. Have a look at it.
As Cronos is EVM-compatible, we can use Ethereum NFT smart contract standards. The two main standards are ERC721, where each NFT is unique, and ERC1155, where each NFT exists in a set number of copies. Here, we use ERC721.
The majority of the smart contract code is imported from the excellent OpenZeppelin libraries.
This particular NFT smart contract is set-up so that each token ID is associated with its own URI on IPFS, where anyone can find the metadata associated with this particular Token ID.
For each token ID, the URI of the NFT’s metadata can be retrieved thanks to the following function:
function tokenURI(uint256 tokenId)
public view virtual override returns (string memory)
{
require(
_exists(tokenId),
"ERC721URIStorage: URI query for nonexistent token"
);
return _tokenURIs[tokenId];
}
One key addition to the standard ERC721 is the mintBatchWithURI
function, which makes it possible to mint multiple NFTs to multiple recipients with multiple metadata URIs with a single blockchain transaction:
function mintBatchWithURI(address[] memory recipients, string[] memory _tokenURIList)
public virtual onlyController
{
for (uint256 i = 0; i < recipients.length; i++) {
require(
recipients[i] != address(0),
"Must not mint to the zero address"
);
_mint(recipients[i], _tokenIdTracker.current());
_setTokenURI(_tokenIdTracker.current(), _tokenURIList[i]);
_tokenIdTracker.increment();
}
}
Depending on your needs, feel free to further customize the smart contract, particularly if you need to set-up an allow-list and a purchase functionality for the primary NFT sale. There are numerous Solidity smart contract repositories online, to borrow ideas from.
If you make any significant change to the standard smart contracts provided by OpenZeppelin, please remember to develop tests with 100% coverage before deploying to production. This tutorial is not provided with automated tests as it is purely for informational purposes.
Now, in a Terminal window in the project root’s directory, you can compile the smart contract with:
npx hardhat compile
The compiled version of the smart contract can be found under ./artifacts/contracts/MyERC721.sol/MyERC721.json
. This JSON file contains the ABI and the bytecode that you will need if you build back-end applications that interact with this smart contract.
You can now run a script to deploy the smart contract to your local Ganache blockchain with:
npx hardhat run --network ganache scripts/deploy-NFT-dev.script.ts
This basic script is uses Hardhat to compile and deploy the smart contract , and mint one NFT, according to the Ganache configuration specified in the hardhat.config.ts
file. Yay! You can play around with the smart contract.
But we are not done yet
As you may or may not know, when it comes to NFTs the smart contract is only 25% of the work. We need to:
Create the image associated with each NFT, and upload it to IPFS.
Create the metadata associated with each NFT, and upload it to IPFS.
Create an end-to-end script that is going to repeat these operations for each NFT and then mint the NFTs on the blockchain, while storing the URI of each NFT metadata (the tokenURI) in the NFT smart contract.
That magic is happening in ./scripts/deploy-NFT-full.ts
. In contrast with the previous script, this script does not use the Hardhat framework. While it is possible to create the end-to-end deployment script within the Hardhat framework, in this post we create Typescript code directly in NodeJS so that we can easily re-use the code in back-end or front-end applications later.
Additionally, the custom code is designed to pause at each deployment step, so that you can monitor and check what is happening and, even more importantly, wait for each step to complete successfully before you launch the next step — failure to do so is a frequent source of errors.
Overview of the deployment script
Let’s look at./scripts/deploy-NFT-full.ts
.
At the top of the script you can find the network configuration parameters. Uncomment the relevant code section depending on whether you want to deploy to Cronos testnet or Cronos mainnet:
// Cronos testnet
const network = {
url: "https://cronos-testnet-3.crypto.org:8545",
gasPriceGWei: 5000,
blockExplorerPrefix: "https://cronos.crypto.org/explorer/testnet3/tx/",
};
// Cronos mainnet
// const network = {
// url: "https://evm-cronos.crypto.org/",
// gasPriceGWei: 5000,
// blockExplorerPrefix: "https://cronoscan.com/tx/",
// };
The script deploys the NFT smart contract to the chosen network and mints 3 NFTs, with Token IDs 0, 1, and 2.
The main()
function lists the deployment steps and calls various sub routines:
uploadContractLevelMetadata
creates the contract-level metadata according to OpenSea specifications (here) and uploads it to IPFS, then retrieves the corresponding CID (content ID) from IPFS.uploadNFTtoIPFS
creates the 3 NFT-level metadata according to OpenSea specifications (here) and uploads the 3 sets of images and metadata to IPFS, then retrieves the corresponding CIDs (content IDs) from IPFS. This step uses thegetNFTData
function which simply returns NFT data and metadata that has been entered manually for this tutorial.checkTokenCIDs
is simply a check sub routine. It reads all the CIDs generated by the previous steps and displays their content in the Terminal for checking. It also downloads the images from IPFS and stores them in./nft-items/downloads
so that you can check that the images have been properly stored in IPFS.deploy
deploys the ERC721 smart contract to the chosen network, and stores the contract-level metadata CID.mint
mints the 3 NFTs and associates them with the corresponding CIDs.read
reads the smart contract directly from the blockchain, and displays some key information like the name, symbol, contract URI, address of contract owner, owner of Token 0, and URI of token 0, to check that the deployment has been completed successfully.
You can run the deployment script in the Terminal in the project’s root directory by typing:
npx ts-node ./scripts/deploy-NFT-full.ts
After each step, the script prompts you to press “Enter” to continue. When it comes to the steps that involve a blockchain transaction (deploy and mint), please pay particular attention to the prompts that require you to copy and paste the URL of the transaction in the Cronos block explorer. You should only trigger next steps once the block explorer shows “Success” in green on the transaction page, like so:
Let’s look at a few code snippets.
Uploading stuff to IPFS
Let’s look at how we upload NFT images and metadata. This is done in the uploadNFTtoIPFS
function.
This part of the code loads the NFT image from the ./nft-items/images
directory, uploads the image to IPFS, retrieves the image CID, and pins the image in your Infura account to make it more persistent.
This part of the code updates the NFT metadata so that it includes a link to the image, uploads the metadata JSON object to IPFS, retrieves the metadata CID, and pins the stored object in your Infura account to make it more persistent.
One thing that can be a little confusing with IPFS is that the URI format for an object depends on what you need to do with it:
In the way that things are done here, the CID alphanumeric string refers to a folder rather than a stored file. The actual file is stored at [cid]/[filename], for example QmVQCBtZTBPpVw5ncwUERcPrqbxEW1CeADSXCN7GJxNFTo/i0.png or QmPczVESmbJUtsoinVMh135A4J5Zy2yD2z1JzDXjngGZcT/i0.json. This happens when we put a /[anything]/ path in the path field of the IPFS ‘add’ function. The only reason why we are doing this, is to make URIs more human-readable.
The image URI is stored with the prefix `https://ipfs.io/ipfs/` prefix so that it is directly accessible by NFT wallets and marketplaces via a HTTP URL. The ipfs.io domain refers to a IPFS gateway operated by IPFS. That’s why the NFT metadata will show an image URI that looks like ‘https://ipfs.io/ipfs/QmVQCBtZTBPpVw5ncwUERcPrqbxEW1CeADSXCN7GJxNFTo/i0.png'
The NFT metadata is stored with the prefix ‘ipfs://’ in the smart contract, to indicate to NFT wallets and marketplaces that it is a IPFS URI. So the Token URI will look like ‘ipfs://QmPczVESmbJUtsoinVMh135A4J5Zy2yD2z1JzDXjngGZcT/i0.json’
Deploying and minting on chain
Let’s look at how we interact with the chain. This is done in the deploy
and mint
functions.
The functions leverage the ethers.js library directly, a convenient and well-documented alternative to the web3.js library for Typescript developers.
Please refer to the ethers.js documentation for more details. The workflow here is quite standard for Ethereum developers. It involves the creation of a Web3 provider (called ethersProvider in the code) and the connection of that provider to a specific smart contract and a specific signer, before calling deployment and minting methods.
For example the deployment code is:
And the minting code is:
What is important here, is to make sure that we wait for the contract to be deployed before starting minting. This is enabled by the main()
script which prompts you to look at the Cronos block explorer before you trigger the next deployment step, as there is no error-free programmatic way to be 100% sure that asynchronous steps are completed in the right order.
That’s it!
By executing the deploy-NFT-full.ts
script, you can complete the end-to-end deployment of a NFT collection on Cronos.