Hardhat教程

1. Hardhat概述

欢迎来到我们的以太坊合约和 dApp 开发初学者指南。本教程旨在快速让您从头开始构建一些东西。

为了协调这个过程,我们将使用 Hardhat,这是一种有助于在以太坊上构建的开发环境。它可以帮助开发人员管理和自动化构建智能合约和 dApp 过程中固有的重复性任务,并且它允许你围绕此工作流程轻松引入更多功能。这意味着 hardhat 在最核心的地方进行编译和测试。

Hardhat 还内置了 Hardhat 网络,这是一个专为开发而设计的本地以太坊网络。它允许你部署合约、运行测试和调试代码。

在本教程中,我们将引导您完成:

  • 为以太坊开发设置 Node.js 环境
  • 创建和配置 Hardhat 项目
  • 实现 Solidity 智能合约代币
  • 使用 Hardhat 为你的合约编写自动化测试
  • 使用Hardhat EVM的 console.log() 调试 Solidity
  • 将您的合约部署到 Hardhat EVM 和以太坊测试网

要遵循本教程,你应该能够:

  • 用 [JavaScript] 编写代码(https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/JavaScript_basics)
  • 操作 terminal
  • 使用 git
  • 了解 智能合约 如何工作的基础知识
  • 设置 Metamask 钱包

如果你无法执行上述任何操作,请点击链接并花一些时间学习基础知识。

2. 搭建环境

大多数以太坊库和工具都是用 JavaScript 编写的,Hardhat 也是如此。如果你不熟悉 Node.js,它是基于 Chrome 的 V8 JavaScript 引擎构建的 JavaScript 运行时。它是在 Web 浏览器之外运行 JavaScript 的最流行的解决方案,而 Hardhat 就是在它之上构建的。

:::提示

Hardhat for Visual Studio Code 是官方的 Hardhat 扩展,它为 VSCode 添加了对 Solidity 的高级支持。如果你使用 Visual Studio Code,可尝试一下!

:::

安装Node.js

如果你已经安装了的 Node.js >=16.0,则可以跳到下一节。如果没有,请按照以下步骤在 Ubuntu,MacOS 和 Windows 上安装它。

Linux

Ubuntu

将以下命令复制并粘贴到终端中:

sudo apt update
sudo apt install curl git
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs

MacOS

确保你已安装 git。否则,请遵循这些说明安装.

在 MacOS 上有多种安装 Node.js 的方法。我们将使用 Node 版本管理器(nvm)。将以下命令复制并粘贴到终端中:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
nvm install 18
nvm use 18
nvm alias default 18
npm install npm --global # Upgrade npm to the latest version

Windows

如果你使用 Windows,我们强烈推荐你使用用于 Linux 的 Windows 子系统(也就是 WSL2)。你可以不用它来使用 Hardhat,但如果你使用它,效果会更好。

要使用 WSL2 来安装 Node.js,可参考这篇手册.

还要确保你已经在 WSL2 上安装了 git.

升级Node.js

如果你的 Node.js 版本很老的并且不被 Hardhat 支持,则需要通过以下指引升级。

Linux

Ubuntu
  1. 在终端运行 sudo apt remove nodejs 命令来删除 Node.js。
  2. 这里查找你希望安装的 Node.js 版本并且按照指令来安装。
  3. 在终端运行 sudo apt update && sudo apt install nodejs 命令来再次安装 Node.js。

MacOS

你可以使用 nvm来切换你的 Node.js 版本。在终端运行以下命令升级到 Node.js 18.x

nvm install 18
nvm use 18
nvm alias default 18
npm install npm --global # 升级 npm 到最新的版本

Windows

你需要遵循与之前相同的安装说明,但选择不同的版本。你可以在此处查看所有可用版本的列表。

3. 创建新的Hardhat项目

我们将使用 Node.js 的包管理器(npm)来安装 Hardhat,NPM 是一个 Node.js 软件包管理器和一个 JavaScript 代码库。

