We are going to build on-chain NFTs, that have different Tiers and prices.
In the previous lesson, we talked about NFTs and their use cases. If we can use an NFT to login to a web page, or to access a service, we can use them to differentiate between different levels or categories in that service. Think of some streaming services out there such as Netflix, Disney+, etc. Some have different levels of access depending on the subscription. We want to let users access different services depending on the NFT they mint and own.
Before we start coding, we need to create our project template. We are going to follow the same steps as in previous lessons. Using our package manager (npm, yarn, etc) we create a Hardhat project and remove unnecessary files.
If you’ve done our previous lesson, it’s the exact same process. Make a note of remembering these steps and what they do, for we will use them a lot in the future.
Let’s first open a console and cd into our d_d_academy
folder, or create it
first if you don't have it. Then let's create a folder for our NFT project:
## (OPTIONAL) create a folder for our D_D Academy projects mkdir d_d_academy cd d_d_academy ## create a folder for this project mkdir tierNFT cd tierNFT ## initialize our folder as an npm package npm init -y ## install hardhat (and its dependencies) npm install --save-dev hardhat ## create a Hardhat project npx hardhat
The --save-dev
flag used in the last command, lets the project know it's a
development dependency (not needed in production). You can view what
dependencies are needed in the package.json
file in the root of the project.
Choose Create a Javascript project
and hardhat will create an example
project for us. It will give us 3 prompts for options. Choosing the defaults is
ok for us. Here's what mine asked me:
✔ What do you want to do? · Create a JavaScript project ✔ Hardhat project root: · ~/d_d_academy/tierNFT ✔ Do you want to add a .gitignore? (Y/n) · y ✔ Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)? (Y/n) · y
The project asked us to install @nomicfoundation/hardhat-toolbox
in the
last prompt. If they didn’t install or we accidentally chose ‘n’, we can always
install them manually with:
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-chai-matchers
We delete some files so we start fresh:
rm contracts/*.sol rm scripts/*.js rm test/*.js
In a Hardhat project, the default folders are supposed to be for:
contracts/
is where the source files for your contracts should be.scripts/
is where simple automation scripts go.test/
is where your tests should go.We now need to add our last dependency (OpenZeppelin contracts):
npm install @openzeppelin/contracts
Open Zeppelin developed a lot of standard contracts that are super powerful, widely used and fully tested and audited.
Fire up your code editor and let’s start hacking. I’m using VSCode, so I run
code .
in my terminal.
Let’s create an empty file named TierNFT.sol
inside the contracts/
folder
and copy this code inside:
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; contract TierNFT { }
Now that we have our License, the version of solidity we are using and the contract set up, we can add the logic and the variables we need to store.
As we are creating tiers for the categories of our NFTs, we need to also store information about tiers in our contracts.
We'll write our smart contract step by step in four stages.
Let’s get started with a simple contract, inheriting OpenZeppelin ERC721 like we did last time.
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract TierNFT is ERC721 { constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} }
We add a constructor to our contract, which will mirror the one inherited from ERC721.sol. If we had no constructor, the compiler would throw errors.
Now let’s go ahead and add a mint function that only uses _safeMint
i don't understand 'only uses'....as opposed to what?'
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import '@openzeppelin/contracts/token/ERC721/ERC721.sol' contract TierNFT is ERC721 { uint256 public totalSupply; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public payable { totalSupply++; _safeMint(msg.sender, totalSupply); } }
Now add the tiers and assign their values, where each one represents a service subscription.
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; string constant TIER_NAME_0 = "Basic"; string constant TIER_NAME_1 = "Medium"; string constant TIER_NAME_2 = "Premium"; uint256 constant TIER_VALUE_0 = 0.01 ether; uint256 constant TIER_VALUE_1 = 0.02 ether; uint256 constant TIER_VALUE_2 = 0.05 ether; contract TierNFT is ERC721 { uint256 public totalSupply; mapping(uint256 => uint256) public tokenTier; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public payable { totalSupply++; _safeMint(msg.sender, totalSupply); } // We will add more code here }
We have added Basic
, Medium
and Premium
as tiers and assigned their values. We store what tier each NFT holds in mapping(uint256 => uint256) public tokenTier;
.
Now we need to modify the mint function for the tier NFTs:
// constructor part of the code... function mint() public payable { require( msg.value >= TIER_VALUE_0, "Not enough value for the minimum Tier" ); uint256 tierId = 0; if (msg.value >= TIER_VALUE_2) tierId = 2; else if (msg.value >= TIER_VALUE_1) tierId = 1; totalSupply++; _safeMint(msg.sender, totalSupply); tokenTier[totalSupply] = tierId; } // We will add more code here }
The mint function selects tiers based on the amount of native token value it receives. If it doesn't get enough for tier 0,it will give a message telling us, "Not enough value for the minimum Tier". Otherwise we can select the tier we want..... as long as we can afford it!
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import '@openzeppelin/contracts/token/ERC721/ERC721.sol' string constant TIER_NAME_0 = "Basic"; string constant TIER_NAME_1 = "Medium"; string constant TIER_NAME_2 = "Premium"; uint256 constant TIER_VALUE_0 = 0.01 ether; uint256 constant TIER_VALUE_1 = 0.02 ether; uint256 constant TIER_VALUE_2 = 0.05 ether; contract TierNFT is ERC721 { uint256 public totalSupply; mapping(uint256 => uint256) public tokenTier; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public payable { require( msg.value >= TIER_VALUE_0, "Not enough value for the minimum Tier" ); uint256 tierId = 0; if (msg.value >= TIER_VALUE_2) tierId = 2; else if (msg.value >= TIER_VALUE_1) tierId = 1; totalSupply++; _safeMint(msg.sender, totalSupply); tokenTier[totalSupply] = tierId; } }
When we inherited Open Zeppelin's ERC721, it gave us a function for tokenURI
where we can store an image, or a video, or much more. With the help of this ERC721 contract we have the ability to define a base path for creating an
unique URI which adds the token ID to the end of it.
// Place this under the other imports at the top: import "@openzeppelin/contracts/utils/Base64.sol"; import "@openzeppelin/contracts/utils/Strings.sol";
Next, we import Base64.sol
which encodes the tokenURI so it can return a JSON file needed for the tier NFTs.
Remember how we talked about this token ID at the end of the URI? Strings.sol
will write it as a string inside the JSON file. Go ahead and import the magic of these two files to your contract.
For this lesson we won’t be creating a separate JSON file. We will actually code it into the contract.
// mint function part of the code... // Create the tokenURI json here, instead of creating files individually function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_exists(tokenId), "Nonexistent token"); string memory imageSVG = "PLACEHOLDER FOR SVG IMAGE"; string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "', name(), " #", Strings.toString(tokenId), '", "description": "TierNFTs collection",' '"image": "data:image/svg+xml;base64,',Base64.encode(bytes(imageSVG)), '"}' ) ) ) ); return string(abi.encodePacked("data:application/json;base64,", json)); } // We will add more code here }
Let’s stop to break it down and examine it a little.
tokenURI
function, you'll notice override
, an ERC721
function we'll use, since we are not creating a separate JSON file to store images or other services, but creating it right here in the contract.require(_exists(tokenId). "Nonexistent token");
. According to ERC721 specification, it is required to throw an error if the NFT doesn't exist.imageSVG
is a placeholder for our image, and we will deal with it a bit later.Base64.encode
is for encoding the JSON into Base64 so browsers can translate
it into a file much in the same way as a file attached to an email.string( abi.encodePacked () )
concatenates the string in a similar way to our previous lessons.This is the JSON format of our metadata:
'{"name": "', name(), " #", Strings.toString(tokenId), '", "description": "TierNFTs collection",' '"image": "data:image/svg+xml;base64,',Base64.encode(bytes(imageSVG)), '"}'
The part data:image/svg+xml;base64
tells the browser that the code after the comma is a string of text written in Base64, so the browser
can decode it back into our SVG file format. For example, if our
collection TierNFT
, and the TokenID were 3
, our JSON would end up look
something like this:
{ "name": "TierNFT #3", "description": "TierNFTs collection",' "image": "data:image/svg+xml;base64,A_BUNCH_OF_BASE64_LETTERS_AND_NUMBERS_HERE" }
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import '@openzeppelin/contracts/token/ERC721/ERC721.sol' import '@openzeppelin/contracts/utils/Base64.sol' import '@openzeppelin/contracts/utils/Strings.sol' string constant TIER_NAME_0 = "Basic"; string constant TIER_NAME_1 = "Medium"; string constant TIER_NAME_2 = "Premium"; uint256 constant TIER_VALUE_0 = 0.01 ether; uint256 constant TIER_VALUE_1 = 0.02 ether; uint256 constant TIER_VALUE_2 = 0.05 ether; contract TierNFT is ERC721 { uint256 public totalSupply; mapping(uint256 => uint256) public tokenTier; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public payable { require( msg.value >= TIER_VALUE_0, "Not enough value for the minimum Tier" ); uint256 tierId = 0; if (msg.value >= TIER_VALUE_2) tierId = 2; else if (msg.value >= TIER_VALUE_1) tierId = 1; totalSupply++; _safeMint(msg.sender, totalSupply); tokenTier[totalSupply] = tierId; } // Create the tokenURI json on the fly without creating files individually function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_exists(tokenId), "Nonexistent token"); string memory imageSVG = "PLACEHOLDER FOR SVG IMAGE"; string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "', name(), " #", Strings.toString(tokenId), '", "description": "TierNFTs collection",' '"image": "data:image/svg+xml;base64,',Base64.encode(bytes(imageSVG)), '"}' ) ) ) ); return string(abi.encodePacked("data:application/json;base64,", json)); } }
Okay. We've done a bunch of things with our contract and now we're going to do some scalable vector graphic magic!
Add these lines right above the other constants defined for the tiers:
string constant SVG_START = '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="none" font-family="sans-serif"><defs><filter id="A" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="500" width="500"><feDropShadow dx="1" dy="2" stdDeviation="8" flood-opacity=".67" width="200%" height="200%" /></filter><linearGradient id="B" x1="0" y1="0" x2="15000" y2="0" gradientUnits="userSpaceOnUse"><stop offset=".05" stop-color="#ad00ff" /><stop offset=".23" stop-color="#4e00ec" /><stop offset=".41" stop-color="#ff00f5" /><stop offset=".59" stop-color="#e0e0e0" /><stop offset=".77" stop-color="#ffd810" /><stop offset=".95" stop-color="#ad00ff" /></linearGradient><linearGradient id="C" x1="0" y1="60" x2="0" y2="110" gradientUnits="userSpaceOnUse"><stop stop-color="#d040b8" /><stop offset="1" stop-color="#e0e0e0" /></linearGradient></defs><path fill="url(#B)" d="M0 0h15000v500H0z"><animateTransform attributeName="transform" attributeType="XML" type="translate" from="0 0" to="-14500 0" dur="16s" repeatCount="indefinite" /></path><circle fill="#1d1e20" cx="100" cy="90" r="45" filter="url(#A)" /><text x="101" y="99" text-anchor="middle" class="nftLogo" font-size="32px" fill="url(#C)" filter="url(#A)">D_D<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 100 90" to="360 100 90" dur="5s" repeatCount="indefinite" /></text><g font-size="32" fill="#fff" filter="url(#A)"><text x="250" y="280" text-anchor="middle" class="tierName">'; string constant SVG_END = "</text></g></svg>"; // string constant TIER_NAME = ...
Here, we prepared the start and end of our SVG. We can test this out by joining the start and end of the SVG into this online svg editor.
And now some more modifications. Inside the tokenURI function, right below require(…)
, add these lines:
// require(...); string memory tierName = tokenTier[tokenId] == 2 ? TIER_NAME_2 : tokenTier[tokenId] == 1 ? TIER_NAME_1 : TIER_NAME_0; string memory imageSVG = string( abi.encodePacked(SVG_START, tierName, SVG_END) ); // string memory imageSVG = string(...
tierName
will store the type of tierNFT we are getting.imageSVG
is to create an SVG image with the corresponding tier type inside it.For marketplaces to recognize our NFT assets, we need to add some JSON attributes. We created our JSON metadata attributes based on Opensea Metadata Standard, which you can take a look at, if you'd like a deeper understanding.
We'll replace the JSON part to add our attributes:
'{"name": "',name()," #",Strings.toString(tokenId), '", "description": "TierNFTs collection",' '"image": "data:image/svg+xml;base64,', Base64.encode(bytes(imageSVG)), '","attributes":[{"trait_type": "Tier", "value": "',tierName, '" }]}'
We added attributes which are basically some trait types, based on the Metadata Standard.
// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import '@openzeppelin/contracts/token/ERC721/ERC721.sol' import '@openzeppelin/contracts/utils/Base64.sol' import '@openzeppelin/contracts/utils/Strings.sol' string constant SVG_START = '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="none" font-family="sans-serif"><defs><filter id="A" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="500" width="500"><feDropShadow dx="1" dy="2" stdDeviation="8" flood-opacity=".67" width="200%" height="200%" /></filter><linearGradient id="B" x1="0" y1="0" x2="15000" y2="0" gradientUnits="userSpaceOnUse"><stop offset=".05" stop-color="#ad00ff" /><stop offset=".23" stop-color="#4e00ec" /><stop offset=".41" stop-color="#ff00f5" /><stop offset=".59" stop-color="#e0e0e0" /><stop offset=".77" stop-color="#ffd810" /><stop offset=".95" stop-color="#ad00ff" /></linearGradient><linearGradient id="C" x1="0" y1="60" x2="0" y2="110" gradientUnits="userSpaceOnUse"><stop stop-color="#d040b8" /><stop offset="1" stop-color="#e0e0e0" /></linearGradient></defs><path fill="url(#B)" d="M0 0h15000v500H0z"><animateTransform attributeName="transform" attributeType="XML" type="translate" from="0 0" to="-14500 0" dur="16s" repeatCount="indefinite" /></path><circle fill="#1d1e20" cx="100" cy="90" r="45" filter="url(#A)" /><text x="101" y="99" text-anchor="middle" class="nftLogo" font-size="32px" fill="url(#C)" filter="url(#A)">D_D<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 100 90" to="360 100 90" dur="5s" repeatCount="indefinite" /></text><g font-size="32" fill="#fff" filter="url(#A)"><text x="250" y="280" text-anchor="middle" class="tierName">'; string constant SVG_END = "</text></g></svg>"; string constant TIER_NAME_0 = "Basic"; string constant TIER_NAME_1 = "Medium"; string constant TIER_NAME_2 = "Premium"; uint256 constant TIER_VALUE_0 = 0.01 ether; uint256 constant TIER_VALUE_1 = 0.02 ether; uint256 constant TIER_VALUE_2 = 0.05 ether; contract TierNFT is ERC721 { uint256 public totalSupply; mapping(uint256 => uint256) public tokenTier; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public payable { require( msg.value >= TIER_VALUE_0, "Not enough value for the minimum Tier" ); uint256 tierId = 0; if (msg.value >= TIER_VALUE_2) tierId = 2; else if (msg.value >= TIER_VALUE_1) tierId = 1; totalSupply++; _safeMint(msg.sender, totalSupply); tokenTier[totalSupply] = tierId; } // Create the tokenURI json on the fly without creating files individually function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_exists(tokenId), "Nonexistent token"); string memory tierName = tokenTier[tokenId] == 2 ? TIER_NAME_2 : tokenTier[tokenId] == 1 ? TIER_NAME_1 : TIER_NAME_0; string memory imageSVG = string( abi.encodePacked(SVG_START, tierName, SVG_END) ); string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "',name()," #",Strings.toString(tokenId), '", "description": "TierNFTs collection",' '"image": "data:image/svg+xml;base64,', Base64.encode(bytes(imageSVG)), '","attributes":[{"trait_type": "Tier", "value": "',tierName, '" }]}' ) ) ) ); return string(abi.encodePacked("data:application/json;base64,", json)); } }
We need to find a way to actually withdraw any funds our contract generates, otherwise they'll get stuck in the contract .... that we created!
// Place this next to the other imports at the top: import "@openzeppelin/contracts/access/Ownable.sol";
We import Ownable.sol
, so that only we can withdraw those funds, and not anyone else. Clever, eh?
// Modify the contract definition, by adding 'Ownable' at the end of the line: contract TierNFT is ERC721, Ownable { // Our whole contract code here }
We inherit Ownable
from the Open Zeppelin contract into ours.
If your phone is ringing, or someone is knocking at your door right now, ignore all of it! Let’s get this withdraw function coded in here!!
// tokenURI function part of the code... // Function to withdraw funds from contract function withdraw() public onlyOwner { // Check that we have funds to withdraw uint256 balance = address(this).balance; require(balance > 0, "Balance should be > 0"); // Withdraw funds. (bool success, ) = payable(owner()).call{value: balance}(""); require(success, "Withdraw failed"); } // 'withdraw' will be our last function at the end of the contract }
onlyOwner
- You're going to see this modifier a lot. It comes from the Ownable contract we just imported. It's very powerful. It makes
sure that only the account that deployed the contract (owner is assigned on its
constructor) can execute the function that it appears in.uint256 balance = address(this).balance
against require(balance > 0, "Balance should be > 0");
, we can see if we actually have something to withdraw. And we would hope so, as the function consumes gas. And, as you've probably guessed, it will throw an error otherwise.(bool success, ) = payable(owner()).call{value: balance}("")
- is an actual
transfer of funds which uses the whole balance that we checked in the previous
instruction.require(success, "Withdraw failed")
- This is a good practice because
call
doesn’t revert. With this practice we can make sure that the transfer
occurred and throw an error if it doesn’t.// SPDX-License-Identifier: MIT pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Base64.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; string constant SVG_START = '<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" fill="none" font-family="sans-serif"><defs><filter id="A" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="500" width="500"><feDropShadow dx="1" dy="2" stdDeviation="8" flood-opacity=".67" width="200%" height="200%" /></filter><linearGradient id="B" x1="0" y1="0" x2="15000" y2="0" gradientUnits="userSpaceOnUse"><stop offset=".05" stop-color="#ad00ff" /><stop offset=".23" stop-color="#4e00ec" /><stop offset=".41" stop-color="#ff00f5" /><stop offset=".59" stop-color="#e0e0e0" /><stop offset=".77" stop-color="#ffd810" /><stop offset=".95" stop-color="#ad00ff" /></linearGradient><linearGradient id="C" x1="0" y1="60" x2="0" y2="110" gradientUnits="userSpaceOnUse"><stop stop-color="#d040b8" /><stop offset="1" stop-color="#e0e0e0" /></linearGradient></defs><path fill="url(#B)" d="M0 0h15000v500H0z"><animateTransform attributeName="transform" attributeType="XML" type="translate" from="0 0" to="-14500 0" dur="16s" repeatCount="indefinite" /></path><circle fill="#1d1e20" cx="100" cy="90" r="45" filter="url(#A)" /><text x="101" y="99" text-anchor="middle" class="nftLogo" font-size="32px" fill="url(#C)" filter="url(#A)">D_D<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 100 90" to="360 100 90" dur="5s" repeatCount="indefinite" /></text><g font-size="32" fill="#fff" filter="url(#A)"><text x="250" y="280" text-anchor="middle" class="tierName">'; string constant SVG_END = "</text></g></svg>"; string constant TIER_NAME_0 = "Basic"; string constant TIER_NAME_1 = "Medium"; string constant TIER_NAME_2 = "Premium"; uint256 constant TIER_VALUE_0 = 0.01 ether; uint256 constant TIER_VALUE_1 = 0.02 ether; uint256 constant TIER_VALUE_2 = 0.05 ether; contract TierNFT is ERC721, Ownable { uint256 public totalSupply; mapping(uint256 => uint256) public tokenTier; constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} function mint() public payable { require( msg.value >= TIER_VALUE_0, "Not enough value for the minimum Tier" ); uint256 tierId = 0; if (msg.value >= TIER_VALUE_2) tierId = 2; else if (msg.value >= TIER_VALUE_1) tierId = 1; totalSupply++; _safeMint(msg.sender, totalSupply); tokenTier[totalSupply] = tierId; } // Create the tokenURI json on the fly without creating files individually function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_exists(tokenId), "Nonexistent token"); string memory tierName = tokenTier[tokenId] == 2 ? TIER_NAME_2 : tokenTier[tokenId] == 1 ? TIER_NAME_1 : TIER_NAME_0; string memory imageSVG = string( abi.encodePacked(SVG_START, tierName, SVG_END) ); string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "', name(), " #", Strings.toString(tokenId), '", "description": "TierNFTs collection", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(imageSVG)), '","attributes":[{"trait_type": "Tier", "value": "', tierName, '" }]}' ) ) ) ); return string(abi.encodePacked("data:application/json;base64,", json)); } // Function to withdraw funds from contract function withdraw() public onlyOwner { // Check that we have funds to withdraw uint256 balance = address(this).balance; require(balance > 0, "Balance should be > 0"); // Withdraw funds. (bool success, ) = payable(owner()).call{value: balance}(""); require(success, "Withdraw failed"); } }
We need a script so we can get our smart contract deployed. Let’s write that.
Create a new javascript file named deploy.js
in the scripts
folder with this
code:
const hre = require('hardhat') /** Set contract and collection name **/ const CONTRACT_NAME = 'TierNFT' const COLLECTION_NAME = 'TierNFT' const COLLECTION_SYMBOL = 'Tier' /** Main deploy function **/ async function main() { const contractFactory = await hre.ethers.getContractFactory(CONTRACT_NAME) const contract = await contractFactory.deploy( COLLECTION_NAME, COLLECTION_SYMBOL, ) await contract.deployed() // Print our newly deployed contract address console.log(`Contract deployed to ${contract.address}`) } /** Run Main function - Do not change **/ main().catch((error) => { console.error(error) process.exitCode = 1 })
CONTRACT_NAME = "TierNFT"
- is the name of our contract, which
will tell Hardhat exactly what to deploy.const COLLECTION_NAME = "TierNFT"
and
const COLLECTION_SYMBOL = "Tier"
- we define the Name and Symbol to pass to the constructor for the deployment.hre.ethers.getContractFactory(CONTRACT_NAME)
- this asks Hardhat Runtime
Environment to get us a contract factory for our contract.contractFactory.deploy
- We are asking the contract factory to deploy our
contract. This is the deploy transaction!COLLECTION_NAME, COLLECTION_SYMBOL
- These are the parameters for our
contract's constructor function.await contract.deployed()
- It waits for the transaction to be approved and our
contract is finished deploying.main().catch( … )
at the very end makes sure that all the previous
code is executed when this script is run, and also prints any errors to the console.Now the time has come for us to deploy our smart contract. But before we can do
that, we need to modify the code in hardhat.config.js
placed in our root folder to this:
require('@nomicfoundation/hardhat-toolbox') require('dotenv').config() /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.12', networks: { mumbai: { url: 'https://rpc-mumbai.maticvigil.com', accounts: [process.env.PRIVATE_KEY], }, }, }
What we are doing is adding the RPC and the network to the config file. Now we need to connect our wallet to the testnet and add some testnet tokens, so we can actually pay for the deployment of the contract.
To add the Polygon Mumbai network head over to the Chainlist page and connect your wallet. Make sure you toggle the testnet button, otherwise no testnets will show up, and search for Mumbai. You will see the testnet network with chainID 80001. Add it to your wallet. Note: Always make sure to use a separate browser profile, with a separate wallet, holding only testnet tokens, for any tutorials. See our section on Wallets for background on your security, your private keys and your recovery seed phrases!
A testnet is a sandbox environment where developers can test, create and modify functionalities, monitor and simulate a mainnet blockchain's network performance, fix bugs and other network failures without having to worry about breaking a main chain, and paying in real crypto coins to do so! Mainnets cost - testnets generally don't.
We get testnet tokens from faucets. A faucet is a website, which on request, will drip a small amount of testnet tokens onto your address, and sometimes require completion of small tasks before doing so. some testnet on the wallets. Note: Testnet and mainnet are separate networks. You can't for example send tokens from a testnet to a mainnet. Let's head over and get some on this website.
If you complete the required tasks, you can get tokens for multiple testnets.
Before we deploy, we need to add a .env
file to our root folder to make sure we are not pushing our private keys into public repositories.
For your wallet's private key, the most sensitive data of the project, you need to open Metamask, click on the three dots next to your Account Name, and then on Account Details, then click on Export Private Key. It will ask for your Metamask password, the one you use to open it, NOT your seed phrase. It also shows you a
notice so you know that you are entering the danger zone. Confirm and you'll be able to copy your private key. Add your private key into the .env
file like so:
PRIVATE_KEY=f8abc629b....
Also, make sure you have a line that says .env
in the .gitignore
file in our
root folder. If this file doesn't exist, create it and add that line! This makes
sure we don't accidentally upload our .env
file to our public repositories by
mistake.
Now we will run this command to install the dotenv
package:
npm install dotenv --save
This takes care of loading our environment variables from the .env
file, so we
don’t have to store sensitive information, such as private keys, to standard
configuration files, which may need uploaded to a project's repo.
Remember to always protect your private keys, and your recovery seed phrases to keep your wallet safe and unwanted guests out.
We will deploy our smart contract by using this command:
npx hardhat run scripts/deploy.js --network mumbai
We specify where we want the contract to be deployed in the —-network
part
of the command.
Woohoo! Finally we deployed our contract! And a contract address, which we will need in a bit.
Without a new script we won’t be able to mint any of our NFTs.
The mint function will run three times to mint each different Tier.
The code for what we want is below, but we need a home for it. So let's go back to the /scripts
directory and create a mint.js
file and paste in the code below. When we're done, we can grab our shiny new contract address from the command line and paste it after const CONTRACT_ADDRESS =
in the file:
const hre = require('hardhat') /** Set contract and collection name **/ const CONTRACT_NAME = 'TierNFT' const CONTRACT_ADDRESS = 'INSERT_CONTRACT_ADDRESS_HERE' const VALUE_TIER_0 = '0.01' // in ethers/matic const VALUE_TIER_1 = '0.02' // in ethers/matic const VALUE_TIER_2 = '0.05' // in ethers/matic /** Main deploy function **/ async function main() { const contractFactory = await hre.ethers.getContractFactory(CONTRACT_NAME) const contract = await contractFactory.attach(CONTRACT_ADDRESS) // Print our newly deployed contract address console.log(`Attached contract: ${contract.address}`) // Call the mint function for Tier 0 let txn = await contract.mint({ value: hre.ethers.utils.parseEther(VALUE_TIER_0), }) await txn.wait() // Wait for the NFT to be minted console.log('Minted a Tier 0 NFT!') // Call the mint function for Tier 1 txn = await contract.mint({ value: hre.ethers.utils.parseEther(VALUE_TIER_1), }) await txn.wait() // Wait for the NFT to be minted console.log('Minted a Tier 1 NFT!') // Call the mint function for Tier 2 txn = await contract.mint({ value: hre.ethers.utils.parseEther(VALUE_TIER_2), }) await txn.wait() // Wait for the NFT to be minted console.log('Minted a Tier 2 NFT!') let totalSupply = await contract.totalSupply() console.log("Collection's new totalSupply: ", totalSupply) } /** Run Main function - Do not change **/ main().catch((error) => { console.error(error) process.exitCode = 1 })
const contract = await contractFactory.attach(CONTRACT_ADDRESS)
will
make sure that we are not deploying the contract again. Instead we need
Hardhat to use the contract addresss we just deployed to the testnet.let txn = await contract.mint(...
is calling the mint function.value: hre.ethers.utils.parseEther(VALUE_TIER_0)
- defines the value that we
want to send to the mint function. This defines which Tier we get.ethers.utils.parseEther
- here we use Ethers to translate the value into wei i.e. multiply it with 10**18let totalSupply = await contract.totalSupply()
- is calling the
totalSupply()
function to check if the 3 NFTs minted correctly.To mint our tier NFTs we will run the following command.
npx hardhat run scripts/mint.js --network mumbai
If we look at our terminal we will see something like this.
We have just minted 3 NFTs with different Tiers!
Let’s go ahead and view them on the Opensea marketplace. This could take a few minutes to appear, don't panic. You can search your newly created collection with your contract address or with the name that you chose in https://testnets.opensea.io/
Of course we already have a couple up there, but you will be able to view the three NFTs of your own, which you so diligently minted. You're an artist!
Developer DAO Foundation © 2023
Website content licensed under CC BY-NC 4.0.
Website code is licensed under MIT.