当前位置:   article > 正文

使用Hardhat和React Typescript构建NFT集合Web3应用程序_hardhat react

hardhat react

目录

介绍

什么是以太坊(Ethereum)?

什么是ERC?

什么是NFT智能合约?

什么是坚固性(Solidity)?

什么是安全帽(Hardhat)?

先决条件

创建一个React Typescript Web3应用程序

编译ABI

部署和使用本地网络/区块链

编写NFT可收集智能合约

Mint NFT功能

提取余额功能

编译合约

测试合同

在本地部署协定

React客户端

Material UI

连接钱包

加载NFT集合

Mint NFT

提取

尾声:React路由器

如何使用源代码

结论


 

介绍

在本文中,我将向您展示如何创建NFT合约,以及如何构建React Web3应用程序来加载NFT集合、生成NFT和提取功能。

什么是以太坊(Ethereum)

以太坊是区块链世界中的大牌。它是第二大区块链平台,是开发基于区块链的去中心化应用程序的首选。以太坊重新定义了区块链技术的吸引力,并向世界展示了它不仅仅是一个点对点的现金系统。虽然新来者大多将比特币与区块链技术联系起来,但加密货币只是该技术的一个方面。但以太坊是可编程的区块链平台和开源。因此,用户可以开发他们选择的不同应用程序。以太坊创新了大量先进的概念,例如去中心化应用程序、智能合约、虚拟机、ERC代币等。

什么是ERC

ERC基本上意味着以太坊征求意见,其基本作用是为以太坊提供功能。它具有一套用于在以太坊上创建代币的标准规则。ERC代币中的说明概述了代币的销售、购买、单位限制和存在。

ERC-20ERC721代币是ERC代币标准的初始类型,在定义以太坊生态系统的功能方面发挥着重要作用。您可以将它们视为在以太坊区块链上创建和发布智能合约的标准。同样重要的是要注意,人们可以投资在智能合约的帮助下创建的代币化资产或智能属性。ERC更像是所有开发人员在开发智能合约时都应遵循的模板或格式。

ERC20ERC721的区别是可替代性和不可替代性之间的差异。可替代资产是您可以与另一个类似实体交换的资产。另一方面,不可替代的资产正好相反,不能相互交换。例如,房屋很容易被视为不可替代的资产,因为它具有一些独特的属性。当涉及到加密世界时,以数字形式表示资产肯定必须考虑可替代性和不可替代性方面。

什么是NFT智能合约?

NFT智能合约是ERC721代币。它是一种不可替代的代币,称为NFT,是一种数字资产,代表现实世界的对象,如区块链上的艺术、音乐和视频。NFT使用在加密货币区块链上记录和认证的识别码和元数据,这使得区块链上代表的每个NFT都是唯一的资产。与同样记录在区块链上的加密货币不同,NFT不能等价交易或交换,因此它们是不可替代的。智能合约是存在于区块链中的编程。这使网络能够存储NFT交易中指示的信息。智能合约是自动执行的,可以检查合同条款是否得到满足,以及执行条款,而无需中介或中央机构。

什么是坚固性(Solidity)

Solidity是一种面向对象的高级语言,用于实现智能合约。智能合约是管理以太坊状态内账户行为的程序。

Solidity是静态类型的,支持继承、库和复杂的用户定义类型以及其他功能。使用Solidity,您可以创建用于投票、众筹、盲目拍卖和多重签名钱包等用途的合约。

什么是安全帽(Hardhat)

Hardhat是以太坊开发环境。轻松部署合约、运行测试和调试Solidity代码,而无需处理实时环境。Hardhat网络是一个专为开发而设计的本地以太坊网络。

先决条件

NFT代币具有元数据(图像和属性)。它可以存储在IPFS(星际文件系统)或链上。我们将我们的NFT元数据以SVG格式存储在IPFS上。Opensea的店面支持SVG图片格式,合约部署到ethereum区块链mainnettestnet后,可以在其中查NFT

