目录
介绍
在本文中,我将向您展示如何创建NFT合约,以及如何构建React Web3应用程序来加载NFT集合、生成NFT和提取功能。
什么是以太坊(Ethereum)?
以太坊是区块链世界中的大牌。它是第二大区块链平台,是开发基于区块链的去中心化应用程序的首选。以太坊重新定义了区块链技术的吸引力,并向世界展示了它不仅仅是一个点对点的现金系统。虽然新来者大多将比特币与区块链技术联系起来,但加密货币只是该技术的一个方面。但以太坊是可编程的区块链平台和开源。因此,用户可以开发他们选择的不同应用程序。以太坊创新了大量先进的概念,例如去中心化应用程序、智能合约、虚拟机、ERC代币等。
什么是ERC?
ERC基本上意味着以太坊征求意见,其基本作用是为以太坊提供功能。它具有一套用于在以太坊上创建代币的标准规则。ERC代币中的说明概述了代币的销售、购买、单位限制和存在。
ERC-20和ERC721代币是ERC代币标准的初始类型,在定义以太坊生态系统的功能方面发挥着重要作用。您可以将它们视为在以太坊区块链上创建和发布智能合约的标准。同样重要的是要注意,人们可以投资在智能合约的帮助下创建的代币化资产或智能属性。ERC更像是所有开发人员在开发智能合约时都应遵循的模板或格式。
ERC20与ERC721的区别是可替代性和不可替代性之间的差异。可替代资产是您可以与另一个类似实体交换的资产。另一方面,不可替代的资产正好相反,不能相互交换。例如,房屋很容易被视为不可替代的资产,因为它具有一些独特的属性。当涉及到加密世界时,以数字形式表示资产肯定必须考虑可替代性和不可替代性方面。
什么是NFT智能合约?
NFT智能合约是ERC721代币。它是一种不可替代的代币,称为NFT,是一种数字资产,代表现实世界的对象,如区块链上的艺术、音乐和视频。NFT使用在加密货币区块链上记录和认证的识别码和元数据,这使得区块链上代表的每个NFT都是唯一的资产。与同样记录在区块链上的加密货币不同,NFT不能等价交易或交换,因此它们是不可替代的。智能合约是存在于区块链中的编程。这使网络能够存储NFT交易中指示的信息。智能合约是自动执行的,可以检查合同条款是否得到满足,以及执行条款,而无需中介或中央机构。
什么是坚固性(Solidity)?
Solidity是一种面向对象的高级语言,用于实现智能合约。智能合约是管理以太坊状态内账户行为的程序。
Solidity是静态类型的,支持继承、库和复杂的用户定义类型以及其他功能。使用Solidity,您可以创建用于投票、众筹、盲目拍卖和多重签名钱包等用途的合约。
什么是安全帽(Hardhat)?
Hardhat是以太坊开发环境。轻松部署合约、运行测试和调试Solidity代码,而无需处理实时环境。Hardhat网络是一个专为开发而设计的本地以太坊网络。
先决条件
NFT代币具有元数据(图像和属性)。它可以存储在IPFS(星际文件系统)或链上。我们将我们的NFT元数据以SVG格式存储在IPFS上。Opensea的店面支持SVG图片格式,合约部署到ethereum区块链mainnet、testnet后,可以在其中查NFT。
在我们创建NFT合约之前,我们需要将SVG图像上传到IPFS。多亏了PinataCID网站,它使这项工作变得非常容易。转到Pinata网站并创建一个帐户,如果您上传最多1GB的数据,则免费。注册后,您将被带到Pin图管理器窗口。使用界面上传您的文件夹。上传文件夹后,您将获得与之关联的文件夹。它应该看起来像这样:
对于我的文件夹,是CIDQmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg。因此,此文件夹的IPFS URL是ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg。
此URL不会在浏览器中打开。为此,您可以使用IPFS网关的HTTP URL。尝试访问此链接:https://ipfs.io/ipfs/QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/0001.svg。它将显示我命名为 00001.png 并上传到我的文件夹的图像。
创建一个React Typescript Web3应用程序
构建智能合约时,您需要一个开发环境来部署合约、运行测试和调试Solidity代码,而无需处理实时环境。
React App是将代码编译为可在客户端应用程序中运行的Solidity代码的完美应用程序。
Hardhat是一个专为全栈开发而设计的Ethereum开发环境和框架。
ethers.js是一个完整而紧凑的库,用于从客户端应用程序(如React、Vue、Angular)与Ethereum Blockchain及其生态系统进行交互。
MetaMask帮助处理帐户管理并将当前用户连接到区块链。连接他们的MetaMask钱包后,您可以与全球可用的Ethereum API(window.ethereum)进行交互,该API用于识别web3兼容浏览器的用户(如MetaMask用户)。
首先,我们创建一个typescript React应用程序。
npx create-react-app react-web3-ts --template typescript
接下来,切换到新目录并安装ethers.js和hardhat。
npm install ethers hardhat chai @nomiclabs/hardhat-ethers
首先从React应用程序文件夹中删除 README.md 和 tsconfig.json,否则会出现冲突。运行以下命令:
npx hardhat
然后选择“创建TypeScript项目”。
它会提示您安装一些依赖项。
Hardhat已安装。我们只需要安装hardhat-toolbox。
npm install --save-dev @nomicfoundation/hardhat-toolbox@^1.0.1
现在在VS Code中打开“react-web3-ts”文件夹,它应该是这样的:
有一个示例合约 Lock.sol。
从我们的React应用程序中,我们与智能合约交互的方式是使用ethers.js库、合约地址以及由hardhat根据合约创建的ABI。
什么是ABI?ABI代表应用程序二进制接口。您可以将其视为客户端应用程序和以太坊区块链之间的接口,您将在其中部署要与之交互的智能合约。
ABIs通常由开发框架(HardHat)从Solidity智能合约编译而成。您还经常可以在Etherscan上找到智能合约的ABIs。
Hardhat-toolbox包括type-chain。默认情况下,typechain在根文件夹下的typechain-types中生成键入内容。这将导致我们的React客户端编码出现问题。React会抱怨我们应该只从 src 文件夹导入类型。所以我们在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, // should overloads with full signatures
// like deposit(uint256) be generated always,
// even if there are no overloads?
externalArtifacts: ['externalArtifacts/*.json'], // optional array of glob
// patterns with external artifacts to process (for example external libs
// from node_modules)
dontOverrideCompile: false // defaults to false
}
};
export default config;
在hardhat.config.ts中,我添加了一个本地网络,请注意,如果要连接MetaMask,则必须将链ID设置为1337。此外,我们添加了一个自定义的类型链配置,其中我们的文件夹是 src/typechain-types。
编译ABI
在VS Code终端中运行以下命令。
npx hardhat compile
现在,您应该在 src 目录中看到一个名为类型链类型的新文件夹。您可以找到示例合约 Lock.sol 的所有类型和接口。
部署和使用本地网络/区块链
若要部署到本地网络,首先需要启动本地测试节点。
npx hardhat node
您应该会看到地址和私钥的列表。
这些是为我们创建的20个测试帐户和地址,我们可以用来部署和测试我们的智能合约。每个账户还加载了10,000个假以太币。稍后,我们将学习如何将测试帐户MetaMask导入其中,以便我们可以使用它。
现在我们可以运行部署脚本,并为我们要部署到本地网络的CLI提供一个标志。在VS Code中,打开另一个终端以运行以下命令:
npx hardhat run scripts/deploy.ts --network localhost
锁定协定部署到0x5FbDB2315678afecb367f032d93F642f64180aa3。
还行。编译并部署示例协定。这验证了我们的web3开发环境是否已设置。现在我们运行“hardhat clean”来清理示例合约并开始编写我们自己的NFT可收集智能合约。
npx hardhat clean
编写NFT可收集智能合约
现在让我们安装OpenZeppelin合约包。这将使我们能够访问ERC721合约(NFT的标准)以及我们稍后会遇到的一些辅助库。
npm install @openzeppelin/contracts
从合约文件夹中删除 lock.sol。创建一个名为NFTCollectible.sol的新文件。
我们将使用Solidity v8.4。我们的合同将继承自OpenZeppelin的ERC721Enumerable和Ownable合同。前者除了在处理NFT集合时有用的一些辅助函数外,还具有ERC721(NFT)标准的默认实现。后者允许我们为合同的某些方面添加管理权限。
除此之外,我们还将使用OpenZeppelin的SafeMath和Counters库分别安全地处理无符号整数算法(通过防止溢出)和令牌ID。
这就是我们的合同的样子:
// contracts/NFT.sol
// SPDX-License-Identifier: MIT
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.");
}
}
我们在构造函数调用中设置baseTokenURI。我们还调用父构造函数并为我们的NFT集合设置名称和符号。
我们的NFT JSON元数据可在文章开头提到的这个IPFS UR中找到。
当我们将其设置为基本UR时,OpenZeppelin的实现会自动推断每个令牌的 URI。它假定令牌1的元数据将在ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/1上可用,令牌2的元数据将在ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/2上可用,依此类推。
但是,我们需要告诉我们的合约,我们定义的baseTokenURI变量是合约必须使用的基本URI。为此,我们覆盖一个名为_baseURI()的空函数并使其返回baseTokenURI。
Mint NFT功能
现在让我们将注意力转向主要的mint NFT功能。我们的用户和客户在想从我们的收藏中购买和mint NFT时会调用此功能。
任何人都可以通过支付一定数量的ether + gas来制造一定数量的NFT,因为他们正在向此功能发送以太币,我们必须将其标记为应付。
在允许造币厂发生之前,我们需要进行三项检查:
- 集合中还剩下足够的NFT供调用者铸造请求的数量。
- 调用方要求铸造大于0且小于每笔交易允许的最大NFT数量。
- 调用方已发送足够的ether来铸造所需数量的 NFT。
提取余额功能
如果我们无法撤回已发送到合约的以太币,那么到目前为止我们付出的所有努力都将付诸东流。
让我们编写一个函数,允许我们提取合约的全部余额。这显然会被标记为onlyOwner。
编译合约
首先,编译我们的新智能合约。
npx hardhat compile
编译完成后,将在 src/typechain-types 文件夹中生成新合约的类型。
您可以在 NFTCollectible__factory.ts 上找到新联系人的ABI。
测试合同
删除Lock.ts并在测试文件夹中添加 NFTCollectible.ts。让我们从下面的代码开始。
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;
}
因为每个测试用例都需要部署合约。所以我们把部署写成beforeEach。
beforeEach(async function () {
// Get the ContractFactory and Signers here.
[owner, addr1] = await ethers.getSigners();
const contractFactory = await ethers.getContractFactory("NFTCollectible");
contract = await contractFactory.deploy("baseTokenURI");
await contract.deployed();
}); beforeEach(async function () {
// Get the ContractFactory and Signers here.
[owner, addr1] = await ethers.getSigners();
const contractFactory = await ethers.getContractFactory("NFTCollectible");
contract = await contractFactory.deploy("baseTokenURI");
await contract.deployed();
});
现在我们添加事务测试用例。
- reserveNFTs为所有者保留10个NFT。
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);
});
- NFT的价格为0.01ETH,因此需要支付0.03ETH才能铸造3个NFT。
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);
});
- 铸造NFT时,矿工支付智能合约和汽油费。汽油费归矿工所有,但加密货币归合同而不是所有者所有。
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);
});
运行测试。
npx hardhat test
在本地部署协定
更改scripts\deplot.ts 文件中的主函数。
将lock合约部署替换为main函数中的NTFCollectible合约部署。请注意,我们需要将我们的NFT集合基本URL传递给合约的构造函数。
const baseTokenURI = "ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/";
// Get contract that we want to deploy
const contractFactory = await ethers.getContractFactory("NFTCollectible");
// Deploy contract with the correct constructor arguments
const contract = await contractFactory.deploy(baseTokenURI);
// Wait for this transaction to be mined
await contract.deployed();
console.log("NFTCollectible deployed to:", contract.address);
在VS Code中打开终端以运行:
npx hardhat node
打开另一个终端以运行部署命令。
npx hardhat run scripts/deploy.ts --network localhost
我们的合同被部署到地址0x5FbDB2315678afecb367f032d93F642f64180aa3。
React客户端
我们已经部署了我们的合约。接下来,我将向您展示如何构建一个React客户端来使用此智能合约提供的功能。
Material UI
Material UI是一个开源的React组件库,它实现了Google的 Material Design。
它包括一个全面的预构建组件集合,这些组件开箱即用,可在生产中使用。
Material UI设计精美,并具有一套自定义选项,可让您轻松地在我们的组件之上实现您自己的自定义设计系统。
安装Material UI。
npm install @mui/material @emotion/react @emotion/styled
安装Material UI图标。
npm install @mui/icons-material
MUI具有所有不同类型的组件。我们将使用App Bar, Box, Stack, Modal和Image List。
- App Bar
App Bar显示与当前屏幕相关的信息和操作。
- Box
Box组件充当大多数CSS实用程序需求的包装器组件。
- Stack
Stack组件管理沿垂直或水平轴的直接子项的布局,每个子项之间具有可选的间距和/或分隔符。
- Modal
Modal组件为创建对话框、弹出框、lightbox或其他任何内容提供了坚实的基础。
- 图像列表
图像列表在有组织的网格中显示图像集合。
目前,tsconfig.json是由hardhat typescript创建的。它没有完全覆盖React Typescript。更新 tsconfig.json,如下所示:
{
"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"]
}
下载MetaMask.svg到src文件夹,我们将其用作logo。
在 src 文件夹下添加 Demo.tsx 文件,然后复制以下代码。我们从基本应用栏开始。
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;
然后更改 index.tsx 以加载演示而不是应用程序。
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>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
运行它。
npm start
连接钱包
让我们仔细检查我们的NFT集合合约部署的地址。它是0x5FbDB2315678afecb367f032d93F642f64180aa3。定义第一个const。
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
然后定义IWallet接口。
interface IWallet {
iconColor: string;
connectedWallet: string;
contractAddress: string;
contractSymbol: string;
contractBaseTokenURI: string;
contractOwnerAddress: string;
contractPrice: string;
isOwner: boolean;
}
我们需要使用useState hook来初始化和更新IWallet实例。React从16.8中引入了Hooks。useState是一个内置钩子,允许您在函数组件中使用本地状态。将初始状态传递给此函数,它将返回一个具有当前状态值(不一定是初始状态)的变量和另一个函数来更新此值。
const [state, setState] = React.useState<IWallet>({
iconColor: "disabled",
connectedWallet: "",
contractSymbol: "",
contractAddress: "",
contractBaseTokenURI: "",
contractOwnerAddress: "",
contractPrice: "",
isOwner: false
});
导入ethers和NFTCollectible__factory。
import { BigNumber, ethers } from "ethers";
import { NFTCollectible__factory } from
'./typechain-types/factories/contracts/NFTCollectible__factory'
现在写连接钱包功能。
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 contract = new ethers.Contract
//(contractAddress, NFTCollectible__factory.abi, signer) as NFTCollectible;
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);
}
};
你可以在这里看到,必须安装MetaMask扩展,否则你无法从窗口中获取Ethereum对象。进行UI更改,添加“连接”按钮和“已连接帐户”、“合同地址”、“合同基础令牌URI”文本字段。此外,使用Account Circle图标指示是否已连接。
<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>
以下是使用Stack的外观。堆栈有两个方向,“row”和“column”。
运行我们的应用。
单击连接按钮。
呵呵,工作一样迷人!
加载NFT集合
为图像URL集合添加状态hook。
const [nftCollection, setNFTCollection] = React.useState<string[]>([]);
写入加载NFT集合函数。
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);
}
};
导入ImageList和ImageListItem。
import ImageList from '@mui/material/ImageList';
import ImageListItem from '@mui/material/ImageListItem';
将图像列表与NFT URL集合绑定。
<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>
再一次运行应用程序并单击“加载NFT集合”按钮。
Mint NFT
添加IService接口。
interface IService {
account: string;
ethProvider?: ethers.providers.Web3Provider,
contract?: NFTCollectible;
currentBalance: number;
ethBalance: string;
mintAmount: number;
}
使用状态挂钩。
const [service, setService] = React.useState<IService>({
account: "",
currentBalance: 0,
ethBalance: "",
mintAmount: 0
});
Mint NFT功能。
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
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>
运行应用程序,单击“Mint NFT”按钮。您将看到一个弹出对话框。
提取
Withdraw功能。
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
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仅适用于部署合同的合同所有者。因此,提取按钮仅对所有者启用。
<Button variant="contained" disabled={!state.isOwner}
onClick={handleOpenWithdrawal}>Withdraw</Button>
如果MetaMask当前连接的帐户不是合约的所有者,则“提取”按钮将被禁用。
在MetaMask中将帐户更改为所有者。
更改为所有者后,单击我们反应客户端中的连接按钮,提取按钮将被启用。
点击“提取”按钮。
就是这样。一个小而有趣的web3应用程序完成了。现在你打开了一扇通往全新世界的大门。
尾声:React路由器
我们已经完成了react web3客户端的构建。有一个非常重要的反应概念我没有提到。那是React Router。在我们的应用程序中,我创建了一个演示组件。并直接放到index.tsx中。对于简单的应用程序来说,这不是问题。但是,如果您有多个组件想要导航怎么办?React Router提供完美的解决方案。
React Router不仅仅是将URL与函数或组件匹配:它是关于构建映射到URL的完整用户界面,因此它可能包含比您习惯的更多概念。React router做以下三个主要工作。
- 订阅和操作历史记录堆栈
- 将URL与您的路由匹配
- 从路由匹配项呈现嵌套UI
安装Ract Router。最新版本是V6。
npm install react-router-dom
现在在App.tsx中导入react router和演示组件。
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Link from '@mui/material/Link';
import Demo from './Demo'
在App.tsx中创建Home函数。使用Link让用户更改URL或useNavigate自己更改。这里我们使用的Link来自Material UI而不是来自react-router-dom。本质上,除了样式之外,它们是一回事。
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>
);
}
在App函数中配置路由。
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/demo" element={<Demo />} />
</Routes>
</Router>
);
}
在React Router以前的版本中,您必须以某种方式对路由进行排序,以便在多个路由与不明确的URL匹配时获得正确的路由进行呈现。V6要聪明得多,并且会选择最具体的匹配项,因此您不必再担心这一点。
不要忘记最后一件事,改回index.tsx中的App组件。
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
现在运行我们的应用程序。
npm start
单击 Wbe3 NFT演示,它将导航到我们的web3组件。
如何使用源代码
首先,在您的浏览器(Chrome,Firefox或Edge)上安装MetaMask扩展程序。
然后下载并提取源代码,使用Visual Studio Code打开react-web3-ts文件夹。
然后在VS Code中打开新终端。
- 安装所有依赖项:
npm install
- 编译智能合约:
npx hardhat compile
- 启动Hardhat节点:
npx hardhat node
- 打开另一个终端,部署合约:
npx hardhat run scripts/deploy.ts --network localhost
- 启动应用:
npm start
结论
我们在这里涵盖了很多内容,从智能合约到web3应用程序。我希望你学到了很多东西。现在你可能会问,“智能合约有什么用?“
智能合约相对于集中式系统的优势:
- 数据不能更改或篡改。因此,恶意行为者几乎不可能操纵数据。
- 它是完全去中心化的。
- 与任何集中支付钱包不同,您无需向中间人支付任何佣金百分比即可进行交易。
- 最重要的是,智能合约可能会为您的财务自由打开一扇门。
示例项目现在在github中,dapp-ts。
https://www.codeproject.com/Articles/5338801/Build-NFT-Collection-Web3-Application-with-Hardha