你可以使用 Node.js 的其它包管理器,但是我们建议你按照这个手册使用 npm 7 或者更高的版本。如果你遵循了之前的章节步骤,那么应该已经安装了它。

打开一个新终端,运行以下命令来创建一个新的目录:

mkdir hardhat-tutorial
cd hardhat-tutorial

然后初始化一个 npm 项目,如下所示。系统将提示你回答一些问题。

::::提示

使用代码段中的选项卡选择首选的包管理器。我们建议使用 npm7 或更高版本,因为它使安装 Hardhat 的依赖关系变得更容易。

::::

::::tabsgroup{options="npm 7+,npm 6,yarn"}

:::tab{value="npm 7+"}

npm init

:::

:::tab{value="npm 6"}

npm init

:::

:::tab{value=yarn}

yarn init

:::

::::

现在我们可以安装 Hardhat:

::::tabsgroup{options="npm 7+,npm 6,yarn"}

:::tab{value="npm 7+"}

npm install --save-dev hardhat

:::

:::tab{value="npm 6"}

npm install --save-dev hardhat

:::

:::tab{value=yarn}

yarn add --dev hardhat

:::

::::

在安装 Hardhat 的同一目录中运行以下命令:

npx hardhat

使用你的键盘选择 Create an empty hardhat.config.js 并且按下回车键。

$ npx hardhat
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.9.9 👷‍

? What do you want to do? …
  Create a JavaScript project
  Create a TypeScript project
❯ Create an empty hardhat.config.js
  Quit

当 Hardhat 在运行时,它从当前工作目录开始搜索最近的 hardhat.config.js 文件。这个文件通常存在你项目的根目录下,而且一个空的 hardhat.config.js 文件已经足够可以让 Hardhat 去工作了。你的整个配置都包含在这个文件中。

Hardhat的架构

Hardhat 是围绕任务和插件的概念设计的。Hardhat 的大部分功能来自插件,你可以自由选择想要使用的插件

任务

每次从命令行运行 Hardhat 时,都在运行一个任务。例如,npx hardhat compile 正在运行 compile 任务。要查看项目中当前可用的任务,运行 npx hardhat。你可以通过运行 npx hardhat help [task] 来探索任何任务。

:::提示

你可以创建你自己的任务,可参阅创建任务指南

:::

插件

就最终使用的工具而言,Hardhat 是独立的,但它确实有一些内置的默认值。所有这些都可以被覆盖。大多数情况下,使用给定工具的方法是使用将其集成到 Hardhat 中的插件。

在本教程中,我们将使用我们推荐的插件,@nomicfoundation/hardhat-toolbox,它提供了开发智能合约所需的一切。

要安装它,请在项目目录中运行此命令:

::::tabsgroup{options="npm 7+,npm 6,yarn"}

:::tab{value="npm 7+"}

npm install --save-dev @nomicfoundation/hardhat-toolbox

:::

:::tab{value="npm 6"}

npm install --save-dev @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-chai-matchers @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan chai ethers hardhat-gas-reporter solidity-coverage @typechain/hardhat typechain @typechain/ethers-v5 @ethersproject/abi @ethersproject/providers

:::

:::tab{value=yarn}

yarn add --dev @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-chai-matchers @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan chai ethers hardhat-gas-reporter solidity-coverage @typechain/hardhat typechain @typechain/ethers-v5 @ethersproject/abi @ethersproject/providers

:::

::::

将高亮显示的行添加到 hardhat.config.js 中,使其看起来像这样:

require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.9",
};

4. 编写和编译合约

我们将创建一个简单的智能合约,它会实现一个可以交易的 token。Token 合约最常用于交换和存储价值。在本教程中,我们不会深入研究合约的 Solidity 代码,但你应该知道我们实现的一些逻辑:

  • Token 的总供应量是固定的,不能更改。
  • Token 的整个供应量分配给部署合约的账户地址。
  • 任何人都可以收到 Token。
  • 任何拥有至少一个 Token 的人都可以转移 Token。
  • Token 是不可分割的。你可以转移1、2、3或37个 Token,但不能转移2.5个。

