Hardhat Tutorial & Guides

Hardhat Tutorial & Guides

本文是对 Hardhat 官网的入门教程 Hardhat’s tutorial for beginners 及 指南 Guides 的部分翻译。

1. 设置环境

大多数的 Ethereum 库和工具都是使用 JavaScript 编写的,Hardhat 也是如此。因此在设置环境的时候,需要安装 Node.js

本教程推荐使用 VSCode + Hardhat 插件。如果您使用的是 Windows,推荐您使用 wsl 安装 Node.js 以使用 Hardhat。

2. 创建新的 Hardhat 工程

在空文件夹中初始化 npm 工程:npm init,如果想直接跳过需要回答的问题,可以直接使用:npm init -y。该命令会在空文件夹中生成一个 package.json 文件。

安装 Hardhat 到本地,并将包(package)保存到 devDependencies 中(见npm install):npm install --save-dev hardhat (devDependencies 选项体现在 package.json 中)。

在安装了 Hardhat 的相同文件夹下可以运行 hardhat:npx hardhat (npx 可以从本地或远程 npm 包运行一个命令而不需要知道包确切的路径)。

选择 Create an empty hardhat.config.js,将会在文件夹中创建一个 hardhat.config.js 配置文件,选择创建 JS/TS 工程将会创建包含了合约文件、测试脚本和部署脚本的模板工程。(一个 hardhat.config.js 配置文件足以在默认工程结构中使用 Hardhat)

选择创建 JS/TS 工程后的初始化工程结构为:

contracts/         # 合约源文件
scripts/           # 自动化脚本文件
test/              # 测试文件
hardhat.config.js  # 配置文件

当 Hardhat 运行时,它会从当前工作目录开始搜索最近的 hardhat.config.js 配置文件,该文件通常位于项目的根目录中,空的 hardhat.config.js 足以让 Hardhat 工作。Hardhat 的全部设置都包含在此文件中。

$ npm init -y  # 初始化 npm 工程,生成 package.json 配置文件
$ npm install --save-dev hardhat  # 安装 Hardhat 到本地
$ npx hardhat  # 创建新的 hardhat.config.js 配置文件 或 创建 JS/TS 工程

Hardhat 结构

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

任务

每次从命令行运行 Hardhat,都是在运行一个任务。比如,npx hardhat compile 就是在运行 compile 任务。npx hardhatnpx hardhat help 可以查看当前可用的任务。npx hardhat help [task_name] 可以进一步了解某个可用的任务。

用户也可以创建自己的任务,查看创建任务进一步了解。

插件

Hardhat 不限制最终使用的工具形态,但有一些内置的默认值。所有的默认值都可以修改。大多数情况下,使用给定工具的方法是将其集成到 Hardhat 的插件中。

本教程中将使用推荐的插件 @nomicfoundation/hardhat-toolboxHardhat Toolbox 绑定了开始使用 Hardhat 开发智能合约所需的一切。

在当前文件夹运行命令来安装:

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

然后在 hardhat.config.js 文件开头添加新行:

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

配置

对于 Hardhat 的所有配置都在 hardhat.config.js 配置文件中更改,具体的可配置选项参考 Hardhat Configuration

3. 编写 & 编译合约

本节要实现的是编写一个简单的智能合约实现可以被转换的代币(本处的代币合约并没有符合 ERC20 标准)。

编写合约

创建新的名为 contracts 的目录,在该目录中创建名为 Token.sol 的文件。将下面的代码复制到合约文件中。

//SPDX-License-Identifier: UNLICENSED

// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.8.9;


