免费手把手教你Web3入门实战

833 阅读13分钟

前置知识

要学习本课程,首先需要知道几个概念:

  • 智能合约:类似于双方交易的合同,一旦部署就不能修改了,运行在区块链上
  • ERC20 代币:同质化代币
  • NFT:非同质化代币,类似于数字纪念品
  • 测试网:用于开发人员部署和测试智能合约,例如 Sepolia、Avalanche Fujid 等等
  • Solidity:大部分智能合约所使用的编程语言,这里默认您已经熟练它的语法,如果没学过推荐这里

技术栈:Nextjs + tailwind + solidity + ethersjs + rainbowkit + wagmi + hardhat

项目介绍:用户可以上下架自己的 NFT 并且还可以用 ERC20 代币去购买 NFT

ERC20

ERC20代币是基于以太坊区块链上的特定类型的加密货币代币。 ERC20标准定义了一系列代币必须遵守的规则,以便与以太坊网络上的其他代币或应用程序兼容。

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

上面是 ERC20 协议的接口定义,说明一下每个字段的含义:

  • totalSupply:总发行量
  • balanceOf:账户余额
  • transfer:转账
  • allowance,approve:授权
  • transferFrom:转账

但是如果让我们自己去实现一个 ERC20 合约还是有些费劲,而且不可能每个人要部署合约的时候都去实现一套吧,那么这里就可以直接用 npm 包去安装一个 @openzeppelin/contracts ,从包里面可以导入 ERC20 合约

ERC721

ERC721-非同质化代币标准,同样可以从 @openzeppelin/contracts 中导入,而且它里面大部分也是这些方法,这里就不详细赘述了

ERC1155

ERC1155 是结合了 ERC20 和 ERC721 两者的,它既发行同质化代币也发行非同质化代币,具体实现就是将一个 NFT 拆分成了无数小块,这样既具备同质化代币的特征也具备非同质化代币特征,具体实现在上一篇文章 RWA 中有具体介绍,这里不再赘述

ERC20 合约初始化

  1. 首先要给自己的代币取个名字,绞尽脑汁想出一个名字,就叫他 YongLeToken,简称 YT
  2. Solidity 不支持小数,所以需要用整数去模拟小数,需要指定 decimals,也就是小数固定有几位
  3. 初始化铸造代币,发行量设置为 10000000000

代码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract YongLeToken is ERC20{
    constructor() ERC20("YongLeToken","YT") {
        // 初始化给合约 mint 这么多 token
        _mint(msg.sender, 10000000000*10**6);
    }

    function decimals()public  view  virtual  override  returns (uint8) {
        return  6;
    }
}

NFTToken 合约开发

  1. NFT 中必须要有一个 tokenId,这个 id 可以是自增的,所以我们需要一个计数器,从 @openzeppelin/contracts/utils/Counters.sol 中可以获得一个计数器
  2. 还是取名,自己想取什么名字就取什么名字吧

代码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFTToken is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;

    // 每 mint 一个 nft 都需要把 count+1 表示 nft 总数量
    constructor() ERC721("NFTToken", "NT") {
        _tokenIdCounter.increment();
    }

    function mint(address to) public {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }
}

就这样三下两除二,我们就发行了自己的 ERC20 代币和 NFT,是不是感觉自己很牛啊!这是因为我们站在了别人的肩膀上。继续,好戏还在后面呢!

NFTMarket 合约开发

  1. 首先要拿到 YongLeToken 用来交易 NFT
  2. 其次就是依次实现上架 NFT、下架 NFT、购买 NFT 的功能,如何定义这些方法的参数呢?上架 NFT 的时候肯定需要一个价格、tokenId、NFTToken 合约地址和 cid

解释一下 cid,链上的存储空间非常昂贵,所以 NFT 不可能把所有数据都存储在链上,它智能存储一个地址的映射,然后通过其他的去中心化的服务去存储资源(例如图片、视频等),那么这个 cid 就是映射资源的 hash。存储静态资源我用的是 Pinata

然后需要用一个 mapping 来存储 tokenId 和 NFT 数据的映射。代码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; // 防止重入攻击
import "hardhat/console.sol";