:::提示

你可能听说过 ERC-20,这是以太坊的一个 token 标准。token 如 DAI 和 USDC 实现了 ERC-20 标准,这使得它们都可以与任何可以处理 ERC-20 token 的软件兼容。为了简单起见,我们将要构建的 token 不实现 ERC-20 标准。

:::

编写智能合约

开始创建一个名为 contracts 的新文件夹,并且在文件中创建一个名为 Token.sol 的文件。

粘贴下面的的代码到文件中,并且花一分钟通读以下这段代码。它很简单,而且充满了解释 Solidity 基本原理的注释。

:::提示

要在 Visual Studio Code 中为 solididity 获得语法高亮显示和编辑帮助,请尝试在Hardhat for Visual Studio Code

:::

//SPDX-License-Identifier: UNLICENSED

// Solidity文件必须以这个pragma开头。
// 它将被Solidity编译器用来验证它的版本。
pragma solidity ^0.8.9;


// 这是智能合约的主要构建模块。
contract Token {
    // 一些用于标识token的字符串类型变量。
    string public name = "My Hardhat Token";
    string public symbol = "MHT";

    // 固定数量的tokens,存储在无符号整数类型变量中。
    uint256 public totalSupply = 1000000;

    // 一个地址类型变量被用作存储以太坊账户。
    address public owner;

    // 一个key/value键值对map映射。我们在这里存储每个账户的余额。
    mapping(address => uint256) balances;

    // 这个转让事件帮助链下应用理解在你的合约中发生的什么。
    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    /**
     * 合约初始化。
     */
    constructor() {
        // 这个总供应量被分配给交易发送者,它是部署这个合约的账户。
        balances[msg.sender] = totalSupply;
        owner = msg.sender;
    }

    /**
     * 转移tokens的函数。
     *
     * 这个 `external` 修饰符会使一个函数仅可以在这个合约外部被调用。
     */
    function transfer(address to, uint256 amount) external {
        // 检查这个交易发送者是否有足够的tokens。
        // 如果 `require` 的第一个参数等价于 `false` 那么这个交易将会恢复。
        require(balances[msg.sender] >= amount, "Not enough tokens");

        // 转移金额。
        balances[msg.sender] -= amount;
        balances[to] += amount;

        // 将转移通知链下应用程序。
        emit Transfer(msg.sender, to, amount);
    }

    /**
     * 只读函数,用于检索给定账户的token余额。
     * 这个 `view` 修饰符表明它不能修改合约的状态,它允许我们不需要执行一个交易而调用它。
     */
    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }
}

:::提示

*.sol 是Solidity合约文件的后缀。我们建议将文件名与其包含的合约名保持一致,这是一种常见的做法。

:::

编译智能合约

要编译合约,可在你的终端中运行 npx hardhat compilecompile 任务是内置任务之一。

$ npx hardhat compile
Compiling 1 file with 0.8.9
Compilation finished successfully

这个合约已经成功编译了,它可以准备被使用了。

5. 测试合约

在构建智能合约时编写自动化测试是至关重要的,因为这关系到用户的钱。

为了测试我们的合约,我们将使用 Hardhat Network,这是一个为开发而设计的本地以太坊网络。它内置在 Hardhat 中,被用作默认网络。使用它不需要设置任何东西。

在我们的测试中,我将使用 ethers.js 与我们在上一节中构建的以太坊合约进行交互,并且我们将使用 Mocha 作为我们的测试运行者。

编写测试

在项目根目录中创建一个名为 test 的新目录,并在其中创建一个名为 Token.js 的新文件。

让我们从下面的代码开始。接下来我们将解释它,但现在将其粘贴到 Token.js 中:

const { expect } = require("chai");