// This is the main building block for smart contracts.
contract Token {
    // Some string type variables to identify the token.
    string public name = "My Hardhat Token";
    string public symbol = "MHT";

    // The fixed amount of tokens, stored in an unsigned integer type variable.
    uint256 public totalSupply = 1000000;

    // An address type variable is used to store ethereum accounts.
    address public owner;

    // A mapping is a key/value map. Here we store each account's balance.
    mapping(address => uint256) balances;

    // The Transfer event helps off-chain applications understand
    // what happens within your contract.
    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    /**
     * Contract initialization.
     */
    constructor() {
        // The totalSupply is assigned to the transaction sender, which is the
        // account that is deploying the contract.
        balances[msg.sender] = totalSupply;
        owner = msg.sender;
    }

    /**
     * A function to transfer tokens.
     *
     * The `external` modifier makes a function *only* callable from *outside*
     * the contract.
     */
    function transfer(address to, uint256 amount) external {
        // Check if the transaction sender has enough tokens.
        // If `require`'s first argument evaluates to `false` then the
        // transaction will revert.
        require(balances[msg.sender] >= amount, "Not enough tokens");

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

        // Notify off-chain applications of the transfer.
        emit Transfer(msg.sender, to, amount);
    }

    /**
     * Read only function to retrieve the token balance of a given account.
     *
     * The `view` modifier indicates that it doesn't modify the contract's
     * state, which allows us to call it without executing a transaction.
     */
    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }
}

编译合约

如果您需要自定义 Solidity 编译器选项,则可以通过更改 hardhat.config.js 中的 solidity 字段来实现。使用此字段的最简单方法是设置编译器版本的简写,我们建议始终这样做:

module.exports = {
    solidity: "0.8.9",
};

我们建议始终设置编译器版本,以避免在发布新版本的 Solidity 时出现意外行为或编译错误。

扩展的用法允许对编译器进行更多控制:

module.exports = {
    solidity: {
        version: "0.8.9",
        settings: {
            optimizer: {
                enabled: true,
                runs: 1000,
            },
        },
    },
};

settings 与可以传递给编译器的 Input JSON 中的 setting 条目具有相同的架构(schema)。一些常用的设置是:

  • optimizer: 一个有 enabledruns 作为键的对象,默认值为:{ enabled: false, runs:200 }
  • evmVersion: 一个控制目标 evm 版本的字符串。例如:istanbul, berlin 或者 london。默认值:由 solc 管理。

如果您任何合约的版本编译指示(version pragma)不满足您配置的编译器版本,那么 Hardhat 将抛出错误。

在命令行中运行 npx hardhat compile 可以编译合约。compile 任务是内置任务之一。

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

合约已被成功编译,可以使用。

编译任务会将 contracts 目录中的所有合约源文件都进行编译。所有默认情况下,编译的结果将保存在 artifacts/ 目录下,或者保存在您配置的路径下。您可以在路径配置中了解如何修改。如果目录不存在的话编译之后会自动创建。

在初次编译之后,Hardhat 将会在下次编译的时候做最少的工作。比如,如果你没有更改某些文件,那么这些文件将不会被重新编译。只有那些更改过的文件会被重新编译。

如果想要强制编译您可以使用 --force 选项,或者运行 npx hardhat clean 来清除缓存、删除 artifacts。

4. 测试合约

在构建智能合约时编写自动化测试至关重要。为了测试合约,本节将会使用 Hardhat 网络,这是一个专门为开发而设计的本地以太坊网络。它内置在 Hardhat 中,并用作默认网络。无需设置任何内容即可使用它。

在测试中将会使用 ethers.js 与第3节构建的合约进行交互,并使用 Mocha 作为测试运行器(test runner)。

编写测试

在项目根文件夹下新建名为 test 的目录,并创建名为 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");
const Token = await ethers.getContractFactory("Token");

ethers.js 中的 ContractFactory 是一个用于部署新的智能合约的抽象,因此这里的 Token 是我们的代币合约实例的工厂(factory)。这里的 ethers.getContractFactory() 和上面的 ethers.getSigners() 并不包含在 ethers 官方依赖中,而是 Hardhat 添加到 ethers 对象中的助手(helper)。Helpers 给出了 hardhat-ethers 插件为 ethers 库添加的助手(包括接口及函数声明)。

const hardhatToken = await Token.deploy();

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

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

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

回想一下,部署 token 的帐户获得了全部供应。默认情况下,ContractFactoryContract 实例连接到第一个签名者(signer)。这意味着 owner 变量中的账户执行了部署,而 balanceOf() 应该返回全部供应量。

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

在这里,我们再次使用我们的 Contract 实例在我们的 Solidity 代码中调用智能合约函数。 totalSupply() 返回代币的供应量,我们正在检查它是否等于 ownerBalance,它应该是相等的。