error NftMarket__NotOwnerOf(string message);
error NftMarket__NotApproved(string message);
error NftMarket__NotActive(string message);
error NftMarket__PaymentFailed(string message);
error NftMarket__NotExist(string message);
error NftMarket__IsListed(string message);

contract NftMarket is ReentrancyGuard {
    struct Listing {
        address seller;
        address nftContract;
        uint256 tokenId;
        uint256 price;
        uint256 listTime;
        string cid;
        bool isActive;
    }

    mapping(address => mapping(uint256 => Listing)) public s_listings;
    uint256[] private s_tokenIds;
    // Listing[] private s_listArr;
    IERC20 public immutable i_paymentToken;

    // 上架 nft 事件
    event NftListed(
        address indexed seller,
        uint256 indexed tokenId,
        address indexed nftContract,
        uint256 price
    );

    // nft购买事件
    event NftPurchased(
        address indexed buyer,
        uint256 indexed tokenId,
        address indexed nftContract,
        uint256 price
    );

    event NftUnlisted(
        address indexed seller,
        uint256 indexed tokenId,
        address indexed nftContract
    );

    constructor(address _paymentToken) {
        i_paymentToken = IERC20(_paymentToken);
    }

    /**
    * 1. 初始化一个 nft 对象,传入 nft 合约地址,为什么不用 ERC721 呢?因为 ERC721 比 IERC721 要大得多,多了很多无用的方法,只需要引入 IERC721 即可
      2. ownerOf 用来判断 nft 的归属
      3. 关于approve,合约中所有的代币操作都需要用户给合约授权,授权之后才会执行合约
      4. revert 比 require 耗费更少的 gas,require 底层是用 revert 实现的
      5. nonReentrant避免重入攻击,重入攻击就是攻击者能够循环调用某个方法,关于重入攻击可以参考https://github.com/AmazingAng/WTF-Solidity/blob/main/S01_ReentrancyAttack/readme.md
    /
    function listNft(
        address _nftContract,
        uint256 tokenId,
        uint256 price,
        string memory cid
    ) external {
        IERC721 nft = IERC721(_nftContract);
        if (nft.ownerOf(tokenId) != msg.sender) {
            revert NftMarket__NotOwnerOf("not the owner of the NFT");
        }
        if (!nft.isApprovedForAll(msg.sender, address(this))) {
            revert NftMarket__NotApproved("not approved");
        }
        if (s_listings[_nftContract][tokenId].isActive) {
            revert NftMarket__IsListed("NFT already listed");
        }
        uint256 listTime = block.timestamp;
        Listing memory item = Listing(
            msg.sender,
            _nftContract,
            tokenId,
            price,
            listTime,
            cid,
            true
        );
        s_listings[_nftContract][tokenId] = item;
        s_tokenIds.push(tokenId);
        // s_listArr.push(item);

        emit NftListed(msg.sender, tokenId, _nftContract, price);
    }

    function unlistNft(address _nftContract, uint256 _tokenId) external {
        Listing memory item = s_listings[_nftContract][_tokenId];
        if (!item.isActive) {
            revert NftMarket__NotActive("not active");
        }
        IERC721 nft = IERC721(_nftContract);
        if (nft.ownerOf(_tokenId) != msg.sender) {
            revert NftMarket__NotOwnerOf("not the owner of the NFT");
        }
        if (!nft.isApprovedForAll(msg.sender, address(this))) {
            revert NftMarket__NotApproved("not approved");
        }
        s_listings[_nftContract][_tokenId].isActive = false;
        emit NftUnlisted(msg.sender, _tokenId, _nftContract);
    }

    function buyNft(address _nftContract, uint256 _tokenId) external nonReentrant {
        Listing memory listing = s_listings[_nftContract][_tokenId];
        if (!listing.isActive) {
            revert NftMarket__NotActive("NFT not active");
        }
        IERC721 nft = IERC721(_nftContract);
        // 将购买人身上的钱转给售卖人
        if (
            !i_paymentToken.transferFrom(
                msg.sender,
                listing.seller,
                listing.price
            )
        ) {
            revert NftMarket__PaymentFailed("payment failed");
        }
        nft.safeTransferFrom(listing.seller, msg.sender, _tokenId);
        s_listings[_nftContract][_tokenId].isActive = false;
        emit NftPurchased(msg.sender, _tokenId, _nftContract, listing.price);
    }

    /**
     * 合约中不能直接返回 mapping,所以只能先记录下所有的 tokenId,然后从 listings 拼装数组返回
     * @dev 从 s_listings 中获取上架的 NFT
     */
    function getListsArray(
        address _nftContract
    ) public view returns (Listing[] memory) {
        Listing[] memory listings = new Listing[](s_tokenIds.length);
        for (uint256 i = 0; i < s_tokenIds.length; i++) {
            Listing memory item = s_listings[_nftContract][s_tokenIds[i]];
            listings[i] = item;
        }
        return listings;
    }
}

