This article creates a NFT contract and also builds a React client to load NFT collection, mint NFT and withdraw.
Introduction
In this article, I'll show you how to create a NFT contract, and how to build a React Web3 application to load NFT collection, mint NFT and withdrawal functionalities.
What is Ethereum?
Ethereum is a big name in the world of blockchain. It is the second-largest blockchain platform which serves as the first choice for developing blockchain-based decentralized applications. Ethereum redefined the appeal of blockchain technology and showed the world that it can be much more than being just a peer-to-peer cash system. While newcomers mostly associate bitcoins with blockchain technology, cryptocurrencies are only one aspect of the technology. But Ethereum is programmable blockchain platform and open-source. So, the users can develop different applications of their choice. Ethereum innovated a plethora of advanced concepts, such as — decentralized applications, smart contracts, virtual machines, ERC tokens, etc.
What is ERC?
ERC basically means Ethereum Request for Comments, and its basic role is offering functionality for Ethereum. It features a standard set of rules for creating tokens on Ethereum. The instructions in the ERC tokens outline the sales, purchases, unit limits, and existence of tokens.
ERC-20 and ERC721 tokens are the initial types of ERC token standards that serve an important role in defining the functionality of the Ethereum ecosystem. You can think of them as the standards for the creation and publication of smart contracts on the Ethereum blockchain. It is also important to note that people could invest in tokenized assets or smart properties created with the help of smart contracts. ERC is more of a template or format which all developers should follow in developing smart contracts.
The difference ERC20 vs. ERC721 is the differences between fungibility and non-fungibility. Fungible assets are the assets that you can swap with another similar entity. On the other hand, non-fungible assets are the opposite and cannot be swapped for one another. For example, a house could be easily considered a non-fungible asset as it would have some unique properties. When it comes to the crypto world, representation of assets in the digital form would definitely have to consider the aspects of fungibility and non-fungibility.
What is NFT Smart Contract?
NFT Smart Contract is ERC721 token. It's a non-fungible token, referred to as an NFT, is a digital asset that represents real-world objects such as art, music, and videos on a blockchain. NFTs use identification codes and metadata that is logged and authenticated on cryptocurrency blockchains, which renders each NFT represented on the blockchain a unique asset. Unlike cryptocurrencies, which are also logged on blockchains, NFTs cannot be traded or exchanged at equivalency, hence they are non-fungible. A smart contract is programming that exists within the blockchain. This enables the network to store the information that is indicated in an NFT transaction. Smart contracts are self-executing and can check that the contract terms have been satisfied, as well as execute the terms without the need for an intermediary or central authority.
What's Solidity?
Solidity is an object-oriented, high-level language for implementing smart contracts. Smart contracts are programs which govern the behaviour of accounts within the Ethereum state.
Solidity is statically typed, supports inheritance, libraries and complex user-defined types among other features. With Solidity you can create contracts for uses such as voting, crowdfunding, blind auctions, and multi-signature wallets.
What's Hardhat?
Hardhat is Ethereum development environment. Easily deploy your contracts, run tests and debug Solidity code without dealing with live environments. Hardhat Network is a local Ethereum network designed for development.
Prerequisites
NFT token has meta-data (image and properties). It can be stored on IPFS (InterPlanetary File System) or on chain. We store our NFT meta data on IPFS as SVG format. SVG image format is supported by Opensea
's storefront and you can view the NFT in it after the contract is deployed to ethereum
blockchain mainnet
, testnet
.
Before we create NFT contract, we need to upload SVG images to IPFS. Thanks to Pinata
website, it makes this work pretty easy. Go to the Pinata website and create an account, It’s free if you’re uploading up to 1 GB of data. Once you have signed up, you will be taken to the Pin Manager window. Upload your folder using the interface. Once you’ve uploaded your folder, you will get a CID
associated with it. It should look something like this:
For my folder, the CID
is QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg
. Therefore, the IPFS URL for this folder is ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg
.
This URL will not open in a browser. In order to do that, you can use a HTTP URL of an IPFS gateway. Try visiting this link: https://ipfs.io/ipfs/QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/0001.svg. It will display an image that I named 00001.png and uploaded to my folder.
Create a React Typescript Web3 Application
When building smart contracts, you need a development environment to deploy contracts, run tests, and debug Solidity
code without dealing with live environments.
React
app is a perfect application to compile Solidity
code into code that can be run in a client-side application.
Hardhat is an Ethereum
development environment and framework designed for full stack development.
ethers.js is a complete and compact library for interacting with the Ethereum Blockchain
and its ecosystem from client-side applications like React
, Vue
, Angular
.
MetaMask
helps to handle account management and connecting the current user to the blockchain. Once connected their MetaMask
wallet, you can interact with the globally available Ethereum
API (window.ethereum
) that identifies the users of web3-compatible browsers (like MetaMask
users).
First, we create a typescript React
app.
npx create-react-app react-web3-ts --template typescript
Next, change into the new directory and install ethers.js and hardhat
.
npm install ethers hardhat chai @nomiclabs/hardhat-ethers
Delete README.md and tsconfig.json from the React application folder first, otherwise you will get a conflict. Run the below command:
npx hardhat
Then select “Create a TypeScript project”.
It will prompt you to install some dependencies.
Hardhat is already installed. We just need to install hardhat-toolbox
.
npm install --save-dev @nomicfoundation/hardhat-toolbox@^1.0.1
Now open “react-web3-ts” folder in VS Code, it should be like the below:
There is a sample contract Lock.sol.
From our React app, the way that we will interact with the smart contract is using a combination of the ethers.js library, the contract address, and the ABI
that will be created from the contract by hardhat
.
What is an ABI
? ABI
stands for application binary interface. You can think of it as the interface between your client-side application and the Ethereum blockchain where the smart contract you are going to be interacting with is deployed.
ABIs
are typically compiled from Solidity
smart contracts by a development framework like HardHat
. You can also often find the ABIs
for a smart contract on Etherscan
.
Hardhat-toolbox
includes type-chain
. By default, typechain
generates typings in typechain-types
under root folder. That will cause a problem in our React client coding. React will complain that we should only import types from src folder. So we make changes in hardhat.config.ts.
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.9",
networks: {
hardhat: {
chainId: 1337
}
},
typechain: {
outDir: 'src/typechain-types',
target: 'ethers-v5',
alwaysGenerateOverloads: false,
externalArtifacts: ['externalArtifacts/*.json'],
dontOverrideCompile: false
}
};
export default config;
In hardhat.config.ts, I add a local network, please note you have to set chain Id as 1337, if you want to connect with MetaMask
. Also, we add a customized typechain configuration, where out folder is src/typechain-types.
Compile the ABI
Run the below command in VS Code terminal.
npx hardhat compile
Now, you should see a new folder named typechain-types in the src directory. You can find all types and interfaces for the sample contract Lock.sol.
Deploying and Using a Local Network / Blockchain
To deploy to the local network, you first need to start the local test node.
npx hardhat node
You should see a list of addresses and private keys.
These are 20 test accounts and addresses created for us that we can use to deploy and test our smart contracts. Each account is also loaded up with 10,000 fake Ether. In a moment, we'll learn how to import the test account into MetaMask
so that we can use it.
Now we can run the deploy script and give a flag to the CLI
that we would like to deploy to our local network. In VS Code, open another terminal to run the below command:
npx hardhat run scripts/deploy.ts --network localhost
Lock contract is deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
.
Ok. The sample contract is compiled and deployed. That verified our web3 development environment is setup. Now we run “hardhat clean
” to clean the sample contract and start to write our own NFT Collectible smart contract.
npx hardhat clean
Write NFT Collectible Smart Contract
Let us now install the OpenZeppelin
contracts package. This will give us access to the ERC721
contracts (the standard for NFTs) as well as a few helper libraries that we will encounter later.
npm install @openzeppelin/contracts
Delete lock.sol from contracts folder. Create a new file called NFTCollectible.sol.
We will be using Solidity v8.4
. Our contract will inherit from OpenZeppelin
’s ERC721Enumerable
and Ownable
contracts. The former has a default implementation of the ERC721 (NFT)
standard in addition to a few helper functions that are useful when dealing with NFT collections. The latter allows us to add administrative privileges to certain aspects of our contract.
In addition to the above, we will also use OpenZeppelin
’s SafeMath
and Counters
libraries to safely deal with unsigned integer arithmetic (by preventing overflows) and token IDs respectively.
This is what our contract looks like:
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract NFTCollectible is ERC721Enumerable, Ownable {
using SafeMath for uint256;
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
uint256 public constant MAX_SUPPLY = 100;
uint256 public constant PRICE = 0.01 ether;
uint256 public constant MAX_PER_MINT = 5;
string public baseTokenURI;
constructor(string memory baseURI) ERC721("NFT Collectible", "NFTC") {
setBaseURI(baseURI);
}
function setBaseURI(string memory _baseTokenURI) public onlyOwner {
baseTokenURI = _baseTokenURI;
}
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}
function reserveNFTs() public onlyOwner {
uint256 totalMinted = _tokenIds.current();
require(totalMinted.add(10) < MAX_SUPPLY, "Not enough NFTs");
for (uint256 i = 0; i < 10; i++) {
_mintSingleNFT();
}
}
function mintNFTs(uint _count) public payable {
uint totalMinted = _tokenIds.current();
require(totalMinted.add(_count) <= MAX_SUPPLY, "Not enough NFTs left!");
require(_count >0 && _count <= MAX_PER_MINT,
"Cannot mint specified number of NFTs.");
require(msg.value >= PRICE.mul(_count), "Not enough ether to purchase NFTs.");
for (uint i = 0; i < _count; i++) {
_mintSingleNFT();
}
}
function _mintSingleNFT() private {
uint256 newTokenID = _tokenIds.current();
_safeMint(msg.sender, newTokenID);
_tokenIds.increment();
}
function tokensOfOwner(address _owner) external view returns (uint[] memory) {
uint tokenCount = balanceOf(_owner);
uint[] memory tokensId = new uint256[](tokenCount);
for (uint i = 0; i < tokenCount; i++) {
tokensId[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokensId;
}
function withdraw() public payable onlyOwner {
uint balance = address(this).balance;
require(balance > 0, "No ether left to withdraw");
(bool success, ) = (msg.sender).call{value: balance}("");
require(success, "Transfer failed.");
}
}
We set the baseTokenURI
in our constructor call. We also call the parent constructor and set the name and symbol for our NFT collection.
Our NFT JSON metadata is available at this IPFS URL mentioned at the beginning of the article.
When we set this as the base URI, OpenZeppelin
’s implementation automatically deduces the URI for each token. It assumes that token 1’s metadata will be available at ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/1
, token 2’s metadata will be available at ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/2
, and so on.
However, we need to tell our contract that the baseTokenURI
variable that we defined is the base URI that the contract must use. To do this, we override an empty function called _baseURI()
and make it return baseTokenURI
.
Mint NFTs Function
Let us now turn our attention to the main mint NFTs function. Our users and customers will call this function when they want to purchase and mint NFTs from our collection.
Anyone to mint a certain number of NFTs by paying the required amount of ether + gas
, since they’re sending ether to this function, we have to mark it as payable.
We need to make three checks before we allow the mint to take place:
- There are enough NFTs left in the collection for the caller to mint the requested amount.
- The caller has requested to mint more than 0 and less than the maximum number of NFTs allowed per transaction.
- The caller has sent enough
ether
to mint the requested number of NFTs.
Withdraw Balance Function
All the effort we’ve put in so far would go to waste if we are not able to withdraw the ether that has been sent to the contract.
Let us write a function that allows us to withdraw the contract’s entire balance. This will obviously be marked as onlyOwner
.
Compile Contract
First, compile our new smart contract.
npx hardhat compile
After compile finish, types of the new contract are generated at src/typechain-types folder.
You can find ABI of the new contact at NFTCollectible__factory.ts.
Test Contract
Delete Lock.ts and add NFTCollectible.ts in test folder. Let's start with the code below.
import { expect } from "chai";
import { ethers } from "hardhat";
import { NFTCollectible } from "../src/typechain-types/contracts/NFTCollectible";
import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
describe("NFTCollectible", function () {
let contract : NFTCollectible;
let owner : SignerWithAddress;
let addr1 : SignerWithAddress;
}
Because every test case needs the contract get deployed. So we write deployment as beforeEach
.
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const contractFactory = await ethers.getContractFactory("NFTCollectible");
contract = await contractFactory.deploy("baseTokenURI");
await contract.deployed();
}); beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const contractFactory = await ethers.getContractFactory("NFTCollectible");
contract = await contractFactory.deploy("baseTokenURI");
await contract.deployed();
});
Now we add transaction test cases.
reserveNFTs
reserves 10 NFTs for the owner.
it("Reserve NFTs should 10 NFTs reserved", async function () {
let txn = await contract.reserveNFTs();
await txn.wait();
expect(await contract.balanceOf(owner.address)).to.equal(10);
});
- The price of NFT is 0.01ETH, so need pay 0.03ETH to mint 3 NFTs.
it("Sending 0.03 ether should mint 3 NFTs", async function () {
let txn = await contract.mintNFTs(3,
{ value: ethers.utils.parseEther('0.03') });
await txn.wait();
expect(await contract.balanceOf(owner.address)).to.equal(3);
});
-
When minting an NFT, the minter pays the smart contract and gas fees. The gas fees go to miners, but the cryptocurrency goes to the contract and not the owner.
it("Withdrawal should withdraw the entire balance", async function () {
let provider = ethers.provider
const ethBalanceOriginal = await provider.getBalance(owner.address);
console.log("original eth balanace %f", ethBalanceOriginal);
let txn = await contract.connect(addr1).mintNFTs(1,
{ value: ethers.utils.parseEther('0.01') });
await txn.wait();
const ethBalanceBeforeWithdrawal = await provider.getBalance(owner.address);
console.log("eth balanace before withdrawal %f", ethBalanceBeforeWithdrawal);
txn = await contract.connect(owner).withdraw();
await txn.wait();
const ethBalanceAfterWithdrawal = await provider.getBalance(owner.address);
console.log("eth balanace after withdrawal %f", ethBalanceAfterWithdrawal);
expect(ethBalanceOriginal.eq(ethBalanceBeforeWithdrawal)).to.equal(true);
expect(ethBalanceAfterWithdrawal.gt
(ethBalanceBeforeWithdrawal)).to.equal(true);
});
Run test.
npx hardhat test
Deploying the Contract Locally
Change main function in scripts\deplot.ts file.
Replace lock contract deployment with our NTFCollectible
contract deployment in main
function. Please note we need pass our NFT Collection base URL to the constructor of contract.
const baseTokenURI = "ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/";
const contractFactory = await ethers.getContractFactory("NFTCollectible");
const contract = await contractFactory.deploy(baseTokenURI);
await contract.deployed();
console.log("NFTCollectible deployed to:", contract.address);
Open terminal in VS Code to run:
npx hardhat node
Open another terminal to run deploy command.
npx hardhat run scripts/deploy.ts --network localhost
Our contract is deployed to address 0x5FbDB2315678afecb367f032d93F642f64180aa3
.
React Client
We have deployed our contract. Next, I will show you how to build a React Client to use the functionalities provided by this smart contract.
Material UI
Material UI is an open-source React component library that implements Google's Material Design.
It includes a comprehensive collection of prebuilt components that are ready to use in production right out of the box.
Material UI is beautiful by design and features a suite of customization options that make it easy to implement your own custom design system on top of our components.
Install Material UI.
npm install @mui/material @emotion/react @emotion/styled
Install Material UI Icons.
npm install @mui/icons-material
MUI has all different kinds of components. We’ll use App Bar, Box, Stack, Modal, and Image List.
- App Bar
The App bar displays information and actions relating to the current screen.
- Box
The Box component serves as a wrapper component for most of the CSS utility needs.
- Stack
The Stack component manages layout of immediate children along the vertical or horizontal axis with optional spacing and/or dividers between each child.
- Modal
The modal component provides a solid foundation for creating dialogs, popovers, lightboxes, or whatever else.
- Image List
Image lists display a collection of images in an organized grid.
Currently, tsconfig.json is created by hardhat typescript. It’s not fully cover react typescript. Update tsconfig.json as the below:
{
"compilerOptions": {
"target": "es2021",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"outDir": "dist",
"sourceMap": true,
"jsx": "react-jsx"
},
"include": ["./scripts", "./test", "./src/typechain-types"],
"files": ["./hardhat.config.ts"]
}
Download MetaMask.svg to src folder, we use it as logo.
Add Demo.tsx file under src folder, and copy the below code. We starts with a basic App Bar.
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import CssBaseline from '@mui/material/CssBaseline';
import Container from '@mui/material/Container';
import Stack from '@mui/material/Stack';
import Avatar from '@mui/material/Avatar';
import logo from './metamask.svg';
function Demo() {
return (
<React.Fragment>
<CssBaseline />
<AppBar>
<Toolbar>
<Stack direction="row" spacing={2}>
<Typography variant="h3" component="div">
NFT Collection
</Typography>
<Avatar alt="logo" src={logo} sx={{ width: 64, height: 64 }} />
</Stack>
</Toolbar>
</AppBar>
<Toolbar />
<Container>
</Container>
</React.Fragment>
);
}
export default Demo;
Then change index.tsx to load Demo instead of App.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import Demo from './Demo';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<Demo />
</React.StrictMode>
);
reportWebVitals();
Run it.
npm start
Connect Wallet
Let’s double check the address where our NFT Collection contract deployed. It’s 0x5FbDB2315678afecb367f032d93F642f64180aa3
. Define a const
first.
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
Then define IWallet
interface.
interface IWallet {
iconColor: string;
connectedWallet: string;
contractAddress: string;
contractSymbol: string;
contractBaseTokenURI: string;
contractOwnerAddress: string;
contractPrice: string;
isOwner: boolean;
}
We need use useState
hook to initialize and update IWallet
instance. React introduces Hooks from 16.8. useState
is a built-in hook, lets you use local state within a function component. You pass the initial state to this function and it returns a variable with the current state value (not necessarily the initial state) and another function to update this value.
const [state, setState] = React.useState<IWallet>({
iconColor: "disabled",
connectedWallet: "",
contractSymbol: "",
contractAddress: "",
contractBaseTokenURI: "",
contractOwnerAddress: "",
contractPrice: "",
isOwner: false
});
Import ethers
and NFTCollectible__factory
.
import { BigNumber, ethers } from "ethers";
import { NFTCollectible__factory } from
'./typechain-types/factories/contracts/NFTCollectible__factory'
Now write connect wallet function.
const connectWallet = async () => {
try {
console.log("connect wallet");
const { ethereum } = window;
if (!ethereum) {
alert("Please install MetaMask!");
return;
}
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});
console.log("Connected", accounts[0]);
const provider = new ethers.providers.Web3Provider(ethereum);
const contract = NFTCollectible__factory.connect
(contractAddress, provider.getSigner());
const ownerAddress = await contract.owner();
const symbol = await contract.symbol();
const baseTokenURI = await contract.baseTokenURI();
const balance = await (await contract.balanceOf(accounts[0])).toNumber();
const ethBalance = ethers.utils.formatEther
(await provider.getBalance(accounts[0]));
const isOwner = (ownerAddress.toLowerCase() === accounts[0].toLowerCase());
const price = ethers.utils.formatEther(await contract.PRICE());
setState({
iconColor: "success",
connectedWallet: accounts[0],
contractSymbol: symbol,
contractAddress: contract.address,
contractBaseTokenURI: baseTokenURI,
contractOwnerAddress: ownerAddress,
contractPrice: `${price} ETH`,
isOwner: isOwner
});
console.log("Connected", accounts[0]);
} catch (error) {
console.log(error);
}
};
You can see here, MetaMask
extension has to be installed, otherwise you cannot get Ethereum
object from window. Make UI changes, add Connect button and Connected Account, Contract Address, Contract Base Token URI text fields. Also, use Account Circle icon to indicate connected or not.
<Stack direction="row" spacing={2} sx={{ margin: 5 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<AccountCircle color={state.iconColor} sx={{ mr: 1, my: 0.5 }} />
<TextField id="wallet_address" label="Connected Account"
sx={{ width: 300 }} variant="standard" value={state.connectedWallet}
inputProps={{ readOnly: true, }}
/>
</Box>
<TextField id="contract_symbol" label="Contract Symbol"
vari-ant="standard" value={state.contractSymbol}
inputProps={{ readOnly: true, }}
/>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField id="contract_address" label="Contract Address"
sx={{ width: 400 }} variant="standard" value={state.contractAddress}
inputProps={{ readOnly: true, }}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField id="contract_baseURI" label="Contract Base Token URI"
sx={{ width: 500 }} variant="standard" value={state.contractBaseTokenURI}
inputProps={{ readOnly: true, }}
/>
</Box>
</Stack>
Here is how it looks to use Stack. Stack have two directions, "row
" and "column
".
Run our app.
Click Connect button.
Ooohh, work as charming!
Load NFT Collection
Add state hook for image URL collection.
const [nftCollection, setNFTCollection] = React.useState<string[]>([]);
Write load NFT collection function.
const loadNFTCollection = async () => {
try {
console.log("load NFT collection");
let baseURI: string = state.contractBaseTokenURI;
baseURI = baseURI.replace("ipfs://", "https://gateway.pinata.cloud/ipfs/");
setNFTCollection(
[
`${baseURI}0001.svg`,
`${baseURI}0002.svg`,
`${baseURI}0003.svg`,
`${baseURI}0004.svg`,
]);
} catch (error) {
console.log(error);
}
};
Import ImageList
and ImageListItem
.
import ImageList from '@mui/material/ImageList';
import ImageListItem from '@mui/material/ImageListItem';
Binding image list with NFT URL Collection.
<ImageList sx={{ width: 500, height: 450 }} cols={3} rowHeight={164}>
{nftCollection.map((item) => (
<ImageListItem key={item}>
<img
src={`${item}?w=164&h=164&fit=crop&auto=format`}
srcSet={`${item}?w=164&h=164&fit=crop&auto=format&dpr=2 2x`}
loading="lazy"
/>
</ImageListItem>
))}
</ImageList>
Run app agin and click "Load NFT Collection" button.
Mint NFT
Add IService
interface.
interface IService {
account: string;
ethProvider?: ethers.providers.Web3Provider,
contract?: NFTCollectible;
currentBalance: number;
ethBalance: string;
mintAmount: number;
}
Use state hook.
const [service, setService] = React.useState<IService>({
account: "",
currentBalance: 0,
ethBalance: "",
mintAmount: 0
});
Mint NFT function.
const mintNFTs = async () => {
try {
console.log("mint NFTs");
const address = service.account;
const amount = service.mintAmount!;
const contract = service.contract!;
const price = await contract.PRICE();
const ethValue = price.mul(BigNumber.from(amount));
const signer = service.ethProvider!.getSigner();
let txn = await contract.connect(signer!).mintNFTs(amount, { value: ethValue });
await txn.wait();
const balance = await contract.balanceOf(address);
setService({...service, currentBalance: balance.toNumber(), mintAmount: 0});
} catch (error) {
console.log(error);
}
};
Mint modal dialog.
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Box sx={modalStyle}>
<Stack spacing={1} sx={{ width: 500 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField id="mint_account" label="Account"
sx={{ width: 500 }} variant="standard" value={service.account}
inputProps={{ readOnly: true}}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField id="price" label="NFT Price"
sx={{ width: 500 }} variant="standard" value={state.contractPrice}
inputProps={{ readOnly: true}}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField id="balance" label="Balance"
sx={{ width: 500 }} variant="standard" value={service.currentBalance}
type = "number" inputProps={{ readOnly: true}}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField id="mint_amount" type="number"
label="Mint Amount" sx={{ width: 500 }}
variant="standard" value={service.mintAmount}
onChange={event => {
const { value } = event.target;
const amount = parseInt(value);
setService({...service, mintAmount: amount});
}}
/>
</Box>
<Stack direction="row" spacing={2} sx={{ margin: 5 }}>
<Button variant="outlined" onClick={mintNFTs}>Mint</Button>
<Button variant="outlined" onClick={handleClose}>close</Button>
</Stack>
</Stack>
</Box>
</Fade>
</Modal>
Ru app, click "Mint NFT" button. You'll get a popup dialog.
Withdraw
Withdraw
function.
const withdraw = async () => {
try {
console.log("owner withdraw");
const contract = service.contract!;
const provider = service.ethProvider!;
let txn = await contract.withdraw();
await txn.wait();
const ethBalance = ethers.utils.formatEther
(await provider!.getBalance(service.account));
setService({...service, ethBalance: `${ethBalance} ETH`});
} catch (error) {
console.log(error);
}
};
Withdraw modal dialog,
<Modal
id="withdrawal_modal"
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={openWithdrawal}
onClose={handleCloseWithdrawal}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={openWithdrawal}>
<Box sx={modalStyle}>
<Stack spacing={1} sx={{ width: 500 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField id="owner_account" label="Owner Account"
sx={{ width: 500 }} variant="standard" value={service.account}
inputProps={{ readOnly: true }}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField id="ethbalance" label="ETH Balance"
sx={{ width: 500 }} variant="standard" value={service.ethBalance}
inputProps={{ readOnly: true }}
/>
</Box>
<Stack direction="row" spacing={2} sx={{ margin: 5 }}>
<Button variant="outlined" onClick={withdraw}>Withdraw</Button>
<Button variant="outlined" onClick={handleCloseWithdrawal}>close</Button>
</Stack>
</Stack>
</Box>
</Fade>
</Modal>
Withdraw
is only available for contract owner who deployed the contract. So Withdraw button is only enabled for the owner.
<Button variant="contained" disabled={!state.isOwner}
onClick={handleOpenWithdrawal}>Withdraw</Button>
If MetaMask
currently connected account is not the owner of contract, Withdraw button is disabled.
Change account to the owner in MetaMask
.
After change to the owner, click Connect button in our react client, Withdraw button will be enabled.
Click “WITHDRAW” button.
That's it. A small but interesting web3 app is done. Now you open a gate to the brand new world.
Epilogue: React Router
We have finished building the react web3 client. There is a very important react concept I haven’t mentioned. That is React Router
. In our app, I created a demo component. and directly put it in index.tsx
. That’s not a problem for simple application. But how about if you have multiple components want to navigate? React Router
provides a perfect solution.
React Router
isn't just about matching a url to a function or component: it's about building a full user interface that maps to the URL, so it might have more concepts in it than you're used to. React router
does the three major jobs as the below.
- Subscribing and manipulating the history stack
- Matching the URL to your routes
- Rendering a nested UI from the route matches
Install Ract Router
. The latest version is V6
.
npm install react-router-dom
Now import react router
and demo component in App.tsx
.
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Link from '@mui/material/Link';
import Demo from './Demo'
Create Home
function in App.tsx
. Use Link
to let the user change the URL or useNavigate
to do it yourself. Here we use Link
from Material UI
not from react-router-dom
. Essentially they are same thing except styling.
function Home() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
With cryptocurrencies and blockchain technology,
NFTs have become part of this crazy world.
</p>
<Box
sx={{
display: 'flex', flexWrap: 'wrap',
justifyContent: 'center', typography: 'h3',
'& > :not(style) + :not(style)': {
ml: 2,
},
}}>
<Link href="/demo" underline="none" sx={{ color: '#FFF' }}>
Web3 NFT Demo
</Link>
</Box>
</header>
</div>
);
}
Configuring routes in App
function.
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/demo" element={<Demo />} />
</Routes>
</Router>
);
}
In previous versions of React Router
you had to order your routes a certain way to get the right one to render when multiple routes matched an ambiguous URL. V6
is a lot smarter and will pick the most specific match so you don't have to worry about that anymore.
Don’t forget last thing, change back to App
component in index.tsx
.
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Now run our app.
npm start
Click Wbe3 NFT Demo, it will navigate to our web3 component.
How to Use the Source Code
First, install MetaMask
extension on your browser (Chrome, Firefox or Edge).
Then download and extract the source code, open react-web3-ts folder with Visual Studio Code.
Then open New Terminal in VS Code.
Conclusion
We covered a lot here, from smart contract to web3 app. I hope you learned a lot. Now you may ask, "what's the use of smart contracts?"
Advantages of smart contracts over centralized systems:
- Data cannot be changed or tampered with. So, it is almost impossible for malicious actors to manipulate data.
- It's completely decentralized.
- Unlike any centralized payment wallet, you don't have to pay any commission percentages to a middle man to transact.
- The most important is a smart contract may open a door to your financial freedom.
The sample project is in github now, dapp-ts. Enjoy coding!
History
- 5th August, 2022: Initial version