为此,我们使用了流行的 JavaScript 断言库(assertion library) Chai。这些断言函数称为“匹配器(matchers)”,我们在这里使用的函数来自 @nomicfoundation/hardhat-c​​hai-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);
    });
});

contract.connect(providerOrSigner) 将会返回一个新的连接到 providerOrSigner 的 Contract 示例。

使用 fixtures 重用常见的测试设置

我们在上面编写了两个测试(代币总量和代币转账),在这个案例中这意味着部署代币合约。在更复杂的项目中,此设置可能涉及多个部署和其他交易(transaction)。在每次测试中都这样做意味着大量的代码重复。另外,在每个测试开始时执行许多交易会使测试套件变得更慢。

您可以通过使用 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 将在第一次运行设置,并在其他测试中快速返回到该状态。

完全覆盖

现在我们已经介绍了测试合约所需的基础知识,这里有一个完整的代币测试套件,其中包含许多关于 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 onces 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 (1000 tokens).
            // `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 时,如果您的合约自上次运行测试以来发生更改,则会自动编译它们。

5. 使用 Hardhat 网络 Debug

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

Solidity console.log

在 Hardhat Network 上运行合约和测试时,您可以从 Solidity 代码中通过调用 console.log() 来打印日志消息(logging messages)和合约变量。要使用它,您必须在合约代码中导入 hardhat/console.sol

类似于:

pragma solidity ^0.8.9;

import "hardhat/console.sol";  // added line

contract Token {
    //...
}

然后你可以添加一些 console.log 调用到 transfer() 函数,就好像你在 JavaScript 中使用它一样:

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)

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

6. 部署到实时网络

一旦您准备好与其他人共享您的 dApp,您可能希望将其部署到实时(live)网络。这样,其他人就可以访问不在您系统上本地运行的实例。

“mainnet” 以太坊主网络处理 real money,但测试网络不是这样。这些测试网提供了共享的暂存环境,可以很好地模拟现实世界的场景,而不会危及 real money,以太坊有几个测试网,比如 GoerliSepolia。我们建议您将合约部署到 Goerli 测试网。

在软件层面,部署到测试网与部署到主网相同。唯一的区别是您连接到哪个网络。让我们看看使用 ethers.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 Network 的嵌入实例运行。在这种情况下,当 Hardhat 完成运行时,部署实际上会丢失(多次运行同一个部署脚本后您可能会发现部署的合约地址始终是相同的),但测试我们的部署代码是否有效仍然很有用:

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

你可以按照下面两步将合约部署到 localhost 网络中:

  1. 启动一个本地节点
npx hardhat node
  1. 打开一个新的终端,将合约部署到 localhost 网络中
npx hardhat run scripts/deploy.js --network localhost

使用上面的方法,你将会在本地运行一个以太坊节点,并将合约部署到 localhost 网络中。

实际上,你还可以在本地模拟以太坊主网的节点,并将合约部署到本地模拟的以太坊主网上,进而可以在模拟的主网上测试合约。

使用 npx hardhat help node 命令你将会看到 npx hardhat node 命令的帮助信息,其中包含:

Usage: hardhat [GLOBAL OPTIONS] node [--fork <STRING>] [--fork-block-number <INT>] [--hostname <STRING>] [--port <INT>]

OPTIONS:

  --fork                The URL of the JSON-RPC server to fork from
  --fork-block-number   The block number to fork from
  --hostname            The host to which to bind to for new connections (Defaults to 127.0.0.1 running locally, and 0.0.0.0 in Docker)
  --port                The port on which to listen for new connections (default: 8545)

node: Starts a JSON-RPC server on top of Hardhat Network

也就是说,通过设置 --fork--fork-block-number 选项,你可以在本地模拟以太坊主网任意一个区块,进而可以复现主网上某一时刻的历史状态。其中,--fork 选项的参数类似于 https://eth-mainnet.alchemyapi.io/v2/ALCHEMY_API_KEY

部署到远程网络

要部署到远程网络(例如主网或任何测试网),您需要将 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 官网去获取你的 ALCHEMY_API_KEY 然后回来设置。