避免重入攻击有多重要,看个数据就知道:22年之前几乎每一年都有资产损失,而且超过几十万甚至上百万,还导致了以太坊硬分叉。

所以说要学好 WEB3,安全这一块不能少。

部署合约

下面是合约项目的 packagejson:

{
  "name": "task3",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "compile": "hardhat compile",
    "hardhat-node": "hardhat node",
    "deploy": "hardhat deploy",
    "test": "hardhat test",
    "test-staging": "hardhat test --network sepolia",
    "coverage": "hardhat coverage"
  },
  "dependencies": {
    "@openzeppelin/contracts": "4"
  },
  "devDependencies": {
    "@nomiclabs/hardhat-ethers": "^2.2.3",
    "chai": "4",
    "dotenv": "^16.4.5",
    "ethereum-waffle": "*",
    "ethers": "^5.0.0",
    "hardhat": "^2.22.15",
    "hardhat-deploy": "^0.14.0",
    "solidity-coverage": "^0.8.13"
  }
}

按照 packagejson 中的内容安装 hardhat 相关的依赖,并创建 hardhat.config.js,要执行 hardhat 命令,必须先引入指令,例如要部署合约,必须引入 hardhat-deploy,然后命令行就可以执行 hardhat deploy

require("dotenv").config();
require("hardhat-deploy");
require("solidity-coverage");
require("@nomiclabs/hardhat-ethers");

/**
 * @type {import('hardhat/config').HardhatUserConfig}
 */
const config = {
  solidity: "0.8.20",
  defaultNetwork: "hardhat",
  networks: {
    localhost: {
      chainId: 31337,
      gas: 30_000_000,
      gasPrice: 8000000000,
    },
    hardhat: {
      chainId: 31337,
      gas: 30_000_000,
      gasPrice: 8000000000,
    },
  },
  namedAccounts: {
    deployer: {
      default: 0,
    },
  },
  gasReporter: {
    enabled: process.env.REPORT_GAS !== undefined,
    currency: "USD",
    outputFile: "gas-report.txt",
    noColors: true,
    coinmarketcap: process.env.COINMARKETCAP_API_KEY,
  },
};

module.exports = config;

编写部署脚本:

// deploy-nftToken.js
module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy } = deployments;
  const { deployer } = await getNamedAccounts();
  await deploy("NftToken", {
    from: deployer,
    log: true,
    waitConfirmations: 1,
  });
};

module.exports.tags = ["NftToken"];

// deploy-nftMarket.js
module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy } = deployments;
  const { deployer } = await getNamedAccounts();
  const MissopContract = await deploy("MissopToken", {
    from: deployer,
    log: true,
    waitConfirmations: 1,
  });
  await deploy("NftMarket", {
    from: deployer,
    log: true,
    waitConfirmations: 1,
    args: [MissopContract.address],
  });
};

module.exports.tags = ["NftMarket"];

运行yarn hardhat-node启动本地区块链服务,这样合约在本地就部署成功了!接下来开发前端页面:

前端页面开发

  1. 初始化 nextjs 应用: npx create-next-app@latest

  2. 创建 assets 文件,从合约打包文件中拷贝 abi json 文件到 assets/abi 目录下

  3. 定义.env文件,.env是本地文件不上传仓库,里面含有所有合约的地址以及一些私密配置

  4. nextjs配置读取 env

import dotenv from "dotenv";
const parsedEnv = dotenv.config().parsed;

/** @type {import('next').NextConfig} */
const nextConfig = {
    env: parsedEnv,
}
expor default nextConfig;
  1. 定义与合约交互的 hooks,wagmi 提供了读写合约的方法:useReadContract, useWriteContract
