Polygon: Building an App NFT With Polygon

Learn how to build an app NFT with Polygon and IPFS.

What is Polygon?

Polygon is a layer two blockchain network that is designed as a sidechain solution for scaling alongside the Ethereum blockchain network.

Read below to learn how to build an app NFT with Polygon and IPFS.

Prerequisites:

1. Start by creating a new directory for your NFT app, then navigating into that directory with the commands:

mkdir nft-app-contract && cd nft-app-contract

2. Next, initialize NPM and install HardHat with the commands:

npm init

npm install --save-dev hardhat

npx hardhat

3. Install the OpenZeppelin smart contracts package:

npm install @openzeppelin/contracts

4. In the contracts folder, there will be a default contract that was created. Rename this file dAppNFT.sol, then open the file. Replace the existing content with the following:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract AppNFT is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter public versions;
    mapping(uint256 => string) public builds;
    address public appOwner;

    constructor(string memory tokenURI) ERC721("AppNFT", "APP") {
        appOwner = msg.sender;
        mint(tokenURI);
    }

    function updateApp(string memory newTokenURI) public {
        require(
            msg.sender == appOwner,
            "Only the app owner can make this change"
        );
        uint256 currentVersion = versions.current();
        _setTokenURI(1, newTokenURI);
        builds[currentVersion + 1] = newTokenURI;
        versions.increment();
    }

    function getPreviousBuild(uint256 versionNumber)
        public
        view
        returns (string memory)
    {
        return builds[versionNumber];
    }

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        require(
            _isApprovedOrOwner(_msgSender(), tokenId),
            "ERC721: caller is not token owner nor approved"
        );

        _transfer(from, to, tokenId);
        appOwner = to;
    }

    function mint(string memory tokenURI) private returns (uint256) {
        versions.increment();
        uint256 tokenId = 1;
        uint256 currentVersion = versions.current();
        _mint(msg.sender, tokenId);
        _setTokenURI(tokenId, tokenURI);
        builds[currentVersion] = tokenURI;
        return tokenId;
    }
}

5. Save this file.

Then in the root directory there will be another folder called test. Rename the existing file in the test folder as dAppNFT.js. Replace everything in that file with the following:

const { expect } = require("chai");
const URI = "ipfs://QmTXCPCpdruEQ5HspoTQq6C4uJhP4V66PE89Ja7y8CEJCw";
const URI2 = "ipfs://QmTXCPwpdruEQ5HBpoTQq6C4uJhP4V66PE89Ja7y8CEJC2"

describe("AppNFT", function () {
  async function deploy() {   
    const [owner, otherAccount] = await ethers.getSigners();

    const AppNFT = await ethers.getContractFactory("AppNFT");
    const appNft = await AppNFT.deploy(URI);
    await appNft.deployed();
    return appNft;
  }

  describe("Deployment", function () {
    it("Should deploy contract and mint", async function () {
      const appNft = await deploy();
      const uri = await appNft.tokenURI(1)
      expect(uri).to.equal(URI);
    });

    it("Should set the right version number", async function () {
      const appNft = await deploy();
      const versions = await appNft.versions()
      expect(versions).to.equal(1);
    })

    it("Should return correct URI based on version", async function() {
      const appNft = await deploy();
      const buildURI = await appNft.getPreviousBuild(1);
      expect(buildURI).to.equal(URI);
    })

    it("Should not allow minting additional tokens", async function() {
      const appNft = await deploy();
      let err;
      try {
        await appNft.mint(URI);
      } catch (error) {
        err = error.message;
      }

      expect(err).to.equal("appNft.mint is not a function");      
    })
  });

  describe("Versions", function () {
      it("Should allow the app owner to update versions", async function () {
        const appNft = await deploy();
        const uri = await appNft.tokenURI(1)
        expect(uri).to.equal(URI);
        await appNft.updateApp(URI2);
        const uri2 = await appNft.tokenURI(1)
        expect(uri2).to.equal(URI2);
      });

      it("Should show correct current version", async function () {
        const appNft = await deploy();
        const uri = await appNft.tokenURI(1)
        expect(uri).to.equal(URI);
        await appNft.updateApp(URI2);
        const uri2 = await appNft.tokenURI(1)
        expect(uri2).to.equal(URI2);
      });

      it("Should not allow someone who is not the app owner to update versions", async function() {
        const appNft = await deploy();
        const uri = await appNft.tokenURI(1)
        expect(uri).to.equal(URI);
        const [owner, otherAccount] = await ethers.getSigners();
        let err;
        try {
          await appNft.connect(otherAccount).updateApp(URI2);
          const uri2 = await appNft.tokenURI(1)
          expect(uri2).to.equal(URI2); 
        } catch (error) {
          err = error.message;                    
        }        
        expect(err).to.equal("VM Exception while processing transaction: reverted with reason string 'Only the app owner can make this change'");
      })
  });

  describe("Transfers", function () {
    it("Should not allow transfers from non owner and non approved", async function() {
      const appNft = await deploy();
      const [owner, otherAccount] = await ethers.getSigners();
      let err;
      try {
        await appNft.connect(otherAccount).transferFrom(owner.address, otherAccount.address, 1);
      } catch (error) {
        err = error.message;
      }

      expect(err).to.equal("VM Exception while processing transaction: reverted with reason string 'ERC721: caller is not token owner nor approved'");      
    });

    it("Should allow transfers from owner to another address", async function() {
      const appNft = await deploy();
      const [owner, otherAccount] = await ethers.getSigners();
      await appNft.transferFrom(owner.address, otherAccount.address, 1);   
      expect(await appNft.appOwner()).to.equal(otherAccount.address);
      expect(await appNft.ownerOf(1)).to.equal(otherAccount.address);
    });

    it("Should allow transfer from non-owner if address is approved", async function() {
      const appNft = await deploy();
      const [owner, otherAccount] = await ethers.getSigners();
      await appNft.approve(otherAccount.address, 1);   
      await appNft.connect(otherAccount).transferFrom(owner.address, otherAccount.address, 1);
      expect(await appNft.appOwner()).to.equal(otherAccount.address);
      expect(await appNft.ownerOf(1)).to.equal(otherAccount.address);
    })
  })
});

