# Mock Web3 Project

By [The HMI Guy](https://paragraph.com/@thehmiguy) · 2022-11-12

---

Introduction
------------

I will be discussing the topic of ERC-721 smart contracts and specifically, the ERC-721A contract. The Ethereum Request for Comments (ERC) 721 is the standard for creating non-fungible tokens (NFTs). As the name suggests, NFTs are unique and cannot be replaced by another token. In contrast, fungible items such as Bitcoin, US dollars, and gold are interchangeable. The ERC-721 was proposed in January 2018 by William Entriken, Dieter Shirley, Jacob Evans, and Nastassia Sachs. Since then, many developers and projects have adopted this standard to create NFTs.

ERC-721A
--------

A notable implementation of the ERC-721 standard is the ERC-721A contract, created by the developer [cygaar](https://twitter.com/0xcygaar?s=21&t=ttRiELFYD_flRe0EULyhGw) for the Azuki NFT project. The savings can be observed by comparing the ERC-721A contract to the standard ERC-721 contract from OpenZeppelin. As a reference, a table from cygaar's [article](https://www.azuki.com/erc721a) has been included to demonstrate the potential savings.

![ERC-721A Gas Savings](https://storage.googleapis.com/papyrus_images/5807c2badd1bcac00e325172e70ef18194ccb3109ebddd50ca39df646a88d4a1.png)

ERC-721A Gas Savings

Project Overview
----------------

As a professional with a keen interest in web3 technology, I have decided to undertake a mock project that incorporates an ERC-721A smart contract, an art engine, and a decentralized application (dApp). The smart contract will feature the following:

*   The ERC-721A standard
    
*   Libraries from OpenZeppelin including Strings, Ownable, Merkle Proof, and Reentrancy Guard
    
*   Standard minting, whitelist minting, and airdropping functions
    
*   Three internal modifiers
    
*   Minting and metadata control
    
*   A Merkle Tree for whitelist control
    
*   The Hardhat framework for compiling, unit testing, and deploying the contract.
    

The art engine for this project has been developed by [HashLips](https://twitter.com/hashlipsnft?s=21&t=KZmZXG0smlDBLxYvctrwAA) and the [Sketchy Labs](https://hashlips.online/HashLips) team. It allows developers to customize the entire NFT collection in a single codebase, including rarity, metadata, and collection generation. I would highly recommend reviewing the [GitHub](https://github.com/HashLips/hashlips_art_engine) and [YouTube](https://www.youtube.com/watch?v=vFY_E3IP6OU&list=PLvfQp12V0hS1PWDxlrfASk0Mq6AbC5n5f) channel of HashLips and the Sketchy Labs team. It should be noted that this codebase has been utilized by multiple projects in the industry.

The dApp frontend is planned to be built using React, and it will enable users to mint from the collection. During the whitelist minting phase, users will be required to provide their wallet's Merkle proof. To enhance the user experience, I will implement a mechanism to retrieve the user's wallet from Metamask, pass it to the Merkle tree, and check if the user is whitelisted. If so, the proof will be sent to the dApp without any further action from the user. Upon clicking the mint button, the proof and the mint amount will be sent to the whitelist mint function. This feature is crucial for the overall functionality of the dApp.

Smart Contract
--------------

In order to begin, it is important to ensure that Node.js and npm are properly installed on your system. To verify this, open the terminal and enter the command `node -v` and `npm -v`. If Node.js is not present, please follow the instructions for installation provided in the Node.js documentation. Similarly, if npm is not installed, please refer to the npm documentation for installation instructions.

Once Node.js and npm have been confirmed as properly installed, initialize an npm project by running `npm init` in the terminal. This will create a package.json file, which will be used to manage project dependencies, scripts, and other relevant information.

To proceed, install the [Hardhat](https://hardhat.org/hardhat-runner/docs/getting-started#overview) tooling framework by running `npm install --save-dev hardhat` in the terminal. Once installed, navigate to the directory where Hardhat is located and run `npx hardhat`. When prompted, select the option to "`Create a JavaScript project` and use the default settings for the remaining options. Hardhat provides a variety of tools, but for this project, we will be using the plugin recommended by Hardhat, `npm install --save-dev @nomaticfoundation/hardhat-toolbox`.

    require("@nomicfoundation/hardhat-toolbox");
    
    /** @type import('hardhat/config').HardhatUserConfig */
    module.exports = {
      solidity: "0.8.9",
    };
    

To gain access to the OpenZeppelin libraries, run `npm install @openzeppelin/contracts` in the terminal. Additionally, we will be using the ERC-721A contract created by cygaar, which can be obtained by running `npm install --save-dev erc721`.

Navigate to the `contracts` folder and open the solidity file. At this time, feel free to rename the file to a more appropriate name for the project. For this project, I have chosen to name it "RockPaperScissors" as the NFTs will be based on this theme. Inside the solidity file, import the necessary libraries as the contract will inherit them.

    
    SPDX-License-Identifier: MIT
    
    pragma solidity >=0.8.9 <0.9.0;
    
    import "erc721a/contracts/extensions/ERC721AQueryable.sol";
    import "@openzeppelin/contracts/utils/Strings.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
    import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
    
    import "hardhat/console.sol";
    
    contract RockPaperScissors is ERC721AQueryable, Ownable, ReentrancyGuard 
    

It is important to note that I have chosen to make the `merkleRoot` variable immutable, which means it is passed in during the initialization of the contract. This approach has a few drawbacks, such as the whitelist must be finalized prior to deploying the contract, and any changes to the whitelist will cause a change in the Merkle tree, resulting in a different hash of the root. The reason for this decision is to provide users with confidence that the whitelist will not change once the contract is deployed. If the ability to change the Merkle root is desired, the `immutable` keyword can be removed and a function can be created to update the Merkle root when called by the owner.

The `paused`, `whitelistMintEnabled`, and `revealed` variables allow for control over the different phases of the project. The `pause` variable should be set to "true" upon deployment to prevent users from accessing the mint function and minting tokens from the collection. The `whitelistMintEnabled` variable should be set to `false` to prevent whitelisted users from accessing the whitelist mint function until the whitelist period has begun. The `revealed` variable should also be set to `false` to prevent users from accessing the metadata of the NFTs.

The `whitelistClaimed` mapping is used to keep track of the number of tokens that have been minted by whitelisted users. The `address` is mapped to a `uint` instead of the traditional `boolean` option, which allows for the flexibility to mint the limit in separate transactions.

    
      // =============================================================
      //                          CONSTANTS
      // =============================================================
      uint public constant MAX_SUPPLY = 777;
    
      // =============================================================
      //                          VARIABLES
      // =============================================================
      uint256 public immutable maxMintAmountPerTx;
      bytes32 public immutable merkleRoot;
    
      uint public price;
      uint public wlPrice;
    
      bool public paused = true;
      bool public whitelistMintEnabled = false;
      bool public revealed = false;
    
      mapping(address => uint) public whitelistClaimed;
    
      // =============================================================
      //                          METADATA
      // =============================================================
      string public uriPrefix = "";
      string public uriSuffix = ".json";
      string public hiddenMetadataUri;
    
      // =============================================================
      //                          CONSTRUCTOR
      // =============================================================
      constructor(
        uint256 _maxMintAmountPerTx,
        uint _price,
        uint _wlprice,
        string memory _hiddenMetadataUri,
        bytes32 _merkleRoot
      ) ERC721A("RockPaperScissors", "RPS") {
        maxMintAmountPerTx = _maxMintAmountPerTx;
        setPrice(_price);
        setWLPrice(_wlprice);
        setHiddenMetadataUri(_hiddenMetadataUri);
        merkleRoot = _merkleRoot;
      }
    

The constructor of the contract is quite straightforward. Upon deployment, four arguments are passed to the contract, which are used to set the state variables or call setters that write to the state variables. The name and symbol of the project's token are passed directly to the ERC721A constructor.

Additionally, three internal modifiers have been created to be used in some of the functions within the contract. The `callerIsUser` modifier is used to prevent a contract from calling the function. This is implemented to help prevent phishing attacks. For more information on the subject, David has written an article that explains the details about `tx.origin` and `msg.sender`. The image below illustrates the difference between the two.

![tx.origin vs msg.sender](https://storage.googleapis.com/papyrus_images/50e6e018d00a6d854b8417077dc9f2610ec2d8bc8fdfe38b4b52d3a46e6032c4.png)

tx.origin vs msg.sender

The `mintCompliance` modifier is used to ensure that the mint amount is greater than 0 and less than or equal to the maximum batch per transaction. This helps to ensure that the minting process is conducted in a compliant manner.

Similarly, the `mintPriceCompliance` modifier is used to ensure that the value sent is greater than or equal to the price (price \* mint amount). This helps to ensure that the minting process is conducted in a financially compliant manner.

By implementing these three modifiers, the code is kept clean and organized, as the `require` statements do not need to be repeated in various functions. This improves the readability and maintainability of the code.

      // =============================================================
      //                          INTERNAL MODIFIERS
      // =============================================================
    
      /**
       * @dev Ensure a Smart Contract is not interfacing with this contract.
       * msg.sender can either be an account address or smart contract address, while
       * tx.origin will always be the account/wallet address.
       */
      modifier callerIsUser() {
        require(tx.origin == msg.sender, "The caller is another contract");
        _;
      }
      modifier mintCompliance(uint256 _mintAmount) {
        require(
          _mintAmount > 0 && _mintAmount <= maxMintAmountPerTx,
          "Invalid mint amount!"
        );
        require(totalSupply() + _mintAmount <= MAX_SUPPLY, "Max supply exceeded!");
        _;
      }
    
      modifier mintPriceCompliance(uint256 _mintAmount) {
        require(msg.value >= price * _mintAmount, "Insufficient funds!");
        _;
      }
    

The `mint` function is relatively simple in its implementation. When called, the function first checks that the conditions set by the modifiers are met. If they are, the function proceeds to check if the contract is currently paused. If the contract is not paused, the `_safeMint` function is called and the transaction is completed. It is worth noting that the `_safeMint` function includes additional checks that are not covered in the scope of this article.

The `whitelistMint` function is passed the number of tokens the user wishes to mint, as well as proof associated with their wallet. As with the "mint" function, the conditions set by the modifiers are checked before proceeding with the function. If the conditions are met, the function checks if whitelist minting is enabled. Next, it checks if the user has already claimed their allotted number of tokens. Assuming these conditions are met, the `leaf` variable is assigned a hashed value of the user's address. The "MerkleProof" function's "verify" function is then called, passing in three arguments: `_merkleProof` (which is currently passed manually by the user, but will eventually be handled by the dApp), `merkleRoot` (set during deployment), and `leaf` (hashed value of the user's address). The "verify" function returns "true" if the `leaf` can be proven to be a part of the Merkle tree defined by `merkleRoot`. If the function returns "true", the number of tokens minted is added to the user's mapping and the `_safeMint` function is called. Additional information regarding the creation of a Merkle tree will be provided later in this section.

The `airdrop` function is similarly simple in implementation. As with the previous functions, the function begins by checking the conditions set by the `onlyOwner` and `callerIsUser`. In most cases, the airdrop function should only be called by the owner. The "airdrop" function can be used for marketing purposes or to distribute tokens to the development team. After the conditions set by the modifiers are met, the function checks that the total supply of minted tokens plus the amount being airdropped is less than the maximum supply. If this condition is met, the `_safeMint` function is called.

      // =============================================================
      //                          INTERNAL MINT LOGIC
      // =============================================================
    
      function mint(
        uint256 _mintAmount
      )
        public
        payable
        callerIsUser
        mintCompliance(_mintAmount)
        mintPriceCompliance(_mintAmount)
      {
        require(!paused, "The contract is paused!");
    
        _safeMint(msg.sender, _mintAmount);
      }
    
      function whitelistMint(
        uint256 _mintAmount,
        bytes32[] calldata _merkleProof
      )
        public
        payable
        callerIsUser
        mintCompliance(_mintAmount)
        mintPriceCompliance(_mintAmount)
      {
        require(whitelistMintEnabled, "The whitelist sale is not enabled!");
        require(whitelistClaimed[msg.sender] < 2, "Address already claimed!");
        bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
        require(
          MerkleProof.verify(_merkleProof, merkleRoot, leaf),
          "Invalid proof!"
        );
    
        whitelistClaimed[msg.sender] += _mintAmount;
        _safeMint(msg.sender, _mintAmount);
      }
    
      function airdrop(
        address wallet,
        uint256 amount
      ) external onlyOwner callerIsUser {
        require(totalSupply() + amount < MAX_SUPPLY + 1, "Exceed max supply!");
    
        _safeMint(wallet, amount);
      }
    

Since I mentioned a few of these variables earlier, I thought I would show their setter functions. Again, pretty straightforward. Call the function, satisfy the modifier, and set the state variable with the argument that was passed.

      // =============================================================
      //                          SETTERS
      // =============================================================
      function setPrice(uint _price) public onlyOwner {
        price = _price;
      }
    
      function setWLPrice(uint _wlprice) public onlyOwner {
        wlPrice = _wlprice;
      }
    
      function setPaused(bool _state) public onlyOwner {
        paused = _state;
      }
    
      function setWhitelistMintEnabled(bool _state) public onlyOwner {
        whitelistMintEnabled = _state;
      }
    
      function setRevealed(bool _state) public onlyOwner {
        revealed = _state;
      }
    

Merkle Tree
-----------

In order to proceed with creating a Merkle tree, it is necessary to install the "keccak256" and "merkletreejs" packages via npm. This can be done by running `npm install keccak256 and merkletreejs`in the terminal. Once these packages have been installed, it is recommended to create a folder structure for this part of the project. In this example, a folder called "utils" is created, which will contain two files named "addresses.json" and "merkletree.js". The "addresses.json" file will store the array of addresses that should be whitelisted.

Inside the terminal, run `npm install keccak256 and merkletreejs`. Once installed, create a folder structure for this portion of the project. In this example, I created a folder called `utils` with two files called `addresses.json` and `merkletree.js`. Inside the `addresses.json` file, I store the array of addresses that should be whitelisted.

    [  "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",  "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",  "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc",  "0x976EA74026E726554dB657fA54763abd0C3a0aa9",  "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",  "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f",  "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",  "0xBcd4042DE499D14e55001CcbB24a551F3b954096",  "0x71bE63f3384f5fb98995898A86B02Fb2426c5788",  "0xFABB0ac9d68B0B445fB7357272Ff202C5651694a",  "0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec",  "0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097",  "0xcd3B766CCDd6AE721141F452C550Ca635964ce71",  "0x2546BcD3c84621e976D8185a91A922aE77ECEc30",  "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E",  "0xdD2FD4581271e230360230F9337D5c0430Bf44C0",  "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199",  "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",  "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]
    

Inside the merkletree.js file, add the following code. We create two variables for the installs we did earlier. Next, we store the addresses from our `.json` inside the variable called `addresses`. Now we create our Merkle tree _(leaves, tree, root, leaf, and proof)_. As I mentioned earlier, without the dApp handling the proof, we have to manually do it. Line 8, I specify which address in the array I want to be hashed and assigned to leaf. Line 9, I call the getProof, which returns and assigns the hashed values of the neighbor leaves and all parent nodes. This data is important because it allows us to derive the Merkle tree root hash. The commented lines can be used for testing valid and invalid proofs. If you want to learn more, [Peter Blockman](https://twitter.com/peterblockman?s=21&t=KZmZXG0smlDBLxYvctrwAA) created a great [article](https://dev.to/peterblockman/understand-merkle-tree-by-making-a-nft-minting-whitelist-1148) explaining Merkle trees for NFT whitelist minting.

    const { MerkleTree } = require("merkletreejs");
    const keccak256 = require("keccak256");
    const addresses = require("./addresses.json");
    
    const leaves = addresses.map((x) => keccak256(x));
    const tree = new MerkleTree(leaves, keccak256, { sort: true });
    const root = tree.getRoot().toString("hex");
    const leaf = keccak256(addresses[18]);
    const proof = tree.getProof(leaf);
    
    // console.log(tree.verify(proof, leaf, root)); // true
    
    // const badLeaves = addresses.map((x) => keccak256(x));
    // const badTree = new MerkleTree(badLeaves, keccak256);
    // const badLeaf = keccak256("0xAb8483F64d9C6d1EcF9b849Ae677dD3315835c69");
    // const badProof = badTree.getProof(badLeaf);
    // console.log(badTree.verify(badProof, badLeaf, root)); // false
    // console.log(tree.toString());
    console.log("0x" + tree.getRoot().toString("hex"));
    
    const buf2hex = (x) => "0x" + x.toString("hex");
    const AddressProof = tree.getProof(leaf).map((x) => buf2hex(x.data));
    console.log(AddressProof);
    

Unit Testing
------------

A powerful feature of Hardhat is the ability to unit test your contract. Below is an example of some of the unit testing I wrote for this contract. The entire file is 400 lines of code. Unit testing is extremely important for smart contracts. Remember, once deployed to the blockchain, you cannot undo it! Contracts will handle funds, which means you should be extremely cautious before deploying a contract into production. Unit testing syntax and explanations can be found [here.](https://docs.ethers.io/v5/) After writing your unit test, it’s time to test them by running `npx hardhat test`. The result of each test will be written to the output window of your terminal. See the image below.

    const { expect, assert } = require("chai");
    const { ethers } = require("hardhat");
    
    // =============================================================
    //                          SMART CONTRACT - UNIT TESTING
    // =============================================================
    describe("RPS - Unit Testing", function () {
      let RockPaperScissors,
        rockPaperScissorsContract,
        owner,
        addr1,
        addr2,
        addr3,
        addrs;
      this.beforeEach(async function () {
        RockPaperScissors = await ethers.getContractFactory("RockPaperScissors");
        [owner, addr1, addr2, addr3, ...addrs] = await ethers.getSigners();
        rockPaperScissorsContract = await RockPaperScissors.deploy(
          5,
          ethers.BigNumber.from("1000000000000000000"), //1ETH
          ethers.BigNumber.from("1000000000000000000"), //1ETH
          "ipfs://QmZD66sq4Em1xQKSchg45q2AtTf8qts5LZMPx3kCqPYpQg/hidden.png",
          ethers.BigNumber.from(
            "0x04c8b7dc04c57c4e8e83fff68c0e603c2871484788a158e8c63854ecd5d256ee"
          )
        );
      });
      // =============================================================
      //                          DEPLOY
      // =============================================================
      describe("Deployment", function () {
        it("Should set the right owner", async function () {
          expect(await rockPaperScissorsContract.owner()).to.equal(owner.address);
        });
      });
      // =============================================================
      //                          MINT
      // =============================================================
      describe("mint", function () {
        it("Should revert because _mintAmount is 0", async function () {
          await rockPaperScissorsContract.connect(owner).setPaused(false);
          expect(await rockPaperScissorsContract.paused()).to.equal(false);
          const expectedValue = 0;
          const overrides = {
            value: ethers.utils.parseEther("0.1"),
          };
    
          await expect(
            rockPaperScissorsContract.connect(owner).mint(expectedValue, overrides)
          ).to.be.revertedWith("Invalid mint amount!");
        });
        it("Should revert because insufficient funds were sent", async function () {
          await rockPaperScissorsContract.connect(owner).setPaused(false);
          expect(await rockPaperScissorsContract.paused()).to.equal(false);
          const expectedValue = 1;
          const overrides = {
            value: ethers.utils.parseEther("0.1"),
          };
    
          await expect(
            rockPaperScissorsContract.connect(owner).mint(expectedValue, overrides)
          ).to.be.revertedWith("Insufficient funds!");
        });
        it("Should revert because contract is paused", async function () {
          const expectedValue = 1;
          const overrides = {
            value: ethers.utils.parseEther("5"),
          };
    
          await expect(
            rockPaperScissorsContract.connect(owner).mint(expectedValue, overrides)
          ).to.be.revertedWith("The contract is paused!");
        });
        it("Should mint x tokens", async function () {
          await rockPaperScissorsContract.connect(owner).setPaused(false);
          expect(await rockPaperScissorsContract.paused()).to.equal(false);
          const expectedValue = 1;
          const overrides = {
            value: ethers.utils.parseEther("5"),
          };
    
          await rockPaperScissorsContract
            .connect(owner)
            .mint(expectedValue, overrides);
        });
      });
    

![Output Example](https://storage.googleapis.com/papyrus_images/fb1950954bc366ac56dbf343af880aafd37550541c2e908fbd412f973652d2c0.png)

Output Example

Art Engine
----------

To generate the NFTs for this project, I will be using Hashlip’s Art Engine. The GitHub repository for the Art Engine can be found [here](https://github.com/HashLips/hashlips_art_engine). I will not cover every step, as Daniel does a great job covering them in his YouTube video.

This art engine allows you to turn image layers into thousands of unique code-generated artworks. Once the repo has been downloaded, head into the `config.js` file and look for the following variables (`namePrefix`, `description`, `growEditionSizeTo`, and `layersOrder`). Modify these variables to meet your project needs. Once modified, `cd` into the art engine’s directory (i.e. `cd hashlips_art_engine-1.1.2_patch_v6`)and run `npm run generate`. This script will run the `index.js` file, which calls the `main.js` file. This file does most of the generative work. In the image below, the codebase starts to generate each image’s metadata.

![npm run generate](https://storage.googleapis.com/papyrus_images/2fd352c0e5ededb008573119eb7746a3a586cec009d39dbdb14f2cb32b0f1770.png)

npm run generate

Once finished, head to the `build` folder and review the `images` and `json` folders. Select a `.json` file and review the contents. The variables from the `config.js` have been applied to the collection, which will be shown on the marketplace later in this article.

![json Metadata](https://storage.googleapis.com/papyrus_images/c361a1f0c86ca4db3868b84cb28947cac80cf9fdc0669c03d7262e87f2273188.png)

json Metadata

Now that our collection’s metadata has been built, we need to upload our collection to IPFS. IPFS or InterPlantery File System is a P2P network for storing and sharing data in a distributed file system. IPFS uses content-addressing to uniquely identify each file in a global namespace connecting IPFS hosts. An overview of the protocol can be found [here](https://docs.ipfs.tech/concepts/), as well as how to install the [desktop](https://docs.ipfs.tech/install/) application. In summary, most NFTs are stored as metadata that points to an image off-chain where the actual file is hosted.

Once installed, navigate to the `Files` section and click `+import`. Select the `Folder` option in the drop-down and select the `images` folder for the collection. Repeat the same steps for the `.json` folder.

![IPFS Desktop](https://storage.googleapis.com/papyrus_images/9bbe10d6fdd4be565332400c088f8959bf514a89e53b3bbe11bf6c21777a6260.png)

IPFS Desktop

Click `…` for the collection’s metadata (.json) and select `Copy CID`. The `CID` identifier will be passed to our Smart Contract in a later section.

When files are uploaded to IPFS, the file is run through a cryptographic algorithm that gives you something called the Content Identifier, or `CID`. Every `CID` is determined by the content of the file, making it completely unique. This allows content to be verifiable since two images would have completely different `CIDs`. Combine this power with the blockchain, and you get a reference to an image that is verified and cannot be changed.

![Copy CID](https://storage.googleapis.com/papyrus_images/7ecf99e9fc5156195f70ac9cfde4baf422c64b4ad7563e9b97a3e6e31a0495a2.png)

Copy CID

Utilizing Etherscan’s ability to communicate to our contract, connect your Metamask wallet and write to the `uriPrefix` function. Before pasting in the `CID`, type `ipfs://` in front of the string. After writing to the function, head to the `Read Contract` tab and see the value of the `uriPrefix` variable. It should look similar to the image below but with your unique `CID`.

![uriPrefix](https://storage.googleapis.com/papyrus_images/a410a28baea11eaf59a3361798e30289a882bbe6eac11ea1030a1d0502230ebe.png)

uriPrefix

Now that our contract has the metadata needed, let’s review how marketplaces use this data to display the NFT image, description, and properties. Markplaces use the properties of the collection to determine a percentage of trait, which can be used to determine rarity of an NFT. When creating layers, keep in mind that the names for each layer folder and individual images are used for the metadata and will be displayed on each NFT.

![Metadata Example](https://storage.googleapis.com/papyrus_images/208832e4641ef0597d9dea9d679c02f7444656ed928a93220e552501f2d30c23.png)

Metadata Example

As the contract owner, you can set the variable `revealed` to `true`, which will use the `uriPrefix` and `uriSuffix`. If the `revealed` variable is set to `false`, the owner can use a default image for all tokens until the collection is ready to be revealed. Before revealing the RPS collection, all tokens pointed to the variable `hiddenMetadataUri`, which pointed the tokens to a CID of the image below.

![Default Collection Image](https://storage.googleapis.com/papyrus_images/084afbdfebf716cd1d8e97ef8b9bb959719445b4c2a37d4dadfad714961a3b56.png)

Default Collection Image

dApp
----

For the dApp, I will be using [Thirdweb](https://portal.thirdweb.com/platform-overview)’s [React](https://reactjs.org/docs/getting-started.html) SDK for the frontend. To get started run `npx thirdweb@latest create app --evm` in your terminal. You will be asked to give your project a name, framework _(React)_, and language _(JavaScript)_. Once installed, `cd` into the react folder _(project name)_ and run `npm run start` to spin up a local instance of the application.

![Local React Instance](https://storage.googleapis.com/papyrus_images/fb4362309ed3b6f3103068a86dc3a0e810380a373139e8e01fd00f0e78747068.png)

Local React Instance

Inside the `index.js` file, add your `ChainId` _(network)_ that your contract is deployed to. For this project, the contract was deployed to the `Goerli` network.

![Setting Chain ID](https://storage.googleapis.com/papyrus_images/70368a4b2bb8d3d2160f6f83493e16c58e1cf1c3c649b1eaa43cb48bd7c9f748.png)

Setting Chain ID

Now, it’s time to start reading data from our contract on the Goerli network. Create a folder inside of the `src` folder called `components`. Inside the `components` folder, create a file called `mint.js`. Utilizing Thirdweb’s React hook `useContractRead`, we can read a function, view, or mapping. For this example, I will be reading the contract’s `price`, `paused`, and `whitelistEnabled` functions. Using variables to store the value, allows the developer to display the state on the dApp _(later section)_.

![useContractRead Examples](https://storage.googleapis.com/papyrus_images/03b35731f59ada47aaf855202c65040ede0ab9c0ef1a9f73ef6eeaf9bd11e4b4.png)

useContractRead Examples

Let’s start to display the data from our contract onto the dApp.

First, we’ll want to display the price of the NFT. To do so, we can use the `price` variable, which was declared and stored in the earlier section. Once added, we need to format the units from wei to ether and show a default message to the user.

Next, we should display the state of the contract to the user and developer. This should include whether the contract is paused and if the whitelist is enabled.

Lastly, after the user has connected their wallet and selected the right network, we should display the `Mint` button, which will mint one NFT.

![Displaying Data](https://storage.googleapis.com/papyrus_images/9b154c156c27ea6afc5e2e5adc6dd8f5307abd108067011049f1182a4f5e35f7.png)

Displaying Data

Using some basic styling, here’s an example of what the dApp will look like.

![dApp Overview](https://storage.googleapis.com/papyrus_images/fc9161bd996adea766e116896bab39fa17024e0d43eee1029b288b9133da0649.png)

dApp Overview

Etherscan Contract
------------------

[https://goerli.etherscan.io/address/0x58a56731D3177eeC6e395B4397c00F6E1A1436a8#code](https://goerli.etherscan.io/address/0x58a56731D3177eeC6e395B4397c00F6E1A1436a8#code)

---

*Originally published on [The HMI Guy](https://paragraph.com/@thehmiguy/mock-web3-project)*