describe("Token contract", function () {
  it("Deployment should assign the total supply of tokens to the owner", async function () {
    const [owner] = await ethers.getSigners();

    const Token = await ethers.getContractFactory("Token");

    const hardhatToken = await Token.deploy();

    const ownerBalance = await hardhatToken.balanceOf(owner.address);
    expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
  });
});

在你的终端中运行 npx hardhat test。你应该会看到如下的输出:

$ npx hardhat test

  Token contract
    ✓ Deployment should assign the total supply of tokens to the owner (654ms)


  1 passing (663ms)

这意味着测试通过了。现在让我们解释每一行代码:

const [owner] = await ethers.getSigners();

ethers.js 中的 Signer 是代表以太坊帐户的对象。它被用于向合约和其他账户发送交易。这里我们得到连接到了节点上的账户列表,在本例中是 Hardhat Network,我们只保留第一个。

ethers 变量在全局作用域中可用。如果你希望你的代码总是显式的,你可以在顶部添加这一行:

const { ethers } = require("hardhat");

:::提示

要学习更多关于 Signer 的内容,你可以查看 Signers 文档

:::

const Token = await ethers.getContractFactory("Token");

ethers.js 中的 ContractFactory 是一个用于部署新智能合约的抽象,因此这里的 Token 是 token 合约实例的工厂。

const hardhatToken = await Token.deploy();

ContractFactory 上调用 deploy() 将启动部署,并且返回一个解决为 ContractPromise。这个对象为每个智能合约函数都提供了一个方法。

const ownerBalance = await hardhatToken.balanceOf(owner.address);

一旦这个合约被部署后,我们可以在 hardhatToken 上调用我们的合约方法。这里我们通过调用合约的 balanceOf() 方法,获得所有者账户的余额。

重新调用将会获得部署 token 的帐户的全部供应总量。默认情况下,ContractFactoryContract 实例连接到第一个签名者。这意味着 owner 变量中的帐户执行了部署,并且 balanceOf() 应该返回整个供应总额。

expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);

这里在我们的 Solidity 代码中,我们再次使用 Contract 实例去调用一个智能合约函数。totalSupply() 返回这个 token 供应总额,并且我们将检查它是否等价于 ownerBalance,正如它应该的那样。

为此,我们使用了 Chai,这是一个流行的 JavaScript 断言库。这些断言函数被叫做 "matchers",我们在这里使用的是来自 @nomicfoundation/hardhat-chai-matchers 插件,它扩展了 Chai,提供了许多用于测试智能合约的匹配器。

使用不同的账户

如果你需要从默认帐户以外的其他帐户(或 ethers.js 中的 Signer)发送交易来测试代码,则可以在 ethers.js 的 Contract 中使用 connect() 方法来将其连接到其他帐户,像这样:

const { expect } = require("chai");

describe("Token contract", function () {
  // ...previous test...

  it("Should transfer tokens between accounts", async function() {
    const [owner, addr1, addr2] = await ethers.getSigners();

    const Token = await ethers.getContractFactory("Token");

    const hardhatToken = await Token.deploy();

    // Transfer 50 tokens from owner to addr1
    await hardhatToken.transfer(addr1.address, 50);
    expect(await hardhatToken.balanceOf(addr1.address)).to.equal(50);

    // Transfer 50 tokens from addr1 to addr2
    await hardhatToken.connect(addr1).transfer(addr2.address, 50);
    expect(await hardhatToken.balanceOf(addr2.address)).to.equal(50);
  });
});

使用fixtures复用常见的测试配置

我们编写的两个测试从它们的设置开始,在本例中这意味着部署 token 合约。在更复杂的项目中,此设置可能涉及多个部署和其他事务。在每个测试中都这样做意味着大量的重复代码。另外,在每个测试开始时执行许多事务会使测试套件变得更慢。

通过使用 fixtures,你可以避免代码重复并提高测试套件的性能。fixture 是只在第一次调用时运行的设置函数。在随后的调用中,Hardhat 不会重新运行它,而是将网络的状态重置为 fixture 最初执行后的状态。

