Now its time for you to launch your own NFT collection and store its metadata on IPFS
- There should only exist 10 LearnWeb3 Punk NFT's and each one of them should be unique.
- User's should be able to mint only 1 NFT with one transaction.
- The metadata for the NFT's should be stored on IPFS
- There should be a website for your NFT Collection.
- The NFT contract should be deployed on Mumbai testnet
Lets start building 🚀
- You should have completed the IPFS Theory tutorial
Now its time for us to upload our files to ipfs
We would be using a service called Pinata which will help us to pin content on IPFS
Go to Pinata Dashboard and click on
Uploadand then onFolderDownload the LW3Punks folder to your computer and then upload to it
Pinata, name the folderLW3PunksNow you should be able to see a
CIDfor your folder, Awesome!You can check that it actually got uploaded to IPFS is by opening this up:
https://ipfs.io/ipfs/your-nft-folder-cidreplaceyour-nft-folder-cidwith the CID you recieved from pinata.The images for your NFT's have now been uploaded to IPFS but just having images is not enough, each NFT should also have associated metadata
We will now upload metadata for each NFT to IPFS, each metadata file will be a
jsonfile. Example for metadata of NFT1has been given below:{ "name":"1", "description":"NFT Collection for LearnWeb3 Students", "image":"ipfs://QmQBHarz2WFczTjz5GnhjHrbUPDnB48W5BM2v2h6HbE1rZ/1.png" }Note how "image" has ipfs location in it instead of an
httpsurl. Also note that because you uploaded a folder, you will also need to specify which file within the folder has the correct image for the given NFT. Thus in our case the correct way to specify the location for an NFT image would beipfs://CID-OF-THE-LW3Punks-Folder/NFT-NAME.pngWe have pre-generated files for metadata for you, you can download them to your computer from here, upload these files to pinata and name the folder
metadataNow each NFT's metadata has been uploaded to IPFS and pinata should have generated a CID for your metadata folder
You can check that it actually got uploaded to IPFS is by opening this up:
https://ipfs.io/ipfs/your-metadata-folder-cidreplaceyour-metadata-folder-cidwith the CID you recieved from pinata.Copy this CID and store it on your notepad, you will need this futher down in the tutorial
Its time to write our contract 🥳
We will also be using
Ownable.solfrom Openzeppelin which helps you manage theOwnershipof a contract- By default, the owner of an Ownable contract is the account that deployed it, which is usually exactly what you want.
- Ownable also lets you:
- transferOwnership from the owner account to a new one, and
- renounceOwnership for the owner to relinquish this administrative privilege, a common pattern after an initial stage with centralized administration is over.
We would also be using an extension of ERC721 known as ERC721 Enumerable
- ERC721 Enumerable is helps you to keep track of all the tokenIds in the contract and also the tokensIds held by an address for a given contract.
- Please have a look at the functions it implements before moving ahead
To build the smart contract we would be using Hardhat. Hardhat is an Ethereum development environment and framework designed for full stack development in Solidity. In simple words you can write your smart contract, deploy them, run tests, and debug your code.
To setup a Hardhat project, Open up a terminal and execute these commands
mkdir hardhat-tutorial cd hardhat-tutorial npm init --yes npm install --save-dev hardhatIn the same directory where you installed Hardhat run:
npx hardhat
- Select
Create a Javascript project - Press enter for the already specified
Hardhat Project root - Press enter for the question on if you want to add a
.gitignore - Press enter for
Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)?
- Select
Now you have a hardhat project ready to go!
If you are not on mac, please do this extra step and install these libraries as well :)
npm install --save-dev @nomicfoundation/hardhat-toolboxand press enter for all the questions.
In the same terminal now install
@openzeppelin/contractsas we would be importing Openzeppelin's ERC721Enumerable Contract in ourLW3Punkscontract.npm install @openzeppelin/contracts
Now lets create a new file inside the
contractsdirectory and call itLW3Punks.sol// SPDX-License-Identifier: MITpragma solidity^0.8.4; import"@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import"@openzeppelin/contracts/access/Ownable.sol"; import"@openzeppelin/contracts/utils/Strings.sol"; contractLW3PunksisERC721Enumerable, Ownable { using Stringsforuint256; /** * @dev _baseTokenURI for computing {tokenURI}. If set, the resulting URI for each * token will be the concatenation of the `baseURI` and the `tokenId`. */string _baseTokenURI; // _price is the price of one LW3Punks NFTuint256public _price =0.01ether; // _paused is used to pause the contract in case of an emergencyboolpublic _paused; // max number of LW3Punksuint256public maxTokenIds =10; // total number of tokenIds minteduint256public tokenIds; modifier onlyWhenNotPaused { require(!_paused, "Contract currently paused"); _; } /** * @dev ERC721 constructor takes in a `name` and a `symbol` to the token collection. * name in our case is `LW3Punks` and symbol is `LW3P`. * Constructor for LW3P takes in the baseURI to set _baseTokenURI for the collection. */constructor (stringmemorybaseURI) ERC721("LW3Punks", "LW3P") { _baseTokenURI = baseURI; } /** * @dev mint allows an user to mint 1 NFT per transaction. */function mint() publicpayable onlyWhenNotPaused { require(tokenIds < maxTokenIds, "Exceed maximum LW3Punks supply"); require(msg.value>= _price, "Ether sent is not correct"); tokenIds +=1; _safeMint(msg.sender, tokenIds); } /** * @dev _baseURI overides the Openzeppelin's ERC721 implementation which by default * returned an empty string for the baseURI */function _baseURI() internalviewvirtualoverridereturns (stringmemory) { return _baseTokenURI; } /** * @dev tokenURI overides the Openzeppelin's ERC721 implementation for tokenURI function * This function returns the URI from where we can extract the metadata for a given tokenId */function tokenURI(uint256tokenId) publicviewvirtualoverridereturns (stringmemory) { require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); stringmemory baseURI =_baseURI(); // Here it checks if the length of the baseURI is greater than 0, if it is return the baseURI and attach// the tokenId and `.json` to it so that it knows the location of the metadata json file for a given // tokenId stored on IPFS// If baseURI is empty return an empty stringreturnbytes(baseURI).length>0?string(abi.encodePacked(baseURI, tokenId.toString(), ".json")) : ""; } /** * @dev setPaused makes the contract paused or unpaused */function setPaused(boolval) public onlyOwner { _paused = val; } /** * @dev withdraw sends all the ether in the contract * to the owner of the contract */function withdraw() public onlyOwner { address _owner =owner(); uint256 amount =address(this).balance; (boolsent, ) = _owner.call{value: amount}(""); require(sent, "Failed to send Ether"); } // Function to receive Ether. msg.data must be emptyreceive() externalpayable {} // Fallback function is called when msg.data is not emptyfallback() externalpayable {} }
Now we would install
dotenvpackage to be able to import the env file and use it in our config. Open up a terminal pointing athardhat-tutorialdirectory and execute this commandnpm install dotenv
Now create a
.envfile in thehardhat-tutorialfolder and add the following lines, use the instructions in the comments to get yourALCHEMY_API_KEY_URLandMUMBAI_PRIVATE_KEY.If you dont have Mumbai on MetaMask, you can follow this to add it to your MetaMask, make sure that the account from which you get your mumbai private key is funded with mumbai matic, you can get some from here.// Go to https://www.alchemyapi.io, sign up, create // a new App in its dashboard and selectthe chain as Polygon and network as Mumbai, and replace "add-the-alchemy-key-url-here" with its key url ALCHEMY_API_KEY_URL="add-the-alchemy-key-url-here" // Replace this private key with your MUMBAI account private key // To export your private key from Metamask, open Metamask and // go to Account Details > Export Private Key // Be aware of NEVER putting real Ether into testing accounts MUMBAI_PRIVATE_KEY="add-the-mumbai-private-key-here"
Lets deploy the contract to
mumbainetwork. Create a new file, or replace the existing default one, nameddeploy.jsunder thescriptsfolder. Remember to replaceYOUR-METADATA-CIDwith the CID you saved to your notepad.const{ ethers }=require("hardhat");require("dotenv").config({path: ".env"});asyncfunctionmain(){// URL from where we can extract the metadata for a LW3PunksconstmetadataURL="ipfs://YOUR-METADATA-CID/";/* A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, so lw3PunksContract here is a factory for instances of our LW3Punks contract. */constlw3PunksContract=awaitethers.getContractFactory("LW3Punks");// deploy the contractconstdeployedLW3PunksContract=awaitlw3PunksContract.deploy(metadataURL);awaitdeployedLW3PunksContract.deployed();// print the address of the deployed contractconsole.log("LW3Punks Contract Address:",deployedLW3PunksContract.address);}// Call the main function and catch if there is any errormain().then(()=>process.exit(0)).catch((error)=>{console.error(error);process.exit(1);});
Now open the hardhat.config.js file, we would add the
mumbainetwork here so that we can deploy our contract to mumbai. Replace all the lines in thehardhat.config.jsfile with the given below linesrequire("@nomicfoundation/hardhat-toolbox");require("dotenv").config({path: ".env"});constALCHEMY_API_KEY_URL=process.env.ALCHEMY_API_KEY_URL;constMUMBAI_PRIVATE_KEY=process.env.MUMBAI_PRIVATE_KEY;module.exports={solidity: "0.8.4",networks: {mumbai: {url: ALCHEMY_API_KEY_URL,accounts: [MUMBAI_PRIVATE_KEY],},},};
Compile the contract, open up a terminal pointing at
hardhat-tutorialdirectory and execute this commandnpx hardhat compile
To deploy, open up a terminal pointing at
hardhat-tutorialdirectory and execute this commandnpx hardhat run scripts/deploy.js --network mumbai
Save the LW3Punks contract address that was printed on your terminal in your notepad, you would need it futher down in the tutorial.
To develop the website we would be using React and Next Js. React is a javascript framework which is used to make websites and Next Js is built on top of React.
First, You would need to create a new
nextapp. Your folder structure should look something like- IPFS-Practical - hardhat-tutorial - my-appTo create this
my-app, in the terminal point to NFT-Collection folder and typenpx create-next-app@latest
and press
enterfor all the questionsNow to run the app, execute these commands in the terminal
cd my-app npm run devNow go to
http://localhost:3000, your app should be running 🤘Now lets install Web3Modal library(https://github.com/Web3Modal/web3modal). Web3Modal is an easy-to-use library to help developers add support for multiple providers in their apps with a simple customizable configuration. By default Web3Modal Library supports injected providers like (Metamask, Dapper, Gnosis Safe, Frame, Web3 Browsers, etc), You can also easily configure the library to support Portis, Fortmatic, Squarelink, Torus, Authereum, D'CENT Wallet and Arkane. Open up a terminal pointing at
my-appdirectory and execute this commandnpm install web3modal
In the same terminal also install
ethers.jsnpm install ethers
In your public folder, download this folder and all the images in it the LW3Punks folder. Make sure that the name of the downloaded folder is
LW3PunksNow go to styles folder and replace all the contents of
Home.modules.cssfile with the following code, this would add some styling to your dapp:.main { min-height:90vh; display: flex; flex-direction: row; justify-content: center; align-items: center; font-family:"Courier New", Courier, monospace; } .footer { display: flex; padding:2rem0; border-top:1px solid #eaeaea; justify-content: center; align-items: center; } .image { width:70%; height:50%; margin-left:20%; } .title { font-size:2rem; margin:2rem0; } .description { line-height:1; margin:2rem0; font-size:1.2rem; } .button { border-radius:4px; background-color: blue; border: none; color:#ffffff; font-size:15px; padding:20px; width:200px; cursor: pointer; margin-bottom:2%; } @media (max-width:1000px) { .main { width:100%; flex-direction: column; justify-content: center; align-items: center; } }
Open you index.js file under the pages folder and paste the following code, explanation of the code can be found in the comments.
import{Contract,providers,utils}from"ethers";importHeadfrom"next/head";importReact,{useEffect,useRef,useState}from"react";importWeb3Modalfrom"web3modal";import{abi,NFT_CONTRACT_ADDRESS}from"../constants";importstylesfrom"../styles/Home.module.css";exportdefaultfunctionHome(){// walletConnected keep track of whether the user's wallet is connected or notconst[walletConnected,setWalletConnected]=useState(false);// loading is set to true when we are waiting for a transaction to get minedconst[loading,setLoading]=useState(false);// tokenIdsMinted keeps track of the number of tokenIds that have been mintedconst[tokenIdsMinted,setTokenIdsMinted]=useState("0");// Create a reference to the Web3 Modal (used for connecting to Metamask) which persists as long as the page is openconstweb3ModalRef=useRef();/** * publicMint: Mint an NFT */constpublicMint=async()=>{try{console.log("Public mint");// We need a Signer here since this is a 'write' transaction.constsigner=awaitgetProviderOrSigner(true);// Create a new instance of the Contract with a Signer, which allows// update methodsconstnftContract=newContract(NFT_CONTRACT_ADDRESS,abi,signer);// call the mint from the contract to mint the LW3Punksconsttx=awaitnftContract.mint({// value signifies the cost of one LW3Punks which is "0.01" eth.// We are parsing `0.01` string to ether using the utils library from ethers.jsvalue: utils.parseEther("0.01"),});setLoading(true);// wait for the transaction to get minedawaittx.wait();setLoading(false);window.alert("You successfully minted a LW3Punk!");}catch(err){console.error(err);}};/* connectWallet: Connects the MetaMask wallet */constconnectWallet=async()=>{try{// Get the provider from web3Modal, which in our case is MetaMask// When used for the first time, it prompts the user to connect their walletawaitgetProviderOrSigner();setWalletConnected(true);}catch(err){console.error(err);}};/** * getTokenIdsMinted: gets the number of tokenIds that have been minted */constgetTokenIdsMinted=async()=>{try{// Get the provider from web3Modal, which in our case is MetaMask// No need for the Signer here, as we are only reading state from the blockchainconstprovider=awaitgetProviderOrSigner();// We connect to the Contract using a Provider, so we will only// have read-only access to the ContractconstnftContract=newContract(NFT_CONTRACT_ADDRESS,abi,provider);// call the tokenIds from the contractconst_tokenIds=awaitnftContract.tokenIds();console.log("tokenIds",_tokenIds);//_tokenIds is a `Big Number`. We need to convert the Big Number to a stringsetTokenIdsMinted(_tokenIds.toString());}catch(err){console.error(err);}};/** * Returns a Provider or Signer object representing the Ethereum RPC with or without the * signing capabilities of metamask attached * * A `Provider` is needed to interact with the blockchain - reading transactions, reading balances, reading state, etc. * * A `Signer` is a special type of Provider used in case a `write` transaction needs to be made to the blockchain, which involves the connected account * needing to make a digital signature to authorize the transaction being sent. Metamask exposes a Signer API to allow your website to * request signatures from the user using Signer functions. * * @param {*} needSigner - True if you need the signer, default false otherwise */constgetProviderOrSigner=async(needSigner=false)=>{// Connect to Metamask// Since we store `web3Modal` as a reference, we need to access the `current` value to get access to the underlying objectconstprovider=awaitweb3ModalRef.current.connect();constweb3Provider=newproviders.Web3Provider(provider);// If user is not connected to the Mumbai network, let them know and throw an errorconst{ chainId }=awaitweb3Provider.getNetwork();if(chainId!==80001){window.alert("Change the network to Mumbai");thrownewError("Change network to Mumbai");}if(needSigner){constsigner=web3Provider.getSigner();returnsigner;}returnweb3Provider;};// useEffects are used to react to changes in state of the website// The array at the end of function call represents what state changes will trigger this effect// In this case, whenever the value of `walletConnected` changes - this effect will be calleduseEffect(()=>{// if wallet is not connected, create a new instance of Web3Modal and connect the MetaMask walletif(!walletConnected){// Assign the Web3Modal class to the reference object by setting it's `current` value// The `current` value is persisted throughout as long as this page is openweb3ModalRef.current=newWeb3Modal({network: "mumbai",providerOptions: {},disableInjectedProvider: false,});connectWallet();getTokenIdsMinted();// set an interval to get the number of token Ids minted every 5 secondssetInterval(asyncfunction(){awaitgetTokenIdsMinted();},5*1000);}},[walletConnected]);/* renderButton: Returns a button based on the state of the dapp */constrenderButton=()=>{// If wallet is not connected, return a button which allows them to connect their walletif(!walletConnected){return(<buttononClick={connectWallet}className={styles.button}> Connect your wallet </button>);}// If we are currently waiting for something, return a loading buttonif(loading){return<buttonclassName={styles.button}>Loading...</button>;}return(<buttonclassName={styles.button}onClick={publicMint}> Public Mint 🚀 </button>);};return(<div><Head><title>LW3Punks</title><metaname="description"content="LW3Punks-Dapp"/><linkrel="icon"href="/favicon.ico"/></Head><divclassName={styles.main}><div><h1className={styles.title}>Welcome to LW3Punks!</h1><divclassName={styles.description}> Its an NFT collection for LearnWeb3 students. </div><divclassName={styles.description}>{tokenIdsMinted}/10 have been minted </div>{renderButton()}</div><div><imgclassName={styles.image}src="./LW3punks/1.png"/></div></div><footerclassName={styles.footer}>Made with ❤ by LW3Punks</footer></div>);}
Now create a new folder under the my-app folder and name it
constants.In the constants folder create a file,
index.jsand paste the following code.- Replace
"address of your NFT contract"with the address of the LW3Punks contract that you deployed and saved to your notepad. - Replace
---your abi---with the abi of your LW3Punks Contract. To get the abi for your contract, go to yourhardhat-tutorial/artifacts/contracts/LW3Punks.solfolder and from yourLW3Punks.jsonfile get the array marked under the"abi"key.
exportconstabi=---yourabi---exportconstNFT_CONTRACT_ADDRESS="address of your NFT contract"
- Replace
Now in your terminal which is pointing to
my-appfolder, executenpm run dev
Your LW3Punks NFT dapp should now work without errors 🚀
Make sure before proceeding you have pushed all your code to github :)
We will now deploy your dApp, so that everyone can see your website and you can share it with all of your LearnWeb3 DAO friends.
Go to https://vercel.com/ and sign in with your GitHub
Then click on
New Projectbutton and then select your IPFS-Practical repoWhen configuring your new project, Vercel will allow you to customize your
Root DirectoryClick
Editnext toRoot Directoryand set it tomy-appSelect the Framework as
Next.jsNow you can see your deployed website by going to your dashboard, selecting your project, and copying the
domainfrom there! Save thedomainon notepad, you would need it later.Share your website link with everyone on discord :) and spread happiness.