import { useAccount, useReadContract, useWriteContract } from "wagmi";

import MissopToken from "@/assets/abi/MissopToken.json";
import NftToken from "@/assets/abi/NftToken.json";
import NftMarket from "@/assets/abi/NftMarket.json";

const NftAbi = NftToken.abi;
const NftMarketAbi = NftMarket.abi;
const NftTokenContractAddress = process.env.NftTokenContractAddress;
const NftMarketContractAddress = process.env.NftMarketContractAddress;
const MissopTokenAddress = process.env.MissopTokenContractAddress;

const NFTContractParams = {
  abi: NftAbi,
  address: NftTokenContractAddress,
};
const NFTMarketParams = {
  abi: NftMarketAbi,
  address: NftMarketContractAddress,
};
const MissopTokenParams = {
  abi: MissopToken.abi,
  address: MissopTokenAddress,
};
console.log(MissopTokenParams, NFTMarketParams, NFTContractParams);

/**
 * 写入操作所有方法
 * @returns
 */
export function useWrite() {
  const account = useAccount();
  const { writeContractAsync } = useWriteContract();

  /**
   * 上架 NFT
   * @param {*} tokenId
   * @param {*} price
   * @returns
   */
  async function listNFT(tokenId, price, cid) {
    try {
      return await writeContractAsync({
        functionName: "listNft",
        args: [NftTokenContractAddress, tokenId, price, cid],
        ...NFTMarketParams,
      });
    } catch (error) {
      console.log("error", error);
    }
  }

  /**
   * 铸造 NFT
   * @returns
   */
  async function mintNFT() {
    try {
      return await writeContractAsync({
        functionName: "mint",
        args: [account.address],
        ...NFTContractParams,
      });
    } catch (error) {
      console.log("error", error);
    }
  }

  /**
   * 调用setApprovalForAll,参数为NFTMarket 合约地址和 true
   * @returns
   */
  async function setApprovalForAll() {
    try {
      return await writeContractAsync({
        functionName: "setApprovalForAll",
        args: [NftMarketContractAddress, true],
        ...NFTContractParams,
      });
    } catch (error) {
      console.log("error", error);
    }
  }

  /**
   * 授权NFTMarket 合约使用 MT 代币额度,因此参数为NFTMarket 合约地址
   * @param {*} params
   */
  async function approveAmount(amount) {
    try {
      return await writeContractAsync({
        functionName: "approve",
        args: [NftMarketContractAddress, amount],
        ...MissopTokenParams,
      });
    } catch (error) {
      console.log("error", error);
    }
  }

  /**
   * 购买 NFT
   * @param {*} _tokenId
   * @returns
   */
  async function buyNFT(_tokenId) {
    try {
      return await writeContractAsync({
        functionName: "buyNft",
        args: [NftTokenContractAddress, _tokenId],
        ...NFTMarketParams,
      });
    } catch (error) {
      console.log("error", error);
    }
  }

  async function unlistNFT(_tokenId) {
    try {
      return await writeContractAsync({
        functionName: "unlistNft",
        args: [NftTokenContractAddress, _tokenId],
        ...NFTMarketParams,
      });
    } catch (error) {
      console.log("error", error);
    }
  }

  return {
    listNFT,
    buyNFT,
    mintNFT,
    approveAmount,
    setApprovalForAll,
    unlistNFT,
  };
}

/**
 * 获取所有上架的 NFT
 * @returns
 */
export function useGetListsNFTs() {
  const { data, error } = useReadContract({
    ...NFTMarketParams,
    functionName: "getListsArray",
    args: [NftTokenContractAddress],
  });

  console.log("error", error);

  return data;
}

export function useListings(tokenId) {
  const { data, error } = useReadContract({
    ...NFTMarketParams,
    functionName: "s_listings",
    args: [NftTokenContractAddress, tokenId],
  });

  console.log("error", error);

  return data;
}

  1. wagmi配置
import { http } from "wagmi";
import { hardhat } from "wagmi/chains";
import { getDefaultConfig } from "@rainbow-me/rainbowkit";