在我们创建NFT合约之前,我们需要将SVG图像上传到IPFS。多亏了PinataCID网站,它使这项工作变得非常容易。转到Pinata网站并创建一个帐户,如果您上传最多1GB的数据,则免费。注册后,您将被带到Pin图管理器窗口。使用界面上传您的文件夹。上传文件夹后,您将获得与之关联的文件夹。它应该看起来像这样:

 

对于我的文件夹,是CIDQmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg。因此,此文件夹的IPFS URLipfs://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是一个完整而紧凑的库,用于从客户端应用程序(如ReactVueAngular)与Ethereum Blockchain及其生态系统进行交互。

MetaMask帮助处理帐户管理并将当前用户连接到区块链。连接他们的MetaMask钱包后,您可以与全球可用的Ethereum API(window.ethereum)进行交互,该API用于识别web3兼容浏览器的用户(如MetaMask用户)。

首先,我们创建一个typescript React应用程序。

npx create-react-app react-web3-ts --template typescript

接下来,切换到新目录并安装ethers.jshardhat

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

什么是ABIABI代表应用程序二进制接口。您可以将其视为客户端应用程序和以太坊区块链之间的接口,您将在其中部署要与之交互的智能合约。

ABIs通常由开发框架(HardHat)Solidity智能合约编译而成。您还经常可以在Etherscan上找到智能合约的ABIs

Hardhat-toolbox包括type-chain。默认情况下,typechain在根文件夹下的typechain-types中生成键入内容。这将导致我们的React客户端编码出现问题。React会抱怨我们应该只从 src 文件夹导入类型。所以我们在hardhat.config.ts中进行了更改。

  1. import { HardhatUserConfig } from "hardhat/config";
  2. import "@nomicfoundation/hardhat-toolbox";
  3. const config: HardhatUserConfig = {
  4. solidity: "0.8.9",
  5. networks: {
  6. hardhat: {
  7. chainId: 1337
  8. }
  9. },
  10. typechain: {
  11. outDir: 'src/typechain-types',
  12. target: 'ethers-v5',
  13. alwaysGenerateOverloads: false, // should overloads with full signatures
  14. // like deposit(uint256) be generated always,
  15. // even if there are no overloads?
  16. externalArtifacts: ['externalArtifacts/*.json'], // optional array of glob
  17. // patterns with external artifacts to process (for example external libs
  18. // from node_modules)
  19. dontOverrideCompile: false // defaults to false
  20. }
  21. };
  22. 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。我们的合同将继承自OpenZeppelinERC721EnumerableOwnable合同。前者除了在处理NFT集合时有用的一些辅助函数外,还具有ERC721(NFT)标准的默认实现。后者允许我们为合同的某些方面添加管理权限。

除此之外,我们还将使用OpenZeppelinSafeMathCounters库分别安全地处理无符号整数算法(通过防止溢出)和令牌ID

