Before we start, I would like to mention, that you can use any network you want. With the help of this article, you can deploy to Mainnet also.
Smartcontract repo (erc721): https://github.com/gapon2401/erc721
Mint page repo (erc721-mint): https://github.com/gapon2401/erc721-mint
Smartcontract: Etherscan Goerli link
We need to go through the following steps to reach our goal:
Create new project and install https://hardhat.org — Ethereum development environment. Let’s call this project erc721
.
In command line run
npx hardhat
Select Create a Typescript project
.
In order to continue you will be asked to install some dependencies. The text may vary on the hardhat version.
I will run the following, because I will use yarn
:
yarn add -D hardhat@^2.10.2 @nomicfoundation/hardhat-toolbox@^1.0.1
We will use https://www.openzeppelin.com/ template for ERC-721 smartcontract, where all necessary functions have been implemented. You can take a look on it: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
Install openzeppelin packages and dotenv:
yarn add -D @openzeppelin/contracts dotenv
Remove everything from the folder contracts
and create a file ERC721Contract.sol
with the contents:
https://gist.github.com/gapon2401/d6bf2f0eef9b7e6721b98028f890004e
If you have any difficulties with the code above, feel free to read more about the Solidity. Here is the summary:
constructor
— on deployment we will set the base URI for all tokens, name and a symbol to the token collection.mintByOwner
— is our function, which will create new NFT’s for us. Only owner of the contract can call it.mint
— public mint.setBaseURI
— owner of the contract can change the base URI for tokenswithdraw
—owner can call this function to transfer money from the contract to it’s own wallet addressAll other necessary functions are inhereted in this contract from openzeppelin.
The price of the tokens will be 0.1 ETH:
uint256 public constant PRICE = 100000000000000000;
Install the following dependencies in order to escape the errors in developing 😲
yarn add -D ethers @nomiclabs/hardhat-etherscan @typechain/hardhat hardhat-gas-reporter solidity-coverage @types/chai @types/mocha typescript ts-node @nomicfoundation/hardhat-chai-matchers chai @nomicfoundation/hardhat-network-helpers typechain @nomiclabs/hardhat-waffle @typechain/ethers-v5 @nomiclabs/hardhat-ethers
Now configure your hardhat.config.ts
:
https://gist.github.com/gapon2401/4bd368815c075954f7cac6b43e1cd687
What do we have here:
solidity
— we specified the settings and enabled optimizationnetworks
— networks, that we are going to usegasReporter
— will show us gas units, that we need to spend for deployment or calling the functionsetherscan
— plugin for integration with Etherscan’s contract verification serviceThe next step is to create .env
file in project root. You have to create 5 variables:
PRIVATE_KEY
— the private key of the wallet, that wil be the owner of the contract
ETHERSCAN_API_KEY
-etherscan key. Register on https://etherscan.io and on the page https://etherscan.io/myapikey create and copy your key:
REPORT_GAS=1
— Are we going to use gas reporter or not. We will use it
GOERLI_URL
and MAINNET_URL
— use https://infura.io/ or https://www.alchemy.com/ for getting the urls.
For instance, infura URL for MAINNET_URL
:
Infura URL for GOERLI_URL
:
Now compile the code with the command:
npx hardhat compile
After that command some new folders will appear: artifacts
, cache
, typechain-types
.
It’s time to write deploy function, we will use tasks for it.
Remove folder scripts
, we don’t need it.
Create the folder tasks
and the file deploy.ts
inside it:
https://gist.github.com/gapon2401/6dbb24a1308479d934897abf743c32e1
In this task we get smartcontract and deploy it. After the deployment in console we will see the address of the contract.
We will come back to that function later.
We will write the tests for every exteral/public function.
Clear the folder test
and create index.ts
inside it with the contents:
https://gist.github.com/gapon2401/47162e377dde919f06108905ecdb847f
Run npx hardhat test
.
You should get similar to that:
All tests passed successfully!
For determine the price of deployment and minting functions we have used module hardhat-gas-reporter
. It was enabled by default in hardhat.config.ts
, that’s why after all tests were passed, we got the table:
In the table we can see the gas consumption for methods and deployment.
This is the units of gas we need to deploy the contract.
How much it will cost in ETH?
Use the formula:
(gas units) * (gas price per unit) = gas fee in gwei
In this formula we don’t know the amount of gas price per unit
.
You can use https://ethgasstation.info/ or any other website to find this information. At the moment of writing this article, gas price is 17.
The value can change greatly depending on the time of day. It may be 2 or 10 or 50 🙃. Be careful.
So, the average cost will be:
Deployment = 1 697 092 * 17 = 28 850 564 gwei = 0,028850564 ETH
mint = 88 753 * 17 = 1 508 801 gwei = 0,001508801 ETH
mintByOwner = 91 060 * 17 = 1 548 020 gwei = 0,001548020 ETH
setBaseURI = 32 153 * 17 = 546 601 gwei = 0,000546601 ETH
withdraw = 30 421 * 17 = 517 157 gwei = 0,000517157 ETH
Now the ETH/USD price is $1 592, it means that deployment to mainnet will cost about $46 and calling a mint
function will cost about $2.4.
Images and metadata for NFTs can be stored on your server or in IPFS.
We will cover the first case. Assume, that you have a domain
https://my-domain.com
All NFT images will store here:
https://my-domain.com/collection/assets/
All metadata here:
https://my-domain.com/collection/metadata/
So, the base URI for our smartcontract will be
https://my-domain.com/collection/metadata/
In our smartcontract by default all token URI’s will be counted from 0. Therefore, files for metadata should be like this:
0.json
1.json
....
1234.json
If you use mintByOwner
function, then you can change the name of the file for specific NFT, and it can be like this: custom.json
Upload NFT images on your server. They should be available by links:
https://my-domain.com/collection/assets/0.png
https://my-domain.com/collection/assets/1.png
https://my-domain.com/collection/assets/2.png
Now create metadata for NFT’s.
Here is an example of one file 0.json
:
{
"name": "Test NFT #0",
"image": "https://my-domain.com/collection/assets/0.png"
}
Pay attention, that in image
we have defined the link to the image, which has been upoaded to the server.
Read more about opensea metadata and available values in it.
Upload your metadata files, so they should be available by links:
https://my-domain.com/collection/metadata/0.json
https://my-domain.com/collection/metadata/1.json
https://my-domain.com/collection/metadata/2.json
Everything is ready for deployment.
Take a look on our tasks/deploy.ts
, we’ve specified baseUri
as
https://my-domain.com/collection/metadata/
This is because all our metadata is available by this address.
Here you can change the name
and the symbol
of the collection.
In command line run:
npx hardhat deploy --network goerli
This command will deploy smartcontract to Goerli network. If you want to deploy to mainnet, you can run:
npx hardhat deploy --network mainnet
All these networks were described on hardhat.config.ts
file.
After the deployment you will get the following in your console:
My smartcontract address is:
0xc191B6505B16EBe5D776fb30EFbfe41A9252023a
From now, it is available on etherscan testnet:
https://goerli.etherscan.io/address/0xc191B6505B16EBe5D776fb30EFbfe41A9252023a
We need to verify our smartcontract, so everyone can check the code.
Wait about 5 minutes and try to verify it. If you get an error while verifying, don’t worry, just try to wait more time. In Mainnet I was waiting for 10–15 minutes.
In general verify function looks like this:
npx hardhat verify --network <YOUR_NETWORK> <DEPLOYED_CONTRACT_ADDRESS> <arg1> <arg2> <argn>
Let’s try to determine, what to specify in our verify function:
goerli
0xc191B6505B16EBe5D776fb30EFbfe41A9252023a
tasks/deploy.ts
"https://my-domain.com/collection/metadata/"
"My ERC721 name"
"MyERC721Symbol"
Arguments should be the same, that were in tasks/deploy.ts
!!!
So, in command line I will run:
npx hardhat verify --network goerli 0xc191B6505B16EBe5D776fb30EFbfe41A9252023a "https://my-domain.com/collection/metadata/" "My ERC721 name" "MyERC721Symbol"
The result should be the next:
On this page you can check the code of deployed smartcontract.
🥳We have finished with the smartcontract. Here is the repo, where you can find all the code.
This will be our second project erc721-mint
.
For demo website I will use NextJS.
To speed up the development clone my repository: https://github.com/gapon2401/erc721-mint
Install all dependencies:
yarn install
Open file .env
and fill in the variables:
NEXT_PUBLIC_SMARTCONTRACT_ADDRESS
— address of your smartcontract from the paragraph Deploy and verify smartcontract.NEXT_PUBLIC_SMARTCONTRACT_NETWORK
— the network, where your smartcontract was deployed. We need this to switch the network. Specify goerli
, because we have deployed our smartcontact there.NEXT_PUBLIC_SMARTCONTRACT_INFURA_URL
— network endpoint. You can use https://infura.io to get the url.Go to your dashboard, select your project and copy Api Key
:
In our first smartcontract project erc721
open the file:
artifacts/contracts/ERC721Contract.sol/ERC721Contract.json
Copy abi
:
In current project erc721-mint
open the file:
src/components/smartcontract.ts
Replace the abi
value.
Everything is done! 🎉 You can run the project yarn dev
and try to use it!
In the next section I will cover the main components.
Take a look on main components:
src/components/smartcontract.ts
Contains information about the smartcontract. It's better to dynamically load this file from the server, because we want our web page loads as soon as possible. In this demo mint form appears after the page loads.
2. src/components/Web3Provider.tsx
This component communicates with blockchain. There are some functions and variables stored in React Context:
connected
- has user connected the wallet or notisConnecting
- the state of reconnectingconnect()
- function for connecting to networkreconnect()
– function for reconnecting. Cached provider will be used, that was selected by user at the last timedisconnect()
– function for disconnecting network – selected by usernetwork
: goerli, mainnet etcprovider
– provider for communicating with the wallet: metamask, ledger, tokenary, etcsigner
- the owner of the walletchainId
– ID of the selected networkaccountId
– your wallet addresserror
- error message for web3Modalsrc/components/MintForm.tsx
This component displays form for minting.
Function mint()
is called after the user clicks on the mint button.
Now look at the code:
const transactionResponse = await Contract.mint({
value: ethers.utils.parseEther('0.1'),
})
We are calling mint
function from the smartcontract and send 0.1 ETH to it, because this function requires exactly that value.
What if we want to call mintByOwner
?
We can do it like this:
const transactionResponse = await Contract.mintByOwner( '0x01234566', 'token_uri' )
But pay attention, that this function can be called only by the owner of the contract. Why? We have used onlyOwner
modifier in smartcontract:
function mintByOwner(address to, string calldata tokenUri) external onlyOwner
A few more words about that function.
0x01234566
- this is the address, for which you want to mint token. Make sure to write full address.
token_uri
- this is custom token URI. In our deployed smartcontract the full address to token metadata looks like:
${baseUri + token_uri}.json
Remember, we have specified baseUri
:
https://my-domain.com/collection/metadata/
So, the full address to metadata with token URI equals token_url
will be:
https://my-domain.com/collection/metadata/token_url.json
Also, we don't need to pay for this transaction, except gas price.
Function prepareNetwork()
.
On first connection this function can propose to change the network. In this function are used only goerli
and mainnet
networks. You can change it, if you need, here:
chainId: smartContract.network === 'goerli' ? '0x5' : '0x1',
Function getBalanceOf
.
This function checks is it enough money on the wallet to make a transaction. In the file you may find commented code in two places:
/* reconnect ,*/
and
/* Reconnect to the wallet */
// useEffect(() => {
// ;(async () => {
// await reconnect()
// })()
// }, [reconnect])
Uncomment it to make reconnections every time the page reloads.
Thank you for reading! ❤