export default getDefaultConfig({
  appName: "My RainbowKit App",
  projectId: "YOUR_PROJECT_ID",
  chains: [hardhat],
  ssr: true, // If your dApp uses server side rendering (SSR)
  transports: {
    [hardhat.id]: http(),
  },
});

  1. 定义 providers,由于 wagmi 依赖于 react-query 所以我们也需要安装它,然后我们还需要一个连接钱包的组件:rainbowkit
"use client";

import { WagmiProvider } from "wagmi";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import config from "./wagmi";

import "@rainbow-me/rainbowkit/styles.css";

const queryClient = new QueryClient();

export default function Provider({ children }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider>{children}</RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}

  1. 所有准备工作完毕之后,先画一个简单的原型,实现上述所有功能即可:

截屏2024-11-09 09.04.48.png

  1. 编写 Header 顶部栏组件
"use client";

import { ConnectButton } from "@rainbow-me/rainbowkit";

export default function Header(){
    return (
        <div className="flex justify-between">
            <span className="text-3xl font-bold">Missop NFT 市场</span>
            <ConnectButton />
        </div>
    )
}
  1. 左边展示商家的 NFT 列表ListNFTs组件,右边展示操作按钮Opertaions 组件

ListNFTs组件

import { useGetListsNFTs, useListings, useWrite } from "@/hooks/contract";
import { waitForTransactionReceipt } from "@wagmi/core";
import React from "react";
import { useAccount } from "wagmi";
import config from "../wagmi";
import Image from "next/image";

import { fromUnixTime, formatDate } from "date-fns";

export default function ListNFTs() {
  const { address } = useAccount();
  const listNFTs = useGetListsNFTs() || [];
  console.log("listNFTs", listNFTs);
  const data = useListings(listNFTs?.[0]?.tokenId?.toString?.());
  console.log("data", data);
  const { approveAmount, buyNFT, unlistNFT, setApprovalForAll } = useWrite();
  return (
    <div className="flex gap-4 flex-wrap flex-1">
      {listNFTs
        .filter((el) => el.isActive)
        .map((nft, index) => (
          <div key={index} className="rounded-md w-1/4 p-4 shadow-lg">
            <Image
              width={275}
              height={228}
              src={`https://bronze-elderly-pheasant-478.mypinata.cloud/ipfs/${nft.cid}`}
              alt={"图片"}
            />
            <h2>NFT:#{nft.tokenId.toString()}</h2>
            <p title={nft.cid} className="overflow-hidden overflow-ellipsis whitespace-nowrap">
              cid:{nft.cid}
            </p>
            <p title={nft.seller} className="overflow-hidden overflow-ellipsis whitespace-nowrap">
              持有者:{nft.seller}
            </p>
            <p>价格:{nft.price.toString()}MT</p>
            <p>
              上架时间:{nft.listTime ? formatDate(fromUnixTime(nft.listTime.toString()), "yyyy-MM-dd HH:mm:ss") : ""}
            </p>
            {nft.seller === address ? (
              <button
                className="bg-blue-500 text-white px-4 py-2 rounded-md mt-4"
                onClick={async () => {
                  const isConfirm = window.confirm("确定下架?");
                  if (isConfirm) {
                    // 授予操作权限
                    const approvalHash = await setApprovalForAll();
                    if (approvalHash) {
                      console.log("setApprovalForAll processing......");
                      await waitForTransactionReceipt(config, {
                        hash: approvalHash,
                      });
                      console.log("setApprovalForAll success");
                    }
                    const unlistHash = await unlistNFT(nft.tokenId.toString());
                    if (unlistHash) {
                      console.log("unlistNFT processing......");
                      await waitForTransactionReceipt(config, {
                        hash: unlistHash,
                      });
                      alert("unlistNFT success");
                    }
                  }
                }}
              >
                下架
              </button>
            ) : (
              <button
                className="bg-blue-500 text-white px-4 py-2 rounded-md mt-4"
                onClick={async () => {
                  // 授予额度权限
                  const approveHash = await approveAmount(1000000);
                  if (approveHash) {
                    console.log("approveAmount processing......");
                    await waitForTransactionReceipt(config, {
                      hash: approveHash,
                    });
                    console.log("approveAmount success");
                  }
                  const buyHash = await buyNFT(nft.tokenId.toString());
                  if (buyHash) {
                    console.log("buyNFT processing......");
                    await waitForTransactionReceipt(config, {
                      hash: buyHash,
                    });
                    alert("buyNFT success");
                  }
                }}
              >
                购买
              </button>
            )}
          </div>
        ))}
    </div>
  );
}

