# 如何使用 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 余额，我们可以用它来为我们的测试交易提供资金。
