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

目录

介绍

什么是以太坊(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中进行了更改。

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。我们的合同将继承自OpenZeppelinERC721EnumerableOwnable合同。前者除了在处理NFT集合时有用的一些辅助函数外,还具有ERC721(NFT)标准的默认实现。后者允许我们为合同的某些方面添加管理权限。

除此之外,我们还将使用OpenZeppelinSafeMathCounters库分别安全地处理无符号整数算法(通过防止溢出)和令牌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,因为他们正在向此功能发送以太币,我们必须将其标记为应付。

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

  1. 集合中还剩下足够的NFT供调用者铸造请求的数量。
  2. 调用方要求铸造大于0且小于每笔交易允许的最大NFT数量。
  3. 调用方已发送足够的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, ModalImage 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.svgsrc文件夹,我们将其用作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实例。React16.8中引入了HooksuseState是一个内置钩子,允许您在函数组件中使用本地状态。将初始状态传递给此函数,它将返回一个具有当前状态值(不一定是初始状态)的变量和另一个函数来更新此值。

const [state, setState] = React.useState<IWallet>({
    iconColor: "disabled",
    connectedWallet: "",
    contractSymbol: "",
    contractAddress: "",
    contractBaseTokenURI: "",
    contractOwnerAddress: "",
    contractPrice: "",
    isOwner: false
});

导入ethersNFTCollectible__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的外观。堆栈有两个方向,rowcolumn

运行我们的应用。

单击连接按钮。

呵呵,工作一样迷人!

加载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);
  }
};

导入ImageListImageListItem

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让用户更改URLuseNavigate自己更改。这里我们使用的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组件。

如何使用源代码

首先,在您的浏览器(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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值