const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");
const { expect } = require("chai");

describe("Token contract", function () {
  async function deployTokenFixture() {
    const Token = await ethers.getContractFactory("Token");
    const [owner, addr1, addr2] = await ethers.getSigners();

    const hardhatToken = await Token.deploy();

    await hardhatToken.deployed();

    // Fixtures can return anything you consider useful for your tests
    return { Token, hardhatToken, owner, addr1, addr2 };
  }

  it("Should assign the total supply of tokens to the owner", async function () {
    const { hardhatToken, owner } = await loadFixture(deployTokenFixture);

    const ownerBalance = await hardhatToken.balanceOf(owner.address);
    expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
  });

  it("Should transfer tokens between accounts", async function () {
    const { hardhatToken, owner, addr1, addr2 } = await loadFixture(
      deployTokenFixture
    );

    // Transfer 50 tokens from owner to addr1
    await expect(
      hardhatToken.transfer(addr1.address, 50)
    ).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);

    // Transfer 50 tokens from addr1 to addr2
    // We use .connect(signer) to send a transaction from another account
    await expect(
      hardhatToken.connect(addr1).transfer(addr2.address, 50)
    ).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
  });
});

在这里,我们编写了一个 deployTokenFixture 函数,该函数执行必要的设置并返回稍后在测试中使用的每个值。然后在每个测试中,我们使用 loadFixture 来运行 fixture 并获取这些值。loadFixture 将在第一次运行设置,并在其他测试中迅速返回到该状态。

完整的测试覆盖

现在我们已经介绍了测试合约所需的基本知识,下面是 token 的完整测试套件,其中包含大量关于 Mocha 以及如何组织测试的附加信息。我们建议你仔细阅读。

// This is an example test file. Hardhat will run every *.js file in `test/`,
// so feel free to add new ones.

// Hardhat tests are normally written with Mocha and Chai.

// We import Chai to use its asserting functions here.
const { expect } = require("chai");

// We use `loadFixture` to share common setups (or fixtures) between tests.
// Using this simplifies your tests and makes them run faster, by taking
// advantage of Hardhat Network's snapshot functionality.
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