6. Then, compile the project with the command:

npx hardhat test

7. Next, we’ll need to create the front end of our application. To do this, run the following command:

npx create-react-app app-nft-frontend

8. To configure IPFS, first we need to open the package.json file. In this file, add the following key/value pair:

"homepage": "./"

9. Next, install the AWS CLI tool.

10. Then, configure AWS CLI to use your Filebase account. To do this, open a new terminal window. From there, run the command:

aws configure

This command will generate a series of prompts, which should be filled out as such:

  • Access Key ID: Filebase Access Key

  • Secret Access Key: Filebase Secret Key

  • Region: us-east-1

11. Open the package.json file again. In the scripts section, add a new script called deploy:

"deploy": "npm run build && sh ./upload.sh"

This line includes a script called upload.sh that we haven’t created yet. We’ll create that next.

12. In the root of the project create the file upload.sh. Open this new file in a text editor, then add the following line:

aws --endpoint https://s3.filebase.com s3 sync ./build s3://bucket-name

Replace bucket-name with your Filebase IPFS bucket name.

13. Next, run the following command:

npm run deploy

After running this command, the build script will be initiated, followed by the build folder being uploaded to your Filebase bucket.

14. Navigate to the Filebase dashboard and view the bucket that your build folder was uploaded to.

Take note of this folder’s IPFS CID value:

15. Next, navigate to the smart contract directory in your project and find the hardhat.config.js file.

Replace the existing content with the following:

require("@nomicfoundation/hardhat-toolbox");
module.exports = {
  solidity: "0.8.9",
};
module.exports = {
  solidity: "0.8.9",
  networks: {
    mumbai: {
      url: `https://polygon-mumbai.g.alchemy.com/v2/ALCHEMY_KEY`,
      accounts: ["POLYGON MUMBAI WALLET PRIVATE KEY"]
    }
  }
};

In this file, you will need to replace two values:

  • ALCHEMY_KEY: Your Alchemy API key. For instructions on how to get this value, see this guide.

  • POLYGON MUMBAI WALLET PRIVATE KEY: Polygon wallet private key. For instructions on how to get this value, see this guide.

16. Next, create a file in the root of your project called metadata.json. In this file, insert the following content:

{
"name": "dApp NFT",
"description": "A full-stack decentralized application that can be sold and transferred as an NFT.",
"image": "ipfs://CID",
"animation_url": "ipfs://BUILD_CID
}

Replace the following values:

  • BUILD_CID: This value is from the build folder that we uploaded to Filebase in step 14.

  • CID: This CID represents an image file that will be used for a visual representation of your app. To upload a file to Filebase to be used in this place, follow these steps:

    • Start by clicking on the ‘Buckets’ option from the menu to open the Buckets dashboard.

    • Select your IPFS Bucket.

  • After clicking on the bucket name, you will see any previously uploaded files. To upload another file, select 'Upload', then select 'File' from the options.

  • Select the file you want to upload to the IPFS.

  • Once uploaded, you will be able to view and copy the IPFS CID from the 'CID' category, as seen below.

17. Next, upload the metadata file to IPFS:

aws --endpoint https://s3.filebase.com s3 cp metadata.json s3://bucket-name

Replace bucket-name with your Filebase IPFS bucket name.

18. Finally, open the scripts folder, and then open the deploy.js file. Replace the existing content with the following:

const hre = require("hardhat");
const URI = "ipfs://METADATA_CID"
async function main() {
  const AppNFT = await ethers.getContractFactory("AppNFT");
  const appNft = await AppNFT.deploy(URI);
  await appNft.deployed();
  console.log(`Contract deployed to ${appNft.address}`);
}
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Replace the METADATA_CID value with the IPFS CID for the metadata.json file you uploaded to Filebase in the previous step. This value can be found in the Filebase web console dashboard.

19. Run the following command to deploy this script on the Polygon network:

npx hardhat run scripts/deploy.js --network mumbai

From here, you can view this deployed NFT from a public marketplace like OpenSea by connecting your cryptowallet to the platform, or you can configure your app to be customized, since we used the default React app in this example.

Last updated