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": "./"
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:
In this file, you will need to replace two values:
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.
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.
9. Next, install the .
ALCHEMY_KEY: Your Alchemy API key. For instructions on how to get this value, see
POLYGON MUMBAI WALLET PRIVATE KEY: Polygon wallet private key. For instructions on how to get this value, see