// `describe` is a Mocha function that allows you to organize your tests.
// Having your tests organized makes debugging them easier. All Mocha
// functions are available in the global scope.
//
// `describe` receives the name of a section of your test suite, and a
// callback. The callback must define the tests of that section. This callback
// can't be an async function.
describe("Token contract", function () {
  // We define a fixture to reuse the same setup in every test. We use
  // loadFixture to run this setup once, snapshot that state, and reset Hardhat
  // Network to that snapshot in every test.
  async function deployTokenFixture() {
    // Get the ContractFactory and Signers here.
    const Token = await ethers.getContractFactory("Token");
    const [owner, addr1, addr2] = await ethers.getSigners();

    // To deploy our contract, we just have to call Token.deploy() and await
    // its deployed() method, which happens once its transaction has been
    // mined.
    const hardhatToken = await Token.deploy();

    await hardhatToken.deployed();

    // Fixtures can return anything you consider useful for your tests
    return { Token, hardhatToken, owner, addr1, addr2 };
  }

  // You can nest describe calls to create subsections.
  describe("Deployment", function () {
    // `it` is another Mocha function. This is the one you use to define each
    // of your tests. It receives the test name, and a callback function.
    //
    // If the callback function is async, Mocha will `await` it.
    it("Should set the right owner", async function () {
      // We use loadFixture to setup our environment, and then assert that
      // things went well
      const { hardhatToken, owner } = await loadFixture(deployTokenFixture);

      // `expect` receives a value and wraps it in an assertion object. These
      // objects have a lot of utility methods to assert values.

      // This test expects the owner variable stored in the contract to be
      // equal to our Signer's owner.
      expect(await hardhatToken.owner()).to.equal(owner.address);
    });

    it("Should assign the total supply of tokens to the owner", async function () {
      const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
      const ownerBalance = await hardhatToken.balanceOf(owner.address);
      expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
    });
  });

  describe("Transactions", function () {
    it("Should transfer tokens between accounts", async function () {
      const { hardhatToken, owner, addr1, addr2 } = await loadFixture(
        deployTokenFixture
      );
      // Transfer 50 tokens from owner to addr1
      await expect(
        hardhatToken.transfer(addr1.address, 50)
      ).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);

      // Transfer 50 tokens from addr1 to addr2
      // We use .connect(signer) to send a transaction from another account
      await expect(
        hardhatToken.connect(addr1).transfer(addr2.address, 50)
      ).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
    });

    it("should emit Transfer events", async function () {
      const { hardhatToken, owner, addr1, addr2 } = await loadFixture(
        deployTokenFixture
      );

      // Transfer 50 tokens from owner to addr1
      await expect(hardhatToken.transfer(addr1.address, 50))
        .to.emit(hardhatToken, "Transfer")
        .withArgs(owner.address, addr1.address, 50);

      // Transfer 50 tokens from addr1 to addr2
      // We use .connect(signer) to send a transaction from another account
      await expect(hardhatToken.connect(addr1).transfer(addr2.address, 50))
        .to.emit(hardhatToken, "Transfer")
        .withArgs(addr1.address, addr2.address, 50);
    });

    it("Should fail if sender doesn't have enough tokens", async function () {
      const { hardhatToken, owner, addr1 } = await loadFixture(
        deployTokenFixture
      );
      const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);

      // Try to send 1 token from addr1 (0 tokens) to owner.
      // `require` will evaluate false and revert the transaction.
      await expect(
        hardhatToken.connect(addr1).transfer(owner.address, 1)
      ).to.be.revertedWith("Not enough tokens");

      // Owner balance shouldn't have changed.
      expect(await hardhatToken.balanceOf(owner.address)).to.equal(
        initialOwnerBalance
      );
    });
  });
});

这是针对完整测试套件的 npx hardhat test 的输出:

$ npx hardhat test

  Token contract
    Deployment
      ✓ Should set the right owner
      ✓ Should assign the total supply of tokens to the owner
    Transactions
      ✓ Should transfer tokens between accounts (199ms)
      ✓ Should fail if sender doesn’t have enough tokens
      ✓ Should update balances after transfers (111ms)


  5 passing (1s)

请记住,当你运行 npx hardhat test 时,如果自上次运行测试以来合约发生了更改,则会自动编译它们。

6. 使用Hardhat网络进行调试

Hardhat 内置了 Hardhat 网络,这是一个专为开发设计的以太坊网络。它允许你部署合约,运行测试和调试代码,所有这些都在本地机器的范围内。它是 Hardhat 所连接的默认网络,因此你无需任何设置即可工作。你只需运行测试就好。

Solidity console.log

在 Hardhat 网络上运行合约和测试时,你可以在 Solidity 代码中调用 console.log() 打印日志信息和合约变量。你必须先从合约代码中导入 hardhat/console.sol 再使用它。

就像这样:

pragma solidity ^0.8.9;

import "hardhat/console.sol";

contract Token {
  //...
}

就像在 JavaScript 中使用一样,然后你只需在 transfer() 函数中添加一些 console.log 调用:

function transfer(address to, uint256 amount) external {
    require(balances[msg.sender] >= amount, "Not enough tokens");

    console.log(
        "Transferring from %s to %s %s tokens",
        msg.sender,
        to,
        amount
    );

    balances[msg.sender] -= amount;
    balances[to] += amount;

    emit Transfer(msg.sender, to, amount);
}

运行测试时将显示日志输出:

$ npx hardhat test

  Token contract
    Deployment
      ✓ Should set the right owner
      ✓ Should assign the total supply of tokens to the owner
    Transactions