这就是我们的合同的样子:

  1. // contracts/NFT.sol
  2. // SPDX-License-Identifier: MIT
  3. pragma solidity ^0.8.9;
  4. import "@openzeppelin/contracts/utils/Counters.sol";
  5. import "@openzeppelin/contracts/access/Ownable.sol";
  6. import "@openzeppelin/contracts/utils/math/SafeMath.sol";
  7. import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
  8. contract NFTCollectible is ERC721Enumerable, Ownable {
  9. using SafeMath for uint256;
  10. using Counters for Counters.Counter;
  11. Counters.Counter private _tokenIds;
  12. uint256 public constant MAX_SUPPLY = 100;
  13. uint256 public constant PRICE = 0.01 ether;
  14. uint256 public constant MAX_PER_MINT = 5;
  15. string public baseTokenURI;
  16. constructor(string memory baseURI) ERC721("NFT Collectible", "NFTC") {
  17. setBaseURI(baseURI);
  18. }
  19. function setBaseURI(string memory _baseTokenURI) public onlyOwner {
  20. baseTokenURI = _baseTokenURI;
  21. }
  22. function _baseURI() internal view virtual override returns (string memory) {
  23. return baseTokenURI;
  24. }
  25. function reserveNFTs() public onlyOwner {
  26. uint256 totalMinted = _tokenIds.current();
  27. require(totalMinted.add(10) < MAX_SUPPLY, "Not enough NFTs");
  28. for (uint256 i = 0; i < 10; i++) {
  29. _mintSingleNFT();
  30. }
  31. }
  32. function mintNFTs(uint _count) public payable {
  33. uint totalMinted = _tokenIds.current();
  34. require(totalMinted.add(_count) <= MAX_SUPPLY, "Not enough NFTs left!");
  35. require(_count >0 && _count <= MAX_PER_MINT,
  36. "Cannot mint specified number of NFTs.");
  37. require(msg.value >= PRICE.mul(_count), "Not enough ether to purchase NFTs.");
  38. for (uint i = 0; i < _count; i++) {
  39. _mintSingleNFT();
  40. }
  41. }
  42. function _mintSingleNFT() private {
  43. uint256 newTokenID = _tokenIds.current();
  44. _safeMint(msg.sender, newTokenID);
  45. _tokenIds.increment();
  46. }
  47. function tokensOfOwner(address _owner) external view returns (uint[] memory) {
  48. uint tokenCount = balanceOf(_owner);
  49. uint[] memory tokensId = new uint256[](tokenCount);
  50. for (uint i = 0; i < tokenCount; i++) {
  51. tokensId[i] = tokenOfOwnerByIndex(_owner, i);
  52. }
  53. return tokensId;
  54. }
  55. function withdraw() public payable onlyOwner {
  56. uint balance = address(this).balance;
  57. require(balance > 0, "No ether left to withdraw");
  58. (bool success, ) = (msg.sender).call{value: balance}("");
  59. require(success, "Transfer failed.");
  60. }
  61. }

我们在构造函数调用中设置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,因为他们正在向此功能发送以太币,我们必须将其标记为应付。

在允许造币厂发生之前,我们需要进行三项检查:

  1. 集合中还剩下足够的NFT供调用者铸造请求的数量。
  2. 调用方要求铸造大于0且小于每笔交易允许的最大NFT数量。
  3. 调用方已发送足够的ether来铸造所需数量的 NFT。

提取余额功能

如果我们无法撤回已发送到合约的以太币,那么到目前为止我们付出的所有努力都将付诸东流。

让我们编写一个函数,允许我们提取合约的全部余额。这显然会被标记为onlyOwner

编译合约

首先,编译我们的新智能合约。

npx hardhat compile

编译完成后,将在 src/typechain-types 文件夹中生成新合约的类型。

您可以在 NFTCollectible__factory.ts 上找到新联系人的ABI

测试合同

