A tutorial for full-stack NFT minting application
Tutorial: NFT-minting-tutorial
A tutorial for designing full-stack Non-Fungible Token (NFT) minting application.
Prerequisites
- Node.js ^v16.20.0
- Metamask
- Ganache UI or ganache-cli
- Truffle@latest framework
- InterPlanetary File System / IPFS
- React.js
- Solidity
- OpenZeppelin
- Chai Assertion Library
Overview
This tutorial will walk you through the process of designing a full-stack Non-Fungible Token (NFT) minting application. The application will allow users to mint NFTs, view their NFTs, and transfer their NFTs to other users. The application will be built using React.js, Solidity, and OpenZeppelin. The application will also use the Chai Assertion Library for testing. The application will be deployed to the Local Ethreum / Rinkeby test network.
Step-by-Step Guide
- Clone the repo to your local machine
git clone https://github.com/lamnd09/nft-minting-tutorial.git
cd nft-minting-tutorial
- cd into the client directory
cd client
npm install
npm start
- to the root repository, install dependencies
npm install
npm install @openzeppelin/contracts
Create your first Ethereum Blockchain using Ganache UI or ganache-cli
- Download and install Ganache UI or ganache-cli from https://www.trufflesuite.com/ganache or just run
npm install -g ganache
To check full options of ganache-cli, run
ganache-cli --help
next, in order to create a local Ethereum Blockchain, run
ganache-cli -a 20 -e 1000 -m "test test test test test test test test test test test okay" -i 1337
You should see the output as below:
Ganache CLI v6.12.2 (ganache-core: 2.13.2)
Available Accounts
==================
(0) 0x8dE930dbAd0D99759Db57C2F906010f87D4185FF (1000 ETH)
(1) 0x9032969C6ac84f644bA65E40089ce221053ad624 (1000 ETH)
(2) 0xA1e77EC2f0ad59121DA812cd5be61E3CAc30F472 (1000 ETH)
(3) 0x363404432Aa9d4EF5629f80DAcFF0CD389dd38c8 (1000 ETH)
(4) 0x5c22Fd2D3E9eB70EF3585E0F895eE4d076d04414 (1000 ETH)
(5) 0xF221a97B436f16b8a2916eA232f45D5f5C5DAD10 (1000 ETH)
Private Keys
==================
(0) 0xaefcaad212aba1ac9c96f94f5dcef495467fb745179976fbe31711bc559fedcf
(1) 0xb2152533065ded1085494e704786c14e4258097fba26273adf5f44645fca17d2
(2) 0xceb1d8bc2c141e448d3d31fb5671a1b03a552a8d5ec683ad9513aa8289b84a8b
(3) 0x8ab686ec316e15671f09cceaf463d8d2bca58d87852f7f245aef86a787834abc
(4) 0x5263a193a48c089e9320320bac29b0fbd89c3e1abc5e8c3bc938b87dd4e4de15
(5) 0xc1a2ac12f6560e32fcac5ab7631bd6282c20e2b37abd307ed29ab033db3b1584
HD Wallet
==================
Mnemonic: test test test test test test test test test test test okay
Base HD Path: m/44'/60'/0'/0/{account_index}
Gas Price
==================
20000000000
Gas Limit
==================
6721975
Call Gas Limit
==================
9007199254740991
Listening on 127.0.0.1:8545
Connect Metamask to your local Ethereum Blockchain
- Install Metamask in your web browser
- Connect Metamask to your local Ethereum Blockchain
- Import the private keys of the accounts generated by Ganache UI or ganache-cli
After import, you should see the account information.
Setup IPFS Storage
- Install IPFS on your local machine Link to download IPFS https://docs.ipfs.tech/install/command-line/
For Ubuntu OS, just run :
wget https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz
tar -xvzf kubo_v0.20.0_linux-amd64.tar.gz
cd kubo
sudo bash install.sh
ipfs --version
You can find the detail instructions for your OS in the link above.
- Start IPFS daemon
ipfs init
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://localhost:3000"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]'
ipfs daemon
- Add your file to IPFS
ipfs add <your file>
example,
ipfs add ./gallery/nft1.jpg
added QmZNEyAxoq55x3jZofjmCdqG1ujpYaKh8TGNhLjdkwiHqz nft1.jpeg
31.87 KiB / 31.87 KiB [================================================] 100.00%>
ipfs add ./gallery/nft2.jpg
added QmRuJJT9cqVJvxMukG56WtEBbgbTQrknpWXyHTvDN41guf nft2.jpg
50.52 KiB / 50.52 KiB [================================================] 100.00%
ipfs add ./gallery/nft3.jpg
added QmS7XWuQd8pNPGMEpkTkhft3k5gCCc7TUZzVXQpXcSGj8B nft3.jpg
176.41 KiB / 176.41 KiB [==============================================] 100.00%
- Get the hash of your file
ipfs cat <hash of your file>
Compile and Deploy the Smart Contract to your local Ethereum Blockchain
- Design the Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// Mapping to track the rewards received by each user
mapping(address => uint256) private rewardsReceived;
// Mapping to track the NFTs minted by each user
mapping(address => uint256[]) private userMintedTokens;
// Mapping to check if a token has already been minted
mapping(string => bool) private tokenExists;
constructor() public ERC721("MyNFT", "NFT") {}
function mintNFT(address recipient, string memory tokenURI)
public
returns (uint256)
{
require(!tokenExists[tokenURI], "Token already exists");
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
// Reward the user with 1 ETH.
rewardUser(recipient);
// Track the NFTs minted by the user
userMintedTokens[recipient].push(newItemId);
// Mark the token as minted
tokenExists[tokenURI] = true;
return newItemId;
}
function rewardUser(address user) internal {
uint256 rewardAmount = 1 ether;
require(address(this).balance >= rewardAmount, "Not enough Ether to reward user");
// Update the rewardsReceived mapping
rewardsReceived[user] += rewardAmount;
// Transfer the reward to the user.
(bool success, ) = payable(user).call{value: rewardAmount}("");
require(success, "Failed to send Ether");
emit UserRewarded(user, rewardAmount);
}
// This function allows the contract owner to deposit ETH into the contract
function depositETH() public payable {}
// This function will allow the owner to withdraw all the remaining ETH.
function withdrawETH() public onlyOwner {
uint balance = address(this).balance;
payable(owner()).transfer(balance);
}
// Function to get the total amount of rewards an user received
function totalRewards(address user) public view returns (uint256) {
return rewardsReceived[user];
}
// Function to get all tokens minted by a user
function getMintedTokens(address user) public view returns (uint256[] memory) {
return userMintedTokens[user];
}
// Function to check if a token with a given URI already exists
function checkTokenExists(string memory tokenURI) public view returns (bool) {
return tokenExists[tokenURI];
}
// Event emitted when a user is rewarded
event UserRewarded(address indexed user, uint256 rewardAmount);
}
Let me explain line-by-line of the code Certainly, let’s go line by line:
-
pragma solidity ^0.8.17;
: This line tells that the source code is written in Solidity language version 0.8.17. -
The next three lines import external smart contracts from the OpenZeppelin library, which is a framework of reusable and secure smart contracts in the Solidity language.
-
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
imports a standard implementation of an ERC721 token with URI storage. -
import "@openzeppelin/contracts/utils/Counters.sol";
imports a utility library for safe counter arithmetic. -
import "@openzeppelin/contracts/access/Ownable.sol";
imports a contract that provides basic authorization control functions. This simplifies the implementation of “user permissions”.
-
-
contract MyNFT is ERC721URIStorage, Ownable {
: This line declares the smart contract named “MyNFT”, which inherits from the ERC721URIStorage and Ownable contracts. -
using Counters for Counters.Counter;
: This line attaches the Counters library functions to the Counters.Counter struct. -
Counters.Counter private _tokenIds;
: Here, _tokenIds is a state variable of type Counter that will keep track of the total number of tokens minted by the contract. -
mapping(address => uint256) private rewardsReceived;
: This line declares a state variablerewardsReceived
, which is a mapping from addresses to integers. It will keep track of the total rewards received by each address. -
mapping(address => uint256[]) private userMintedTokens;
: This line declares a state variableuserMintedTokens
, which is a mapping from addresses to arrays of integers. It will keep track of all the tokens minted by each address. -
mapping(string => bool) private tokenExists;
: This line declares a state variabletokenExists
, which is a mapping from strings to booleans. It will help prevent the minting of the same token URI more than once. -
constructor() public ERC721("MyNFT", "NFT") {}
: This is the constructor of the contract which is called when the contract is deployed. It calls the constructor of the ERC721 token with a name and a symbol. -
mintNFT
is a public function that allows users to mint a new NFT. The function first checks if the tokenURI already exists, increments the tokenIds counter, mints the new token, sets its URI, rewards the user, and keeps track of the newly minted token. -
rewardUser
is an internal function that rewards a user with 1 Ether after minting an NFT. It first checks if the contract has enough balance to reward, then sends the Ether and emits aUserRewarded
event. -
depositETH
is a public payable function that allows the owner to deposit Ether into the contract. -
withdrawETH
is a public function that allows the owner to withdraw all the Ether from the contract. -
totalRewards
is a view function that returns the total rewards received by a user. -
getMintedTokens
is a view function that returns all tokens minted by a user. -
checkTokenExists
is a view function that checks if a token with a given URI already exists. -
event UserRewarded
is an event that gets emitted whenever a user is rewarded.
- Compile the smart contract
truffle compile
The output looks like
Compiling your contracts...
===========================
> Compiling ./contracts/MyNFT.sol
> Compiling @openzeppelin/contracts/access/Ownable.sol
> Compiling @openzeppelin/contracts/token/ERC721/ERC721.sol
> Compiling @openzeppelin/contracts/token/ERC721/IERC721.sol
> Compiling @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol
> Compiling @openzeppelin/contracts/utils/Address.sol
> Compiling @openzeppelin/contracts/utils/Context.sol
> Compiling @openzeppelin/contracts/utils/Counters.sol
> Compiling @openzeppelin/contracts/utils/Strings.sol
> Compiling @openzeppelin/contracts/utils/introspection/ERC165.sol
> Compiling @openzeppelin/contracts/utils/introspection/IERC165.sol
> Compiling @openzeppelin/contracts/utils/math/Math.sol
> Artifacts written to /home/ngu202/workspace/nft-minting-tutorial/build/contracts
> Compiled successfully using:
- solc: 0.8.19+commit.7dd6d404.Emscripten.clang
If there is any error, you should fix it before going to the next step..
- Deploy the smart contract to your local Ethereum Blockchain
truffle migrate
The output looks like
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name: 'development'
> Network id: 1337
> Block gas limit: 6721975 (0x6691b7)
1_mynft_migration.js
====================
Replacing 'MyNFT'
-----------------
> transaction hash: 0x034ae890073b5e61e50a3fa50e30f9505bf363cef503689980e985be143241a0
> Blocks: 0 Seconds: 0
> contract address: 0x17f713aC25039abbfFc34354d3084FC2183b49d5
> block number: 1
> block timestamp: 1685989845
> account: 0x8dE930dbAd0D99759Db57C2F906010f87D4185FF
> balance: 999.93683094
> gas used: 3158453 (0x3031b5)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.06316906 ETH
> Saving artifacts
-------------------------------------
> Total cost: 0.06316906 ETH
Summary
=======
> Total deployments: 1
> Final cost: 0.06316906 ETH
Meanwhile, if you check in the ganache-cli console, there will be a transaction crated as :
Transaction: 0x804493fea685ba2270aa45f372150488edc7d1d349c37e8cf25d03156ca20b4d
Contract created: 0x80d09df430fd965e644ddb8925374f49a73d44e5
Gas usage: 3191316
Block Number: 12
Block Time: Mon Jun 05 2023 18:43:32 GMT+0000 (Coordinated Universal Time)
In which:
- Replacing ‘MyNFT’: This line indicates that the ‘MyNFT’ contract is being deployed. If a contract of the same name was already deployed, it’s being replaced with this new one.
- Transaction hash: This is the unique identifier of the transaction that was generated when the contract was deployed.
- The smart contract is deployed to the address
0x17f713aC25039abbfFc34354d3084FC2183b49d5
- The account
0x8dE930dbAd0D99759Db57C2F906010f87D4185FF
is the owner of the smart contract. This an important value that you would use to interact with your contract. - Block number: This is the number of the block in which the contract deployment transaction was included.
- Block timestamp: The timestamp of when the block was mined.
- Account: The address of the account that deployed the contract.
- Balance: The balance of the account that deployed the contract, after the contract was deployed.
- Gas used: This is the total amount of “gas” that was used in the process. In Ethereum, “gas” refers to the computational work required to do a transaction or smart contract operation.
- Gas price: The price you’re paying for each unit of gas, in gwei. The total transaction cost is gas used times gas price.
- Value sent: The value sent along with the transaction, in this case, is 0 ETH.
- Total cost: The total cost of the transaction, in ETH.
- Saving artifacts: Artifacts are a way of storing contract abstractions for easy use later. An artifact includes important information about the contract like the contract’s deployed address and Application Binary Interface (ABI). The ABI is like a list of methods and variables that you can call on your contract.
- Total deployments: The total number of contract deployments executed in this migration.
- Final cost: The total cost of all contract deployments in this migration.
test the smart contract
truffle test
The output looks like
Using network 'development'.
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Contract: MyNFT
✔ should mint a new NFT (222ms)
✔ should fail when minting a token with a URI that already exists (632ms)
✔ should deposit ETH (60ms)
✔ should withdraw all ETH (117ms)
4 passing (2s)
Deposit ETH to the smart contract
node src/utils/depositEthereum.js
The output looks like
transactionHash: '0xd0523aed266b475b891162382d57ffb564ebcbd32bebd972bbbc0305a8f71dc5',
transactionIndex: 0,
blockHash: '0xefbb664f81346beea248de83d464b242f4768bdfc3fcc42a978f84899531845c',
blockNumber: 2,
from: '0x8de930dbad0d99759db57c2f906010f87d4185ff',
to: '0x17f713ac25039abbffc34354d3084fc2183b49d5',
gasUsed: 21316,
cumulativeGasUsed: 21316,
contractAddress: null,
status: true,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
events: {}
}
Client side
Install dependencies
cd client
npm install
npm start
You can see the dashboard as below:
http://localhost:8080/ipfs/QmZNEyAxoq55x3jZofjmCdqG1ujpYaKh8TGNhLjdkwiHqz
Click on the Mint
button, you will see the transaction is created as below:
References
- https://docs.openzeppelin.com/learn/developing-smart-contracts
- https://docs.openzeppelin.com/learn/deploying-and-interacting