上面这个组件最关键的点在于请求完区块链之后不能立即提示操作完成,需要等待矿工挖出区块之后再提示操作完成,这与传统的 web2 有显著的区别。

  1. Operations组件,没有什么值得注意的,和传统 web2 类似
import { useWrite } from "@/hooks/contract";
import { waitForTransactionReceipt } from "@wagmi/core";
import React, { useState } from "react";
import config from "../wagmi";

export default function Operations() {
  const { mintNFT, setApprovalForAll, listNFT } = useWrite();
  const [tokenId, setTokenId] = useState("");
  const [price, setPrice] = useState("");
  const [cid, setCid] = useState("");

  return (
    <div className="flex flex-col gap-4">
      <div>
        <button
          onClick={async () => {
            const mintHash = await mintNFT();
            if (mintHash) {
              console.log("mintHash processing......");
              await waitForTransactionReceipt(config, {
                hash: mintHash,
              });
              alert("mintHash success......");
            }
          }}
          className="bg-blue-500 text-white w-full  px-4 py-2 rounded-md mt-4"
        >
          铸造NFT
        </button>
      </div>
      <div>
        <label for="price" className="block text-sm font-medium leading-6 ">
          上架的 NFT ID:
        </label>
        <div className="relative mt-2 rounded-md shadow-sm">
          <input
            value={tokenId}
            type="text"
            className="block w-full rounded-md border-0 py-1.5 pl-7 pr-7 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
            onChange={(e) => {
              setTokenId(e.target.value);
            }}
          />
        </div>
      </div>
      <div>
        <label for="price" className="block text-sm font-medium leading-6 ">
          上架价格:
        </label>
        <div className="relative mt-2 rounded-md shadow-sm">
          <input
            value={price}
            type="text"
            className="block w-full rounded-md border-0 py-1.5 pl-7 pr-7 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
            onChange={(e) => {
              setPrice(e.target.value);
            }}
          />
        </div>
      </div>
      <div>
        <label for="price" className="block text-sm font-medium leading-6 ">
          cid:
        </label>
        <div className="relative mt-2 rounded-md shadow-sm">
          <input
            value={cid}
            type="text"
            className="block w-full rounded-md border-0 py-1.5 pl-7 pr-7 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
            onChange={(e) => {
              setCid(e.target.value);
            }}
          />
        </div>
      </div>
      <div>
        <button
          className="bg-blue-500 text-white w-full  px-4 py-2 rounded-md mt-4"
          onClick={async () => {
            if (!tokenId) {
              alert("请输入需要上架的 NFT ID");
            } else if (!price) {
              alert("请输入上架价格");
            } else if (!cid) {
              alert("请输入 cid");
            } else {
              const approvalHash = await setApprovalForAll();
              if (approvalHash) {
                await waitForTransactionReceipt(config, {
                  hash: approvalHash,
                });
              }
              const listHash = await listNFT(tokenId, price, cid);
              if (listHash) {
                await waitForTransactionReceipt(config, {
                  hash: listHash,
                });
                alert("上架成功");
                setTokenId("");
                setPrice("");
                setCid("");
              }
            }
          }}
        >
          上架
        </button>
      </div>
    </div>
  );
}

  1. 查看效果,首先需要将 hardhat 命令行返回的某一个私钥导入到 MetaMask 钱包得到一个测试账户

截屏2024-11-09 09.57.13.png

然后连接钱包选择 MetaMask 选择刚才导入的那个账户:

截屏2024-11-09 09.58.21.png

输入 tokenId,上架价格,cid 最终效果如下:

截屏2024-11-09 10.07.58.png

实战到此结束,至此我们实现了一个 NFT 的上下架以及购买,在实战过程中可能会遇到很多问题,这个下期再讲:如何调试 web3 应用。

点击此处查看项目源码

关于作者

我是一个落魄前端负翁,相信很多前端和我一样迷茫,或许找不到工作,或许是工作太忙,大量的负债压垮了自己,但是也得要抽点时间玩一玩 web3,或许还有一线生机呢?