删除Lock.ts并在测试文件夹中添加 NFTCollectible.ts。让我们从下面的代码开始。

  1. import { expect } from "chai";
  2. import { ethers } from "hardhat";
  3. import { NFTCollectible } from "../src/typechain-types/contracts/NFTCollectible";
  4. import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
  5. describe("NFTCollectible", function () {
  6. let contract : NFTCollectible;
  7. let owner : SignerWithAddress;
  8. let addr1 : SignerWithAddress;
  9. }

因为每个测试用例都需要部署合约。所以我们把部署写成beforeEach

  1. beforeEach(async function () {
  2. // Get the ContractFactory and Signers here.
  3. [owner, addr1] = await ethers.getSigners();
  4. const contractFactory = await ethers.getContractFactory("NFTCollectible");
  5. contract = await contractFactory.deploy("baseTokenURI");
  6. await contract.deployed();
  7. }); beforeEach(async function () {
  8. // Get the ContractFactory and Signers here.
  9. [owner, addr1] = await ethers.getSigners();
  10. const contractFactory = await ethers.getContractFactory("NFTCollectible");
  11. contract = await contractFactory.deploy("baseTokenURI");
  12. await contract.deployed();
  13. });

现在我们添加事务测试用例。

  • reserveNFTs为所有者保留10个NFT。

  1. it("Reserve NFTs should 10 NFTs reserved", async function () {
  2. let txn = await contract.reserveNFTs();
  3. await txn.wait();
  4. expect(await contract.balanceOf(owner.address)).to.equal(10);
  5. });

  • NFT的价格为0.01ETH,因此需要支付0.03ETH才能铸造3个NFT。

  1. it("Sending 0.03 ether should mint 3 NFTs", async function () {
  2. let txn = await contract.mintNFTs(3,
  3. { value: ethers.utils.parseEther('0.03') });
  4. await txn.wait();
  5. expect(await contract.balanceOf(owner.address)).to.equal(3);
  6. });

  • 铸造NFT时,矿工支付智能合约和汽油费。汽油费归矿工所有,但加密货币归合同而不是所有者所有。

  1. it("Withdrawal should withdraw the entire balance", async function () {
  2. let provider = ethers.provider
  3. const ethBalanceOriginal = await provider.getBalance(owner.address);
  4. console.log("original eth balanace %f", ethBalanceOriginal);
  5. let txn = await contract.connect(addr1).mintNFTs(1,
  6. { value: ethers.utils.parseEther('0.01') });
  7. await txn.wait();
  8. const ethBalanceBeforeWithdrawal = await provider.getBalance(owner.address);
  9. console.log("eth balanace before withdrawal %f", ethBalanceBeforeWithdrawal);
  10. txn = await contract.connect(owner).withdraw();
  11. await txn.wait();
  12. const ethBalanceAfterWithdrawal = await provider.getBalance(owner.address);
  13. console.log("eth balanace after withdrawal %f", ethBalanceAfterWithdrawal);
  14. expect(ethBalanceOriginal.eq(ethBalanceBeforeWithdrawal)).to.equal(true);
  15. expect(ethBalanceAfterWithdrawal.gt
  16. (ethBalanceBeforeWithdrawal)).to.equal(true);
  17. });

运行测试。

npx hardhat test

在本地部署协定

更改scripts\deplot.ts 文件中的主函数。

lock合约部署替换为main函数中的NTFCollectible合约部署。请注意,我们需要将我们的NFT集合基本URL传递给合约的构造函数。

  1. const baseTokenURI = "ipfs://QmPz4f9RY2pwgiQ34UrQ8ZtLf31QTTS8FSSJ9GCWvktXtg/";
  2. // Get contract that we want to deploy
  3. const contractFactory = await ethers.getContractFactory("NFTCollectible");
  4. // Deploy contract with the correct constructor arguments
  5. const contract = await contractFactory.deploy(baseTokenURI);
  6. // Wait for this transaction to be mined
  7. await contract.deployed();
  8. 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, ModalImage List

  • App Bar

App Bar显示与当前屏幕相关的信息和操作。

  • Box

Box组件充当大多数CSS实用程序需求的包装器组件。

  • Stack

Stack组件管理沿垂直或水平轴的直接子项的布局,每个子项之间具有可选的间距和/或分隔符。

  • Modal

Modal组件为创建对话框、弹出框、lightbox或其他任何内容提供了坚实的基础。

  • 图像列表

图像列表在有组织的网格中显示图像集合。

目前,tsconfig.json是由hardhat typescript创建的。它没有完全覆盖React Typescript。更新 tsconfig.json,如下所示:

  1. {
  2. "compilerOptions": {
  3. "target": "es2021",
  4. "lib": [
  5. "dom",
  6. "dom.iterable",
  7. "esnext"
  8. ],
  9. "allowJs": true,
  10. "skipLibCheck": true,
  11. "esModuleInterop": true,
  12. "allowSyntheticDefaultImports": true,
  13. "strict": true,
  14. "forceConsistentCasingInFileNames": true,
  15. "noFallthroughCasesInSwitch": true,
  16. "module": "commonjs",
  17. "moduleResolution": "node",
  18. "resolveJsonModule": true,
  19. "isolatedModules": true,
  20. "noEmit": true,
  21. "outDir": "dist",
  22. "sourceMap": true,
  23. "jsx": "react-jsx"
  24. },
  25. "include": ["./scripts", "./test", "./src/typechain-types"],
  26. "files": ["./hardhat.config.ts"]
  27. }

下载MetaMask.svgsrc文件夹,我们将其用作logo

 src 文件夹下添加 Demo.tsx 文件,然后复制以下代码。我们从基本应用栏开始。

  1. import * as React from 'react';
  2. import AppBar from '@mui/material/AppBar';
  3. import Toolbar from '@mui/material/Toolbar';
  4. import Typography from '@mui/material/Typography';
  5. import CssBaseline from '@mui/material/CssBaseline';
  6. import Container from '@mui/material/Container';
  7. import Stack from '@mui/material/Stack';
  8. import Avatar from '@mui/material/Avatar';
  9. import logo from './metamask.svg';
  10. function Demo() {
  11. return (
  12. <React.Fragment>
  13. <CssBaseline />
  14. <AppBar>
  15. <Toolbar>
  16. <Stack direction="row" spacing={2}>
  17. <Typography variant="h3" component="div">
  18. NFT Collection
  19. </Typography>
  20. <Avatar alt="logo" src={logo} sx={{ width: 64, height: 64 }} />
  21. </Stack>
  22. </Toolbar>
  23. </AppBar>
  24. <Toolbar />
  25. <Container>
  26. </Container>
  27. </React.Fragment>
  28. );
  29. }
  30. export default Demo;

然后更改 index.tsx 以加载演示而不是应用程序。

  1. import React from 'react';
  2. import ReactDOM from 'react-dom/client';
  3. import './index.css';
  4. import App from './App';
  5. import Demo from './Demo';
  6. import reportWebVitals from './reportWebVitals';
  7. const root = ReactDOM.createRoot(
  8. document.getElementById('root') as HTMLElement
  9. );
  10. root.render(
  11. <React.StrictMode>
  12. <Demo />
  13. </React.StrictMode>
  14. );
  15. // If you want to start measuring performance in your app, pass a function
  16. // to log results (for example: reportWebVitals(console.log))
  17. // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
  18. reportWebVitals();

运行它。

npm start

连接钱包

让我们仔细检查我们的NFT集合合约部署的地址。它是0x5FbDB2315678afecb367f032d93F642f64180aa3。定义第一个const

const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";

然后定义IWallet接口。

  1. interface IWallet {
  2. iconColor: string;
  3. connectedWallet: string;
  4. contractAddress: string;
  5. contractSymbol: string;
  6. contractBaseTokenURI: string;
  7. contractOwnerAddress: string;
  8. contractPrice: string;
  9. isOwner: boolean;
  10. }

我们需要使用useState hook来初始化和更新IWallet实例。React16.8中引入了HooksuseState是一个内置钩子,允许您在函数组件中使用本地状态。将初始状态传递给此函数,它将返回一个具有当前状态值(不一定是初始状态)的变量和另一个函数来更新此值。

  1. const [state, setState] = React.useState<IWallet>({
  2. iconColor: "disabled",
  3. connectedWallet: "",
  4. contractSymbol: "",
  5. contractAddress: "",
  6. contractBaseTokenURI: "",
  7. contractOwnerAddress: "",
  8. contractPrice: "",
  9. isOwner: false
  10. });

导入ethersNFTCollectible__factory

  1. import { BigNumber, ethers } from "ethers";
  2. import { NFTCollectible__factory } from
  3. './typechain-types/factories/contracts/NFTCollectible__factory'

现在写连接钱包功能。

  1. const connectWallet = async () => {
  2. try {
  3. console.log("connect wallet");
  4. const { ethereum } = window;
  5. if (!ethereum) {
  6. alert("Please install MetaMask!");
  7. return;
  8. }
  9. const accounts = await ethereum.request({
  10. method: "eth_requestAccounts",
  11. });
  12. console.log("Connected", accounts[0]);
  13. const provider = new ethers.providers.Web3Provider(ethereum);
  14. const contract = NFTCollectible__factory.connect
  15. (contractAddress, provider.getSigner());
  16. //const contract = new ethers.Contract
  17. //(contractAddress, NFTCollectible__factory.abi, signer) as NFTCollectible;
  18. const ownerAddress = await contract.owner();
  19. const symbol = await contract.symbol();
  20. const baseTokenURI = await contract.baseTokenURI();
  21. const balance = await (await contract.balanceOf(accounts[0])).toNumber();
  22. const ethBalance = ethers.utils.formatEther
  23. (await provider.getBalance(accounts[0]));
  24. const isOwner = (ownerAddress.toLowerCase() === accounts[0].toLowerCase());
  25. const price = ethers.utils.formatEther(await contract.PRICE());
  26. setState({
  27. iconColor: "success",
  28. connectedWallet: accounts[0],
  29. contractSymbol: symbol,
  30. contractAddress: contract.address,
  31. contractBaseTokenURI: baseTokenURI,
  32. contractOwnerAddress: ownerAddress,
  33. contractPrice: `${price} ETH`,
  34. isOwner: isOwner
  35. });
  36. console.log("Connected", accounts[0]);
  37. } catch (error) {
  38. console.log(error);
  39. }
  40. };

你可以在这里看到,必须安装MetaMask扩展,否则你无法从窗口中获取Ethereum对象。进行UI更改,添加连接按钮和已连接帐户合同地址合同基础令牌URI”文本字段。此外,使用Account Circle图标指示是否已连接。

  1. <Stack direction="row" spacing={2} sx={{ margin: 5 }}>
  2. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  3. <AccountCircle color={state.iconColor} sx={{ mr: 1, my: 0.5 }} />
  4. <TextField id="wallet_address" label="Connected Account"
  5. sx={{ width: 300 }} variant="standard" value={state.connectedWallet}
  6. inputProps={{ readOnly: true, }}
  7. />
  8. </Box>
  9. <TextField id="contract_symbol" label="Contract Symbol"
  10. vari-ant="standard" value={state.contractSymbol}
  11. inputProps={{ readOnly: true, }}
  12. />
  13. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  14. <TextField id="contract_address" label="Contract Address"
  15. sx={{ width: 400 }} variant="standard" value={state.contractAddress}
  16. inputProps={{ readOnly: true, }}
  17. />
  18. </Box>
  19. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  20. <TextField id="contract_baseURI" label="Contract Base Token URI"
  21. sx={{ width: 500 }} variant="standard" value={state.contractBaseTokenURI}
  22. inputProps={{ readOnly: true, }}
  23. />
  24. </Box>
  25. </Stack>

以下是使用Stack的外观。堆栈有两个方向,rowcolumn

运行我们的应用。

单击连接按钮。

呵呵,工作一样迷人!

加载NFT集合

为图像URL集合添加状态hook

const [nftCollection, setNFTCollection] = React.useState<string[]>([]);

写入加载NFT集合函数。

  1. const loadNFTCollection = async () => {
  2. try {
  3. console.log("load NFT collection");
  4. let baseURI: string = state.contractBaseTokenURI;
  5. baseURI = baseURI.replace("ipfs://", "https://gateway.pinata.cloud/ipfs/");
  6. setNFTCollection(
  7. [
  8. `${baseURI}0001.svg`,
  9. `${baseURI}0002.svg`,
  10. `${baseURI}0003.svg`,
  11. `${baseURI}0004.svg`,
  12. ]);
  13. } catch (error) {
  14. console.log(error);
  15. }
  16. };

导入ImageListImageListItem

  1. import ImageList from '@mui/material/ImageList';
  2. import ImageListItem from '@mui/material/ImageListItem';

将图像列表与NFT URL集合绑定。

  1. <ImageList sx={{ width: 500, height: 450 }} cols={3} rowHeight={164}>
  2. {nftCollection.map((item) => (
  3. <ImageListItem key={item}>
  4. <img
  5. src={`${item}?w=164&h=164&fit=crop&auto=format`}
  6. srcSet={`${item}?w=164&h=164&fit=crop&auto=format&dpr=2 2x`}
  7. loading="lazy"
  8. />
  9. </ImageListItem>
  10. ))}
  11. </ImageList>

再一次运行应用程序并单击加载NFT集合按钮。

Mint NFT

添加IService接口。

  1. interface IService {
  2. account: string;
  3. ethProvider?: ethers.providers.Web3Provider,
  4. contract?: NFTCollectible;
  5. currentBalance: number;
  6. ethBalance: string;
  7. mintAmount: number;
  8. }

使用状态挂钩。

  1. const [service, setService] = React.useState<IService>({
  2. account: "",
  3. currentBalance: 0,
  4. ethBalance: "",
  5. mintAmount: 0
  6. });

Mint NFT功能。

  1. const mintNFTs = async () => {
  2. try {
  3. console.log("mint NFTs");
  4. const address = service.account;
  5. const amount = service.mintAmount!;
  6. const contract = service.contract!;
  7. const price = await contract.PRICE();
  8. const ethValue = price.mul(BigNumber.from(amount));
  9. const signer = service.ethProvider!.getSigner();
  10. let txn = await contract.connect(signer!).mintNFTs(amount, { value: ethValue });
  11. await txn.wait();
  12. const balance = await contract.balanceOf(address);
  13. setService({...service, currentBalance: balance.toNumber(), mintAmount: 0});
  14. } catch (error) {
  15. console.log(error);
  16. }
  17. };

Mint模态对话框。

  1. <Modal
  2. aria-labelledby="transition-modal-title"
  3. aria-describedby="transition-modal-description"
  4. open={open}
  5. onClose={handleClose}
  6. closeAfterTransition
  7. BackdropComponent={Backdrop}
  8. BackdropProps={{
  9. timeout: 500,
  10. }}
  11. >
  12. <Fade in={open}>
  13. <Box sx={modalStyle}>
  14. <Stack spacing={1} sx={{ width: 500 }}>
  15. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  16. <TextField id="mint_account" label="Account"
  17. sx={{ width: 500 }} variant="standard" value={service.account}
  18. inputProps={{ readOnly: true}}
  19. />
  20. </Box>
  21. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  22. <TextField id="price" label="NFT Price"
  23. sx={{ width: 500 }} variant="standard" value={state.contractPrice}
  24. inputProps={{ readOnly: true}}
  25. />
  26. </Box>
  27. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  28. <TextField id="balance" label="Balance"
  29. sx={{ width: 500 }} variant="standard" value={service.currentBalance}
  30. type = "number" inputProps={{ readOnly: true}}
  31. />
  32. </Box>
  33. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  34. <TextField id="mint_amount" type="number"
  35. label="Mint Amount" sx={{ width: 500 }}
  36. variant="standard" value={service.mintAmount}
  37. onChange={event => {
  38. const { value } = event.target;
  39. const amount = parseInt(value);
  40. setService({...service, mintAmount: amount});
  41. }}
  42. />
  43. </Box>
  44. <Stack direction="row" spacing={2} sx={{ margin: 5 }}>
  45. <Button variant="outlined" onClick={mintNFTs}>Mint</Button>
  46. <Button variant="outlined" onClick={handleClose}>close</Button>
  47. </Stack>
  48. </Stack>
  49. </Box>
  50. </Fade>
  51. </Modal>

运行应用程序,单击Mint NFT按钮。您将看到一个弹出对话框。

提取

Withdraw功能。

  1. const withdraw = async () => {
  2. try {
  3. console.log("owner withdraw");
  4. const contract = service.contract!;
  5. const provider = service.ethProvider!;
  6. let txn = await contract.withdraw();
  7. await txn.wait();
  8. const ethBalance = ethers.utils.formatEther
  9. (await provider!.getBalance(service.account));
  10. setService({...service, ethBalance: `${ethBalance} ETH`});
  11. } catch (error) {
  12. console.log(error);
  13. }
  14. };

Withdraw模态对话,

  1. <Modal
  2. id="withdrawal_modal"
  3. aria-labelledby="transition-modal-title"
  4. aria-describedby="transition-modal-description"
  5. open={openWithdrawal}
  6. onClose={handleCloseWithdrawal}
  7. closeAfterTransition
  8. BackdropComponent={Backdrop}
  9. BackdropProps={{
  10. timeout: 500,
  11. }}
  12. >
  13. <Fade in={openWithdrawal}>
  14. <Box sx={modalStyle}>
  15. <Stack spacing={1} sx={{ width: 500 }}>
  16. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  17. <TextField id="owner_account" label="Owner Account"
  18. sx={{ width: 500 }} variant="standard" value={service.account}
  19. inputProps={{ readOnly: true }}
  20. />
  21. </Box>
  22. <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
  23. <TextField id="ethbalance" label="ETH Balance"
  24. sx={{ width: 500 }} variant="standard" value={service.ethBalance}
  25. inputProps={{ readOnly: true }}
  26. />
  27. </Box>
  28. <Stack direction="row" spacing={2} sx={{ margin: 5 }}>
  29. <Button variant="outlined" onClick={withdraw}>Withdraw</Button>
  30. <Button variant="outlined" onClick={handleCloseWithdrawal}>close</Button>
  31. </Stack>
  32. </Stack>
  33. </Box>
  34. </Fade>
  35. </Modal>

Withdraw仅适用于部署合同的合同所有者。因此,提取按钮仅对所有者启用。

  1. <Button variant="contained" disabled={!state.isOwner}
  2. 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和演示组件。

  1. import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
  2. import Link from '@mui/material/Link';
  3. import Demo from './Demo'

App.tsx中创建Home函数。使用Link让用户更改URLuseNavigate自己更改。这里我们使用的Link来自Material UI而不是来自react-router-dom。本质上,除了样式之外,它们是一回事。

  1. function Home() {
  2. return (
  3. <div className="App">
  4. <header className="App-header">
  5. <img src={logo} className="App-logo" alt="logo" />
  6. <p>
  7. With cryptocurrencies and blockchain technology,
  8. NFTs have become part of this crazy world.
  9. </p>
  10. <Box
  11. sx={{
  12. display: 'flex', flexWrap: 'wrap',
  13. justifyContent: 'center', typography: 'h3',
  14. '& > :not(style) + :not(style)': {
  15. ml: 2,
  16. },
  17. }}>
  18. <Link href="/demo" underline="none" sx={{ color: '#FFF' }}>
  19. Web3 NFT Demo
  20. </Link>
  21. </Box>
  22. </header>
  23. </div>
  24. );
  25. }

App函数中配置路由。

  1. function App() {
  2. return (
  3. <Router>
  4. <Routes>
  5. <Route path="/" element={<Home />} />
  6. <Route path="/demo" element={<Demo />} />
  7. </Routes>
  8. </Router>
  9. );
  10. }

React Router以前的版本中,您必须以某种方式对路由进行排序,以便在多个路由与不明确的URL匹配时获得正确的路由进行呈现。V6要聪明得多,并且会选择最具体的匹配项,因此您不必再担心这一点。

不要忘记最后一件事,改回index.tsx中的App组件。

  1. root.render(
  2. <React.StrictMode>
  3. <App />
  4. </React.StrictMode>
  5. );

现在运行我们的应用程序。

npm start

单击 Wbe3 NFT演示,它将导航到我们的web3组件。

如何使用源代码

首先,在您的浏览器(ChromeFirefoxEdge)上安装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应用程序。我希望你学到了很多东西。现在你可能会问,智能合约有什么用?

智能合约相对于集中式系统的优势:

  1. 数据不能更改或篡改。因此,恶意行为者几乎不可能操纵数据。
  2. 它是完全去中心化的。
  3. 与任何集中支付钱包不同,您无需向中间人支付任何佣金百分比即可进行交易。
  4. 最重要的是,智能合约可能会为您的财务自由打开一扇门。

示例项目现在在github中,dapp-ts

https://www.codeproject.com/Articles/5338801/Build-NFT-Collection-Web3-Application-with-Hardha

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/717103
推荐阅读
相关标签
  

闽ICP备14008679号