Transferring from 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 50 tokens
Transferring from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc 50 tokens
      ✓ Should transfer tokens between accounts (373ms)
      ✓ Should fail if sender doesn’t have enough tokens
Transferring from 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 50 tokens
Transferring from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc 50 tokens
      ✓ Should update balances after transfers (187ms)


  5 passing (2s)

查看文档以了解更多关于该特性的信息。

7. 部署到真实网络

一旦你准备好与其他人分享 dApp 后,你可能要做的就是将其部署到一个真实的以太坊网络中。这样,其他人就可以访问不在本地系统上运行的实例了。

具有处理真实金钱的以太坊网络被称为“主网”,然后还有一些独立的“测试网”可以使用。这些测试网提供了共享的登录环境,可以很好地模拟真实世界的场景,而不需要投入真正的金钱,这里有几个以太坊测试网,像 GoerliSepolia。我们推荐你部署你的合约到 Goerli 测试网。

在软件级别,部署到测试网与部署到主网相同。唯一的区别是你连接到的哪个网络。让我们看看使用 ether.js 部署合约的代码是什么样的。

使用的主要概念是 SignerContractFactoryContract,我们在测试部分中对此进行了解释。与测试相比,没有什么新东西需要做,因为当你测试合约时,实际上是在向开发网络进行部署。这使得代码非常相似,甚至完全相同。

让我们在项目根目录中创建一个新的目录 scripts,并将以下内容粘贴到该目录中的 deploy.js 文件中:

async function main() {
  const [deployer] = await ethers.getSigners();

  console.log("Deploying contracts with the account:", deployer.address);

  console.log("Account balance:", (await deployer.getBalance()).toString());

  const Token = await ethers.getContractFactory("Token");
  const token = await Token.deploy();

  console.log("Token address:", token.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

要告诉 Hardhat 连接到特定的以太坊网络,可以在运行任何任务时使用 --network 参数,如下所示

npx hardhat run scripts/deploy.js --network <network-name>

对于我们当前的配置,在没有 --network 参数的情况下运行它将导致代码针对 Hardhat 网络的嵌入式实例运行。在这种情况下,当 Hardhat 完成运行时,部署实际上会丢失,但测试我们的部署代码是否有效仍然很有用:

$ npx hardhat run scripts/deploy.js
Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Account balance: 10000000000000000000000
Token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3

部署到远程网络

要部署到诸如主网或任何测试网之类的远程网络,你需要添加一个 network 入口到你的 hardhat.config.js 文件中。在这个例子中,我们将使用 Goerli,但是你也可以添加任何类似的网络:

require("@nomicfoundation/hardhat-toolbox");

// Go to https://www.alchemyapi.io, sign up, create
// a new App in its dashboard, and replace "KEY" with its key
const ALCHEMY_API_KEY = "KEY";

// Replace this private key with your Goerli account private key
// To export your private key from Metamask, open Metamask and
// go to Account Details > Export Private Key
// Beware: NEVER put real Ether into testing accounts
const GOERLI_PRIVATE_KEY = "YOUR GOERLI PRIVATE KEY";

module.exports = {
  solidity: "0.8.9",
  networks: {
    goerli: {
      url: `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_API_KEY}`,
      accounts: [GOERLI_PRIVATE_KEY]
    }
  }
};

我们使用了 Alchemy, 但是你将 url 指向其他任何以太坊节点或网关都是可以的。请填入你自己的 ALCHEMY_API_KEY

要在 Goerli 上进行部署,你需要发送一些 Goerli ether 给将要进行部署的地址。你可以从水龙头获得测试网以太,这是一项免费分发测试 ETH 的服务。下面这些是给 Goerli 的:

你必须在交易前将 Metamask 的网络更改为 Goerli。

:::提示

你可以在 ethereum.org 网站上了解更多关于其他测试网的信息,并找到它们的水龙头链接。

:::

最终,运行:

npx hardhat run scripts/deploy.js --network goerli

如果一切正常,你应该会看到部署的合约地址。

8. 前端模板项目

如果你想要快速开始使用 dApp 或使用前端查看整个项目,可以使用我们的 boilerplate 库

包含哪些内容

  • 本次教程中我们使用的 Solidity 合约
  • 测试合约的整个功能
  • 使用 ethers.js 与合约进行交互的最小 React 前端

Solidity合约与测试

在代码库的根目录中,你将找到我们通过本教程与 Token 合约组合在一起的 Hardhat 项目。

  • token 的总供应量是固定的,不能更改。
  • 整个供应被分配给部署合约的地址。
  • 任何人都可以接收 token。
  • 任何人至少有一个 token 都可以转让 token。
  • 这个 token 是不可以分割的。你可以转让1、2、3或37个代币,但不能转让2.5个。

前端app

frontend 你会发现一个简单的 app,允许用户做两件事:

  • 检查连接钱包的余额
  • 发送 token 到一个地址

它是一个独立的 npm 项目,它是用 create-react-app 创建的,这意味着它使用了 webpack 和 babel。

前端文件目录结构

  • src/ 包含了所有代码
    • src/components 包含了 react 组件
      • Dapp.js 是唯一具有业务逻辑的文件。如果你把它用作为模板,可在此处用自己的代码替换它。
      • 其他组件仅渲染HTML,没有逻辑。
      • src/contracts 具有合约的 ABI 和地址,这些由部署脚本自动生成。

如何使用它

首先克隆代码库,然后准备部署合约:

cd hardhat-boilerplate
npm install
npx hardhat node

在这里,我们仅需要安装 npm 项目的依赖项,然后运行 npx hardhat node 启动一个可以供 MetaMask 连接的 Hardhat 网络。在同一目录下的另一个终端中运行如下命令:

npx hardhat --network localhost run scripts/deploy.js

这会将合约部署到 Hardhat 网络。完成后,启动 react web app:

cd frontend
npm install
npm run start

然后在你的浏览器中打开 http://127.0.0.1:3000/,你应该会看到如下所示:

将 MetaMask 中的网络设置为 127.0.0.1:8545

现在点击 web app 的按钮。你应该会看到如下所示:

这里发生的情况是,显示当前钱包余额,前端代码检测到余额为 0,因此你无法尝试转账功能。通过运行如下命令:

npx hardhat --network localhost faucet <your address>

你将运行我们包含的自定义 Hardhat 任务,该任务使用部署帐户的余额向你的地址发送100个MHT和1个ETH。这将允许你向另一个地址发送 token。

你可以在 /tasks/faucet.js 查看该任务的代码,这是从 hardhat.config.js 中需要的。

$ npx hardhat --network localhost faucet 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90
Transferred 1 ETH and 100 tokens to 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90

在运行 npx hardhat node 的终端中,你还应该看到:

eth_sendTransaction
  Contract call:       Token#transfer
  Transaction:         0x460526d98b86f7886cd0f218d6618c96d27de7c745462ff8141973253e89b7d4
  From:                0xc783df8a850f42e7f7e57013759c285caa701eb6
  To:                  0x7c2c195cd6d34b8f845992d380aadb2730bb9c6f
  Value:               0 ETH
  Gas used:            37098 of 185490
  Block #8:            0x6b6cd29029b31f30158bfbd12faf2c4ac4263068fd12b6130f5655e70d1bc257

  console.log:
    Transferring from 0xc783df8a850f42e7f7e57013759c285caa701eb6 to 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90 100 tokens

在我们的合约中显示 transfer() 函数的 console.log 输出,这就是运行水龙头任务后 web app 的样子:

尝试使用它并阅读代码。它充满了注释,解释了发生了什么,并清楚地指出什么是以太坊 boilerplate 代码,什么是 dApp 逻辑。这将使代码库易于为你的项目复用。

9. 最后的想法

恭喜你完成本教程!

这里有一些链接,你可能会发现在你的 dApp 开发旅程中有用:

Happy hacking!