要在 Goerli 上部署,您需要将一些 Goerli ether 发送到将要进行部署的地址。您可以从水龙头中获取测试网以太币,这是一种免费分发测试以太币的服务。 Goerli 有几个水龙头:

在进行交易之前,您必须将 Metamask 的网络更改为 Goerli。

您可以在 ethereum.org 网站上了解有关其他测试网的更多信息并找到指向其水龙头的链接。

最后,运行:

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

如果一切顺利,您应该会看到已部署的合约地址。

7. 验证合约

验证合约意味着公开其源代码以及您使用的编译器设置,这允许任何人编译它并将生成的字节码与部署在链上的字节码进行比较。在像以太坊这样的开放平台中,​​这样做非常重要。

在本小节中将会介绍如何在 Etherscan 浏览器中验证合约,但还有其他方法可以验证合约,例如使用 Sourcify

从 Etherscan 获取 API key

您需要的第一件事是从 Etherscan 获取 API key。想要获取,请访问 Etherscan,登录(如果没有,请创建一个帐户)并打开“API key”选项。然后单击“Add”按钮并为您正在创建的 API key 命名(如“Hardhat”)。之后,您将在列表中看到新创建的密钥。

打开您的 Hardhat config 并添加您刚刚创建的 API key。在 Goerli 测试网上验证合约也需要添加对应的以太坊浏览器的 API key。

module.exports = {
    // ...rest of the config...
    etherscan: {
        apiKey: "ABCDE12345ABCDE12345ABCDE123456789",
    },
};

在 Goerli 测试网上验证合约

在上一节中已经给出了将合约部署到 Goerli 测试网上的方法。

记录下地址和解锁时间,并使用它们运行 verify 任务:

npx hardhat verify --network goerli <address> <unlock time>

TIP:如果您收到一条错误消息,指出该地址没有字节码,这可能意味着 Etherscan 尚未索引您的合约。在这种情况下,请稍等片刻,然后重试。

任务成功执行后,您将看到一个指向已公开验证的合约代码的链接。

要了解有关验证(verifying)的更多信息,请阅读 hardhat-etherscan(plugin) 文档。

8. 编写任务和脚本

Hardhat 的核心是一个任务运行器(task runner),可以让您自动化您的开发工作流程。它带有一些内置任务,例如 compiletest,但您也可以添加自己的自定义任务。

译者认为,Hardhat task 的理念和使用有些类似于 GNU Make,可以对照了解和学习。该网站给出了 GNU make 的官网手册。

本小节将向您展示如何使用任务和脚本扩展 Hardhat 的功能,并假定您已经初始化了一个示例项目。

编写 Hardhat 任务

让我们编写一个打印可用帐户列表的非常简单的任务,并探索它是如何工作的。

复制此任务定义并将其粘贴到您的 Hardhat 配置文件中:

task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
    const accounts = await hre.ethers.getSigners();

    for (const account of accounts) {
        console.log(account.address);
    }
});

现在可以运行它:

npx hardhat accounts

我们正在使用 task 函数来定义我们的新任务。它的第一个参数是任务的名称,它是我们在命令行中用来运行它的名称。第二个参数是任务的描述,当你使用 npx hardhat help 时会打印出来。

第三个参数是运行任务时执行的异步函数。它接收两个参数:

  1. 带有任务参数的对象,此处我们还没有定义。
  2. Hardhat Runtime Environment 或 HRE,它包含 Hardhat 及其插件的所有功能。您还可以在任务执行期间找到注入到 global 命名空间中的所有属性。

您可以在此功能中自由地做任何您想做的事情。在本例中,我们使用 ethers.getSigners() 来获取所有配置的帐户并打印它们的每个地址。

您可以添加参数到您的任务中,Hardhat 将会处理它们的解析(parsing)和验证(validation)。

您还可以覆盖现有任务,这允许您更改 Hardhat 不同部分的工作方式。

要了解有关任务的更多信息,请阅读 Creating a task

编写 Hardhat 脚本

您可以编写脚本并使用 Hardhat 运行它们。它们可以利用 Hardhat 运行时环境 来访问所有 Hardhat 的功能,包括任务运行器(task runner)。

这是一个与我们的 accounts 任务相同的脚本。使用以下内容创建一个 accounts.js 文件:

async function main() {
    const accounts = await ethers.getSigners();

    for (const account of accounts) {
        console.log(account.address);
    }
}

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

使用内置的 run 任务来运行它:

npx hardhat run accounts.js

请注意,我们使用的是 ethers 而没有导入它。这是可能的,因为在 Hardhat 运行时环境中可用的所有内容在脚本中也是全局可用的。ethers 对象已经由 hardhat-toolbox 插件注入到了 Hardhat 运行时环境中。

要了解有关脚本的更多信息,包括如何在不使用 Hardhat 的 CLI 的情况下运行它们,请阅读 Writing scripts with Hardhat

在任务和脚本之间选择

在任务和脚本之间进行选择取决于您。如果您不确定应该使用哪一个,下面的建议可能会有用:

  1. 如果您想自动化不需要参数的工作流程,脚本可能是最佳选择。
  2. 如果您要自动化的工作流程需要一些参数,请考虑创建 Hardhat 任务。
  3. 如果您需要从另一个拥有自己 CLI 的工具访问 Hardhat Runtime Enivronment,例如 jestndb,你应该写一个脚本。确保显式导入 Hardhat 运行时环境,以便可以使用该工具而不是 Hardhat 的 CLI 运行它
  4. 如果你觉得 Hardhat 的参数处理不能满足你的需要,你应该写一个脚本。只需显式导入 Hardhat 运行时环境,使用您自己的参数解析逻辑(例如使用 yargs),并将其作为独立的 Node.js 脚本运行

9. 使用 Hardhat 控制台

Hardhat 有内置的交互式 JavaScript 控制台。你可以通过运行 npx hardhat console 来使用它:

$ npx hardhat console
Welcome to Node.js v12.10.0.
Type ".help" for more information.
>

compile 任务将会在打开 console prompt 之前被调用,但是你可以通过 --no-compile 参数跳过。

控制台的执行环境和任务、脚本以及测试的相同。这意味着配置已经处理完毕,Hardhat 运行时环境已经初始化并注入到全局作用域中。

例如,你将可以在全局范围内访问 config 对象:

> config
{
  solidity: { compilers: [ [Object] ], overrides: {} },
  defaultNetwork: 'hardhat',
  ...
}
>

而且如果你是从上面的教程跟下来的或安装了 @nomiclabs/hardhat-ethers ,那么也可以访问 ethers 对象:

> ethers
{
  Signer: [Function: Signer] { isSigner: [Function] },
  ...
  provider: EthersProviderWrapper {
  ...
  },
  ...
  getSigners: [Function: getSigners],
  ...
  getContractAt: [Function: bound getContractAt] AsyncFunction
}
>

任何注入到 Hardhat 运行时环境中的都将神奇地在全局范围内可用。

或者,如果您是更明确的开发人员,则可以改为明确要求 HRE:

> const hre = require("hardhat")
> hre.ethers
{
  Signer: [Function: Signer] { isSigner: [Function] },
  ...
  provider: EthersProviderWrapper {
  ...
  },
  ...
  getSigners: [Function: getSigners],
  ...
  getContractAt: [Function: bound getContractAt] AsyncFunction
}

历史

您还会注意到控制台具有大多数交互式终端所期望的便捷历史记录功能,包括跨不同会话。您可以按向上箭头键尝试。 Hardhat 控制台只是 Node.js 控制台的一个实例,因此您可以使用在 Node.js 中使用的任何东西。

异步操作和 top-level await

与以太坊网络交互,也即与你的智能合约交互,是异步操作(asynchronous operations)。因此,大多数 API 和库使用 JavaScript 的 Promise 来返回值。

