# 如何使用 Ethers.js 铸造 NFT

本教程介绍了如何通过 ethers.js 库使用以太币在以太坊区块链上铸造 NFT，以及我们来自第一部分的智能合约：如何创建 NFT。我们还将探讨基本测试。

*完成本指南的预计时间：\~10 分钟*

在本练习中，我们将引导您完成使用[OpenZeppelin 库](https://docs.openzeppelin.com/contracts/4.x/erc721)版本 4以及[Ethers.js 以太](https://docs.ethers.io/)坊库而不是 Web3 的替代实现。

我们还将介绍使用[Hardhat 和 Waffle](https://hardhat.org/plugins/nomiclabs-hardhat-waffle.html)测试您的合约的基础知识。对于本教程，我使用的是 Yarn，但如果您愿意，也可以使用 npm/npx。

最后，我们将使用 TypeScript。这已被[很好地记录下来](https://hardhat.org/guides/typescript.html#typescript-support)，所以我们不会在这里介绍它。

在所有其他方面，本教程与 Web3 版本的工作方式相同，包括 Pinata 和 IPFS 等工具。

### 快速提醒

提醒一下，“铸造 NFT”是在区块链上发布您的 ERC721 代币的唯一实例的行为。本教程假设您已经在 NFT 教程系列[的第一部分中成功将智能合约部署到 Goerli 网络](https://www.web3.university/tracks/build-your-first-nft/how-to-create-an-nft)，其中包括 .

### 第 1 步：创建您的 Solidity 合约

OpenZeppelin 是用于安全智能合约开发的库。您只需继承他们对 ERC20 或 ERC721 等流行标准的实现，并根据您的需要扩展行为。我们将把这个文件放在 contracts/MyNFT.sol 中。

```js
// Contract based on https://docs.openzeppelin.com/contracts/4.x/erc721
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract MyNFT is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("MyNFT", "MNFT") {}

    function mintNFT(address recipient, string memory tokenURI)
    public
    returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}
```

### 第 2 步：创建 Hardhat 任务以部署我们的合约并铸造 NFT

创建包含以下内容的文件 tasks/nft.ts：

```js
import { task, types } from "hardhat/config";
import { Contract } from "ethers";
import { TransactionResponse } from "@ethersproject/abstract-provider";
import { env } from "../lib/env";
import { getContract } from "../lib/contract";
import { getWallet } from "../lib/wallet";

task("deploy-contract", "Deploy NFT contract").setAction(async (_, hre) => {
  return hre.ethers
    .getContractFactory("MyNFT", getWallet())
    .then((contractFactory) => contractFactory.deploy())
    .then((result) => {
      process.stdout.write(`Contract address: ${result.address}`);
    });
});

task("mint-nft", "Mint an NFT")
  .addParam("tokenUri", "Your ERC721 Token URI", undefined, types.string)
  .setAction(async (tokenUri, hre) => {
    return getContract("MyNFT", hre)
      .then((contract: Contract) => {
        return contract.mintNFT(env("ETH_PUBLIC_KEY"), tokenUri, {
          gasLimit: 500_000,
        });
      })
      .then((tr: TransactionResponse) => {
        process.stdout.write(`TX hash: ${tr.hash}`);
      });
  });
```

### 第 3 步：创建助手

您会注意到我们的任务导入了一些助手。他们来了。

**合同.ts**

```js
import { Contract, ethers } from "ethers";
import { getContractAt } from "@nomiclabs/hardhat-ethers/internal/helpers";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { env } from "./env";
import { getProvider } from "./provider";

export function getContract(
  name: string,
  hre: HardhatRuntimeEnvironment
): Promise<Contract> {
  const WALLET = new ethers.Wallet(env("ETH_PRIVATE_KEY"), getProvider());
  return getContractAt(hre, name, env("NFT_CONTRACT_ADDRESS"), WALLET);
}
```

**环境文件**

```js
export function env(key: string): string {
  const value = process.env[key];
  if (value === undefined) {
    throw `${key} is undefined`;
  }
  return value;
}
```

**供应商.ts**

请注意，最终的 getProvider() 函数使用 ropsten 网络。此参数是可选的，如果省略则默认为“homestead”。当然，我们使用的是 Alchemy，但有多种[受支持的替代方案](https://docs.ethers.io/v5/api/providers/#providers-getDefaultProvider)。

```js
import { ethers } from "ethers";

export function getProvider(): ethers.providers.Provider {
  return ethers.getDefaultProvider("ropsten", {
    alchemy: process.env.ALCHEMY_API_KEY,
  });
}
```

**钱包.ts**

```js
import { ethers } from "ethers";
import { env } from "./env";
import { getProvider } from "./provider";

export function getWallet(): ethers.Wallet {
  return new ethers.Wallet(env("ETH_PRIVATE_KEY"), getProvider());
}
```

### 第 4 步：创建测试

在您的测试目录下，创建这些文件。请注意，这些测试并不全面。他们测试了 OpenZeppelin 库提供的一小部分 ERC721 功能，旨在为您提供构建块以创建更强大的测试。

**test/MyNFT.spec.ts（单元测试）**

```js
import { ethers, waffle } from "hardhat";
import { Contract, Wallet } from "ethers";
import { expect } from "chai";
import { TransactionResponse } from "@ethersproject/abstract-provider";
import sinon from "sinon";
import { deployTestContract } from "./test-helper";
import * as provider from "../lib/provider";

describe("MyNFT", () => {
  const TOKEN_URI = "http://example.com/ip_records/42";
  let deployedContract: Contract;
  let wallet: Wallet;

  beforeEach(async () => {
    sinon.stub(provider, "getProvider").returns(waffle.provider);
    [wallet] = waffle.provider.getWallets();
    deployedContract = await deployTestContract("MyNFT");
  });

  async function mintNftDefault(): Promise<TransactionResponse> {
    return deployedContract.mintNFT(wallet.address, TOKEN_URI);
  }

  describe("mintNft", async () => {
    it("emits the Transfer event", async () => {
      await expect(mintNftDefault())
        .to.emit(deployedContract, "Transfer")
        .withArgs(ethers.constants.AddressZero, wallet.address, "1");
    });

    it("returns the new item ID", async () => {
      await expect(
        await deployedContract.callStatic.mintNFT(wallet.address, TOKEN_URI)
      ).to.eq("1");
    });

    it("increments the item ID", async () => {
      const STARTING_NEW_ITEM_ID = "1";
      const NEXT_NEW_ITEM_ID = "2";

      await expect(mintNftDefault())
        .to.emit(deployedContract, "Transfer")
        .withArgs(
          ethers.constants.AddressZero,
          wallet.address,
          STARTING_NEW_ITEM_ID
        );

      await expect(mintNftDefault())
        .to.emit(deployedContract, "Transfer")
        .withArgs(
          ethers.constants.AddressZero,
          wallet.address,
          NEXT_NEW_ITEM_ID
        );
    });

    it("cannot mint to address zero", async () => {
      const TX = deployedContract.mintNFT(
        ethers.constants.AddressZero,
        TOKEN_URI
      );
      await expect(TX).to.be.revertedWith("ERC721: mint to the zero address");
    });
  });

  describe("balanceOf", () => {
    it("gets the count of NFTs for this address", async () => {
      await expect(await deployedContract.balanceOf(wallet.address)).to.eq("0");

      await mintNftDefault();

      expect(await deployedContract.balanceOf(wallet.address)).to.eq("1");
    });
  });
});
```

**tasks.spec.ts（集成规范）**

```js
import { deployTestContract, getTestWallet } from "./test-helper";
import { waffle, run } from "hardhat";
import { expect } from "chai";
import sinon from "sinon";
import * as provider from "../lib/provider";

describe("tasks", () => {
  beforeEach(async () => {
    sinon.stub(provider, "getProvider").returns(waffle.provider);
    const wallet = getTestWallet();
    sinon.stub(process, "env").value({
      ETH_PUBLIC_KEY: wallet.address,
      ETH_PRIVATE_KEY: wallet.privateKey,
    });
  });

  describe("deploy-contract", () => {
    it("calls through and returns the transaction object", async () => {
      sinon.stub(process.stdout, "write");

      await run("deploy-contract");

      await expect(process.stdout.write).to.have.been.calledWith(
        "Contract address: 0x610178dA211FEF7D417bC0e6FeD39F05609AD788"
      );
    });
  });

  describe("mint-nft", () => {
    beforeEach(async () => {
      const deployedContract = await deployTestContract("MyNFT");
      process.env.NFT_CONTRACT_ADDRESS = deployedContract.address;
    });

    it("calls through and returns the transaction object", async () => {
      sinon.stub(process.stdout, "write");

      await run("mint-nft", { tokenUri: "https://example.com/record/4" });

      await expect(process.stdout.write).to.have.been.calledWith(
        "TX hash: 0xd1e60d34f92b18796080a7fcbcd8c2b2c009687daec12f8bb325ded6a81f5eed"
      );
    });
  });
});
```

**test-helpers.ts**请注意，这需要导入 NPM 库，包括 sinon、chai 和 sinon-chai。由于使用存根，因此需要调用**sinon.restore() 。**

```js
import sinon from "sinon";
import chai from "chai";
import sinonChai from "sinon-chai";
import { ethers as hardhatEthers, waffle } from "hardhat";
import { Contract, Wallet } from "ethers";

chai.use(sinonChai);

afterEach(() => {
  sinon.restore();
});

export function deployTestContract(name: string): Promise<Contract> {
  return hardhatEthers
    .getContractFactory(name, getTestWallet())
    .then((contractFactory) => contractFactory.deploy());
}

export function getTestWallet(): Wallet {
  return waffle.provider.getWallets()[0];
}
```

### 第 5 步：配置

这是我们相当简单的**hardhat.config.ts**。

```js
import("@nomiclabs/hardhat-ethers");
import("@nomiclabs/hardhat-waffle");
import dotenv from "dotenv";
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

const argv = JSON.parse(env("npm_config_argv"));
if (argv.original !== ["hardhat", "test"]) {
  require('dotenv').config();
}

import("./tasks/nft");

import { HardhatUserConfig } from "hardhat/config";

const config: HardhatUserConfig = {
  solidity: "0.8.6",
};

export default config;
```

请注意，如果我们不运行测试，则只调用 dotenv 的条件。您可能不想在生产环境中运行它，但请放心，如果 .env 文件不存在，dotenv 将默默地忽略它。

### 运行我们的任务

现在我们已经将这些文件放在适当的位置，我们可以运行 hardhat 来查看我们的任务（为简洁起见，不包括内置任务）。

```js
AVAILABLE TASKS:

  deploy-contract    Deploy NFT contract
  mint-nft      Mint an NFT
```

忘记了任务的论据？没问题。

```js
$ hardhat help deploy-contract

Usage: hardhat [GLOBAL OPTIONS] deploy-contract

deploy-contract: Deploy NFT contract
```

### 运行我们的测试

为了运行我们的测试，我们运行**hardhat test**。

```js
mintNft
    ✓ calls through and returns the transaction object (60ms)

  MyNFT
    mintNft
      ✓ emits the Transfer event (60ms)
      ✓ returns the new item ID
      ✓ increments the item ID (57ms)
      ✓ cannot mint to address zero
    balanceOf
      ✓ gets the count of NFTs for this address


  6 passing (2s)

✨  Done in 5.66s.
```

### 概括

在本教程中，我们为基于 Solidity 的经过良好测试的 NFT 基础设施奠定了坚实的基础。**waffle.provider.getWallets()**&#x63D0;供的钱包链接到一个本地假[Hardhat Network](https://hardhat.org/hardhat-network/)帐户，该帐户[方便地预装](https://hardhat.org/hardhat-network/reference/#initial-state)了一个 eth 余额，我们可以用它来为我们的测试交易提供资金。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.chengwf.com/build-your-first-nft/how-to-mint-an-nft-using-ethers-js.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