为了使事情更加简单,Hardhat 的控制台支持 top-level await 语句(例如 console.log(await ethers.getSigners())。

10. 使用 TypeScript

在本教程中,我们将逐步完成使用 TypeScript 的 Hardhat 项目。这意味着您可以在 TypeScript 中编写 Hardhat 配置、任务、脚本和测试。

启用 TypeScript 支持

如果您的配置文件以 .ts 结尾并且是使用有效的 TypeScript 编写的,那么 Hardhat 会自动地启用其 TypeScript 支持。这需要进行一些更改才能正常工作。

安装依赖

TIP:如果您使用 npm 7 或更高版本安装了 @nomicfoundation/hardhat-toolbox,那么您将不需要执行下面几步。

Hardhat 在引擎盖(hood)下面使用 TypeScript 和 ts-node,因此您需要安装它们。您需要打开终端,进入 Hardhat 项目,并且运行

# npm 7+
npm install --save-dev ts-node typescript

为了能够在 TypeScript 中编写测试,你还需要这些包:

# npm 7+
npm install --save-dev chai @types/node @types/mocha @types/chai

TypeScript 配置

您可以轻松地将 JavaScript Hardhat 配置文件转换为 TypeScript 配置文件。让我们从一个新的 Hardhat 项目开始看看这是如何完成的。

打开您的终端,转到一个空文件夹,运行 npx hardhat,然后完成创建 JavaScript 项目的步骤。完成后,您的项目目录应如下所示:

$ ls -l
total 1200
drwxr-xr-x    3 pato  wheel      96 Oct 20 12:50 contracts/
-rw-r--r--    1 pato  wheel     567 Oct 20 12:50 hardhat.config.js
drwxr-xr-x  434 pato  wheel   13888 Oct 20 12:52 node_modules/
-rw-r--r--    1 pato  wheel  604835 Oct 20 12:52 package-lock.json
-rw-r--r--    1 pato  wheel     460 Oct 20 12:52 package.json
drwxr-xr-x    3 pato  wheel      96 Oct 20 12:50 scripts/
drwxr-xr-x    3 pato  wheel      96 Oct 20 12:50 test/

然后,您应该按照上面 【安装依赖】部分中提到的步骤进行操作。

下面,我们会将配置文件从 hardhat.config.js 重命名为 hardhat.config.ts,只需要运行:

mv hardhat.config.js hardhat.config.ts

我们需要对您的配置进行单一更改,以便与 Typescript 一起使用:您必须使用 import/export 而不是require/module.exports

通过使用 TypeScript,您还可以键入您的配置,这将使您免于拼写错误和其他错误。

例如,相同的项目配置需要从这样:

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

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

更改为:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

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

export default config;

最终,你需要创建一个 tsconfig.json 文件。这里是我们推荐的一个:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

这就是它需要的所有了。现在您可以在 TypeScript 中编写配置、测试、任务和脚本。

对项目进行类型检查

出于性能原因,Hardhat 不会在您运行任务时对您的项目进行类型检查。您可以使用 --typecheck 标志显式启用类型检查。

例如,如果您运行 npx hardhat test 并且您的测试有一个有编译错误,测试任务无论如何都会执行。但是如果您运行 npm hardhat test --typecheck,Hardhat 将会在开始运行测试前检测并抛出编译错误。

由于类型检查会增加大量开销,因此我们建议仅在 CI 或 pre-commit/pre-push 钩子中进行。

使用 TypeScript 编写测试和脚本

使用 JavaScript 时,Hardhat 运行时环境中的所有属性都被注入到全局范围内。使用 TypeScript 时,全局范围内没有任何内容可用,您需要使用类似 import { ethers } from "hardhat" 的方式显式导入所有内容。

类型安全的智能合约交互

TIP:如果你安装了 @nomicfoundation/hardhat-toolbox 你可以跳过这小节,因为它包含了 @typechain/hardhat

如果你希望 Hardhat 为您的合约生成类型,你应该安装和使用 @typechain/hardhat。它基于 ABI 生成了类型文件(*.d.ts),并且它几乎不需要配置。

支持路径映射

TypeScript 支持通过 paths 配置选项来定义常规的路径映射(path mapping)

{
  compilerOptions: {
    paths: { "~/*": ["src/*"] },
    // ...Other compilerOptions
  },
}

要在运行 Hardhat 测试或脚本时支持此选项,您需要安装该软件包 tsconfig-paths 并将其注册到您的 hardhat.config.ts 中:

import { HardhatUserConfig } from "hardhat/config";

// This adds support for typescript paths mappings
import "tsconfig-paths/register";

const config: HardhatUserConfig = {
  // Your type-safe config goes here
};

export default config;

直接通过 ts-node 运行测试和脚本

在没有 CLI 的情况下运行 Hardhat 脚本时,您需要使用 ts-node--files flag。

这也可以通过 TS_NODE_FILES=true 启用。

11. 命令行补充

Hardhat 有一个配套的 npm 包,它充当 npx hardhat 的简写,同时,它可以在您的终端中启用命令行完成。

这个包,hardhat-shorthand,安装了一个名为 hh 的全局可访问的二进制文件,它运行你本地安装的 hardhat

安装

要使用 Hardhat shorthand,您需要在**全局范围内(globally)**安装它:

npm install --global hardhat-shorthand

执行此操作后,运行 hh 将等同于运行 npx hardhat。例如,您可以运行 hh compile,而不是运行 npx hardhat compile

安装命令行补全

要启用自动补全(autocomplete)支持,您还需要使用 hardhat-completion 安装 shell 补全脚本,它附带 hardhat-shorthand。运行 hardhat-completion install 并按照说明安装补全脚本:

$ hardhat-completion install
✔ Which Shell do you use ? · zsh
✔ We will install completion to ~/.zshrc, is it ok ? (y/N) · true
=> Added tabtab source line in "~/.zshrc" file
=> Added tabtab source line in "~/.config/tabtab/zsh/__tabtab.zsh" file
=> Wrote completion script to /home/fvictorio/.config/tabtab/zsh/hh.zsh file

      => Tabtab source line added to ~/.zshrc for hh package.

      Make sure to reload your SHELL.

要试用它,请打开一个新终端,转到您的 Hardhat 项目的目录,然后尝试键入 hh,然后键入 tab,即可看到命令行补全效果。

Context

出于最佳实践,Hardhat 项目使用 npm 包 hardhat 的本地安装来确保参与该项目的每个人都使用相同的版本。这就是为什么您需要使用 npx 或 npm 脚本来运行 Hardhat。

这种方法的缺点是无法直接为 hardhat 命令提供自动补全建议,并且会使 CLI 命令更长。这是 hh 解决的两个问题。

故障排除

“Autocompletion is not working”

首先,确保您使用 hardhat-completion install 安装了自动补全脚本,然后重新加载您的 shell 或打开一个新终端重试。

如果仍有问题,请确保您的 Hardhat 配置没有任何问题。你可以通过运行 hh 来做到这一点。如果命令行打印帮助消息,那么您的配置是可以的。如果没有,您将看到问题是什么。

12. 模板工程

如果您想快速开始使用您的 dApp 或使用前端查看整个项目的外观,您可以使用我们的样板代码库

包含了什么

  • 我们在本教程中使用的 Solidity 合约
  • 合约全部功能的测试
  • 使用 ethers.js 和合约交互的最小 React 前端

Solidity 合约 & 测试

在 repo 的根目录中,您将找到我们通过本教程与 Token 合约一起构建的 Hardhat 项目。该项目实现了:

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

前端应用

在前端(frontend),你会发现一个简单的应用程序,它允许用户做两件事:

  • 检查已连接钱包的余额
  • 将代币发送到一个地址

它是一个单独的 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,我们启动了一个 Hardhat Network 实例,您可以使用 MetaMask 连接到该实例。在同一目录中的不同终端中,运行:

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

这会将合约部署到 Hardhat Network。完成后,启动 react web 应用:

cd frontend
npm install
npm run start

然后在浏览器打开 http://127.0.0.1:3000/,会看到连接到钱包的页面。

设置你 Metamask 中的网络至 127.0.0.1:8545

单击 Web 应用程序中的按钮。会看到某个地址的合约的用户信息。

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

npx hardhat --network localhost faucet <your address>

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

您可以在 /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 输出会显示,这就是运行 faucet 任务后 Web 应用程序的样子。

尝试使用它并阅读代码。它包含了解释正在发生的事情的注释,并清楚地表明什么代码是以太坊模板以及什么是实际的 dApp 逻辑。这应该使存储库易于为您的项目重用。

结尾

恭喜您完成教程!

下面是一些可能有用的链接:


Hardhat Tutorial & Guides
https://alphafitz.com/2022/09/21/tools-hardhat-tutorial-guides/
作者
alphafitz
发布于
2022年9月21日
许可协议