惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Stack Overflow Blog
Stack Overflow Blog
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
宝玉的分享
宝玉的分享
N
News and Events Feed by Topic
The Hacker News
The Hacker News
Google DeepMind News
Google DeepMind News
C
CERT Recently Published Vulnerability Notes
F
Full Disclosure
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security @ Cisco Blogs
H
Hacker News: Front Page
L
LangChain Blog
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
B
Blog RSS Feed
H
Heimdal Security Blog
Google Online Security Blog
Google Online Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 三生石上(FineUI控件)
V2EX - 技术
V2EX - 技术
V
Vulnerabilities – Threatpost
Help Net Security
Help Net Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
W
WeLiveSecurity
T
Tenable Blog
D
DataBreaches.Net
Martin Fowler
Martin Fowler
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
S
Secure Thoughts
O
OpenAI News
L
LINUX DO - 热门话题
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI
J
Java Code Geeks
Know Your Adversary
Know Your Adversary
IT之家
IT之家
Latest news
Latest news
Cloudbric
Cloudbric

pseudoyu

周报 #106 - OpenClaw 实践、搬家与生活的能量 周报 #105 - 打破无聊的社会 2025 年末回顾 - Anything different is good 周报 #104 - 被遗忘的生活一隅 周报 #103 - 工作随想与被吞噬的生活 周报 #102 - 我是如何使用 AI 的 周报 #101 - 消失了三个月,我的生活发生了什么变化 周报 #100 - 过去这十年 周报 #99 - 离开 RSS3,迷茫与重启人生 周报 #98 - 生活之书与 Agentic Coding 周报 #97 - 日本旅行小记 28 岁的自白:去找寻自己 周报 #96 - 基于 Roo Code/Cursor 的 iOS 开发工作流 周报 #95 - All AI 与 No AI 周报 #94 - 生活的界限与真实感 周报 #93 - 婚礼纪录片与失落的电影院 周报 #92 - 数码产品 Porn 与断舍离 周报 #91 - 一席、消失的时代与自由意志 周报 #90 - OVH 服务器、Coolify 与对 AI Coding 的思考 周报 #89 - 博客改版与 TheWan App 发布 周报 #88 - 方大同、《回留》与人生清单 周报 #87 - 我们主持了自己的宝可梦婚礼,还为它办了一个展 周报 #86 - 用影像打败时间 周报 #85 - 京华烟云 Moment in Peking 周报 #84 - 想起他们 Echos of Reverie 周报 #83 - 年初收纳(房间、设备、服务、软件) 周报 #82 - Brand New Year 与写日记 2024 年末回顾 - 不完美的日子 周报 #81 - Engaged -> Married 与时间的尺度 周报 #80 - 关于求婚、爱情与婚姻 求婚誓言 | 爱的本质是被看见 周报 #79 - 清迈旅居、曼谷之行与 Follow RSSHub 开发实践 #01:Telegram 频道 RSS 订阅实现与部署方案 Follow —— 信息获取的另一种形态 周报 #78 - NAS、Chromebook 与 Zeabur 折腾小记 周报 #77 - 一间只属于自己的房间(对亲密关系的思考) 周报 #76 - 关于 Adulting 的思考 周报 #75 - 回答 Dayu 关于独立博客的 9 个问题 周报 #74 - 清迈异闻录 周报 #73 - 《社交网络》、Ego 与中秋的现充生活 周报 #71 - Indie App 体验计划 周报 #70 - 消失的附近,Burnout 与 Boreout 周报 #69 - AI Coding 体验与学习的源动力 周报 #68 - 关于写博客这件事与我生活的色彩 使用 GoatCounter 与 Zeabur 搭建网站数据统计系统 周报 #67 - 使用 follow 重塑我的信息输入系统 周报 #66 - 10x 工程师、技术热情与个人工具箱 从零开始搭建你的免费博客评论系统(Remark42 + fly.io) 周报 #65 - Adventure X 体验、Apple Notes 笔记实践与 EpubKit 周报 #64 - 生活在此处(《斯通纳》与《Normal People》) 使用 WebP Cloud 与 Cloudflare WAF 为你的图床添加隐私和版权保护 当云服务器崩溃时,我是如何救援重要数据的 周报 #63 - 不愉快的订花经历、商家和消费者与日渐 AI 化的人 从零开始搭建你的免费图床系统(Cloudflare R2 + WebP Cloud) 2024 年了,我的博客有了什么变化 周报 #62 - 香港之行、5am club 计划与 Rust 学习 周报 #61 - 好朋友的婚礼与对婚姻的思考 27 岁的自白:成长是一件很扫兴的事 周报 #60 - 虫子旁、教育理念与 EpubKit 周报 #59 - 沙漠之行、家庭与家人 周报 #58 - 远程工作这一年 周报 #57 - 生活在别处(巴厘岛之行) 周报 #56 - 面基 Randy 与考科目二 周报 #55 - 油画体验、博客系统升级与对 self-hosting 的思考 周报 #54 - 漂流计划、钱包被盗与 Home Server 周报 #53 - 空白的三个月、生活漂流与春节 周报 #52 - 自谦型人格与感情的另一个视角 周报 #51 - 追星小鹿 Lawrence 与生病 周报 #50 - 天坛漫步、重拾阅读和费曼学习法 周报 #49 - 烧焦的键盘、庞贝神话展、健康调整与神奇的梦 周报 #48 - 内蒙之行、清迈游民、重启的周报与生活 周报 #47 - 采访、拳击与工作状态 周报 #46 - 告别长发、周报初心与合约开发 个人信息获取与知识管理系统(Heptabase + Logseq + Readwise) 周报 #45 - 读书笔记、知识管理与双城生活 周报 #44 - 静寂工人、xLog 与日常 周报 #43 - In the Mood for Love 周报 #42 - 感情观、生活状态与自我 周报 #41 - 生日的仪式感、时间管理与北京之行 26 岁的自白:推石头的人 周报 #40 - 开源之夏、代码之外与流逝的睡眠 周报 #39 - 初患新冠、过去的心绪与失而复得的分享欲 周报 #38 - Foundry、Logseq 与 Surge Ponte 周报 #37 - 浮生半月闲( 武汉 | 香港 | 五月天) 周报 #36 - 肠胃炎、捏捏近况与新的旅途 周报 #35 - 登报、搬家和生病的捏捏 周报 #34 - Don't Stop the Clocks 周报 #33 - 北京 x 故事 x 离别 周报 #32 - 武汉、久别重逢与流动的心绪 周报 #31 - 开源、前端开发与 ChatGPT 实践 周报 #30 - 开源预算、写作初心与对技术的谦卑 Cosmos 区块链架构与 Tendermint 共识机制 周报 #29 - 日语学习经验、Steam Deck 与居酒屋 周报 #28 - Mastodon、故乡与捉迷藏的猫 周报 #27 - 何以为家 周报 #26 - 博客、客制化键盘和新服务器 周报 #25 - 基于 Crossbell 的个人信息输出与同步系统(重构) 摄影集 #02 - Moment In Peking (The Autumn) 摄影集 #01 - Cat In The Box 2022 年末回顾 - 迷茫、低谷与改变
Solidity 智能合约开发 - Hardhat 框架使用
pseudoyu · 2022-06-09 · via pseudoyu

前言

经过了前几篇对智能合约基础、Web3.py、ethers.js 的学习,我们已经掌握了通过程序与区块链网络直接交互的基础知识,不熟悉的同学可以回顾一下:

但是在真正的复杂业务场景中,我们往往会使用一些进一步封装的框架,如 HardHat、Brownie、Truffle 等,HardHat 是其中应用最广泛、插件拓展最为强大的。本系列将从这篇开始专注于 Hardhat 框架的使用与最佳实践,而本篇则会通过一个简单的例子完成其安装、配置与使用。

本文是对 Patrick Collins 的 『Learn Blockchain, Solidity, and Full Stack Web3 Development with JavaScript』 教程的学习整理,强烈建议看原教程视频了解更多细节。

可以点击这里访问本测试 Demo 代码仓库。

Hardhat 介绍

hardhat_homepage

Hardhat 是一个基于 JavaScript 的智能合约开发环境,可以用于灵活地编译、部署、测试和调试基于 EVM 的智能合约,并且提供了一系列工具链来整合代码与外部工具,还提供了丰富的插件生态,提升开发效率。此外,它还提供了模拟以太坊的本地 Hardhat 网络节点,提供强大的本地调试功能。

其 GitHub 地址为 NomicFoundation/hardhat,可以访问其官方文档了解更多。

Hardhat 使用

初始化项目

从零开始搭建一个 Hardhat 项目,我们需要预先安装好 node.jsyarn 环境,这部份参照官方说明根据自己的系统环境按照即可。

首先,我们需要初始化项目并安装 hardhat 依赖包。

yarn init
yarn add --dev hardhat

yarn_add

初始化 Hardhat

然后需要运行 yarn hardhat,通过交互式命令来进行初始化,根据项目需要进行配置,我们的测试 Demo 选择默认值。

hardhat_project_init

优化代码格式化

VS Code 配置

我本地是通过 VS Code 进行代码开发的,可以通过安装 Solidity + HardhatPrettier 两个插件来进行代码格式化,可以使用打开 VS Code 设置,在 settings.json 中增加如下格式化配置:

{
  // ...

  "[solidity]": {
    "editor.defaultFormatter": "NomicFoundation.hardhat-solidity"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

项目配置

为了统一各个使用项目的开发人员的代码格式化样式,我们还可以为项目配置 prettierprettier-plugin-solidity 插件支持:

yarn add --dev prettier prettier-plugin-solidity

yarn_add_prettier_plugin

添加依赖后,可以在项目目录增加 .prettierrc.prettierignore 配置文件来进行格式化统一:

我的 .prettierrc 配置为:

{
  "tabWidth": 4,
  "useTabs": false,
  "semi": false,
  "singleQuote": false
}

我的 .prettierignore 配置为:

node_modules
package.json
img
artifacts
cache
coverage
.env
.*
README.md
coverage.json

编译合约

无需像 ethers.js 一样自定义 compile 命令,HardHat 预置了 compile 命令,可以将合约放在 contracts 目录下,然后通过 yarn hardhat compile 命令来编译合约:

hardhat_compile_contract

添加 dotenv 支持

在开始编写部署脚本之前,我们先配置一下 dotenv 插件,这样我们就可以使用 dotenv 来获取环境变量。我们在开发过程中,会牵扯到很多隐私信息,如私钥等,我们会希望将其存储在 .env 文件或直接设置在终端中,比如我们的 RINKEBY_PRIVATE_TOKEN,这样我们就可以在部署脚本中使用 process.env.RINKEBY_PRIVATE_TOKEN 获取到值,无需在代码中显式写入,减少隐私泄漏风险。

安装 dotenv

yarn add --dev dotenv

yarn_add_dotenv

设置环境变量

.env 文件中,我们可以设置环境变量,比如:

RINKEBY_RPC_URL=url
RINKEBY_PRIVATE_KEY=0xkey
ETHERSCAN_API_KEY=key
COINMARKETCAP_API_KEY=key

我们就可以在 hardhat.config.js 中读取环境变量了:

require('dotenv').config()

const RINKEBY_RPC_URL
    = process.env.RINKEBY_RPC_URL || 'https://eth-rinkeby/example'
const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY || '0xkey'
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || 'key'
const COINMARKETCAP_API_KEY = process.env.COINMARKETCAP_API_KEY || 'key'

配置网络环境

往往我们的合约需要运行在不同的区块链网络上,如本地测试、开发、上线环境等等,Hardhat 也提供了便捷的方式来配置网络环境。

启动网络

我们可以直接运行脚本来启动一个 Hardhat 自带的网络,但该网络仅仅存活于脚本运行期间,想要启动一个本地可持续的网络,需要运行 yarn hardhat node 命令:

hardhat_localhost_node

执行完成后,就生成了测试网络与测试账户,供后续开发调试使用。

我们还可以通过 Alchemy 或 Infura 等平台生成自己的测试网节点,记录其 RPC_URL 供程序连接使用。

定义网络

完成网络环境准备后,我们可以在项目配置 hardhat.config.js 中定义网络:

const RINKEBY_RPC_URL
    = process.env.RINKEBY_RPC_URL || 'https://eth-rinkeby/example'
const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY || '0xkey'

module.exports = {
  defaultNetwork: 'hardhat',
  networks: {
    locakhost: {
      url: 'http://localhost:8545',
      chainId: 31337,
    },
    rinkeby: {
      url: RINKEBY_RPC_URL,
      accounts: [RINKEBY_PRIVATE_KEY],
      chainId: 4,

    },
  },
  // ...,
}

脚本

在 Hardhat 项目中,我们可以通过在 scripts 目录中编写脚本来实现部署等功能,并且通过便捷的命令执行脚本。

编写部署脚本

接下来我们开始编写 deploy.js 脚本。

首先,我们需要从 hardhat 中导入必要包:

const { ethers, run, network } = require('hardhat')

接着则编写 main 方法,包含我们的部署核心逻辑:

async function main() {
  const SimpleStorageFactory = await ethers.getContractFactory(
    'SimpleStorage'
  )
  console.log('Deploying SimpleStorage Contract...')
  const simpleStorage = await SimpleStorageFactory.deploy()
  await simpleStorage.deployed()
  console.log('SimpleStorage Contract deployed at:', simpleStorage.address)

  // 获取当前值
  const currentValue = await simpleStorage.retrieve()
  console.log('Current value:', currentValue)

  // 设置值
  const transactionResponse = await simpleStorage.store(7)
  await transactionResponse.wait(1)

  // 获取更新后的值
  const updatedValue = await await simpleStorage.retrieve()
  console.log('Updated value:', updatedValue)
}

最后运行我们的 main 方法:

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

运行脚本

完成脚本编写后,可以通过 Hardhat 提供的 run 命令来运行脚本。

如不加网络参数,则默认使用 hardhat 网络,可以通过 --network 参数指定网络:

yarn hardhat run scripts/deploy.js --network rinkeby

hardhat_deploy_rinkeby

增加 etherscan 合约验证支持

将合约部署至 Rinkeby 测试网络后可在 Etherscan 上查看合约的地址,并且进行验证。我们可以通过网站进行操作,但 Hardhat 提供了插件支持,更方便进行验证操作。

安装 hardhat-etherscan 插件

我们通过 yarn add --dev @nomiclabs/hardhat-etherscan 命令安装插件。

yarn_add_etherscan_verify_support

启用 etherscan 合约验证支持

完成安装后,我们需要在 hardhat.config.js 中进行配置:

require('@nomiclabs/hardhat-etherscan')

module.exports = {
  // ...,
  etherscan: {
    apiKey: ETHERSCAN_API_KEY,
  },
  // ...,
}

定义 verify 方法

接下来我们需要在部署脚本 deploy.js 中添加 verify 方法。

const { ethers, run, network } = require('hardhat')

async function verify(contractAddress, args) {
  console.log('Verifying SimpleStorage Contract...')
  try {
    await run('verify:verify', {
      address: contractAddress,
      constructorArguements: args,
    })
  }
  catch (e) {
    if (e.message.toLowerCase().includes('already verified!')) {
      console.log('Already Verified!')
    }
    else {
      console.log(e)
    }
  }
}

这个方法我们调用了 hardhat 包中的 run 方法,并且传递了一个 verify 命令,并且传递了一个参数 { address: contractAddress, constructorArguements: args }。因为可能我们的合约已经在 Etherscan 上验证过,所以我们做了一个 try...catch... 错误处理,如果验证过,则会抛出一个错误,并且输出一个提示信息,而不影响我们的部署流程。

设置部署后调用

定义好我们的 verify 方法后,我们可以在部署脚本中调用它:

async function main() {
  // ...

  if (network.config.chainId === 4 && process.env.ETHERSCAN_API_KEY) {
    await simpleStorage.deployTransaction.wait(6)
    await verify(simpleStorage.address, [])
  }

  // ...
}

在这里我们做了两个特殊处理。

首先,我们仅需要在 rinkeby 网络上验证合约,而不需要在本地或其他网络环境验证,因此,我们对 network.config.chainId 进行判断,如果是 4,则执行验证操作;否则,不执行验证操作,此外仅在有 ETHERSCAN_API_KEY 环境变量时执行验证操作。

另外,Etherscan 可能需要在部署后一段时间才能获取到合约地址,因此我们配置了 .wait(6) 等待 6 个区块后再进行验证。

执行效果如下:

hardhat_verify_contract_etherscan

verified_contract_on_etherscan

我们通过 Etherscan 验证后访问后可以直接查看合约源码并进行交互。

interact_with_contract_on_etherscan

合约测试

对于智能合约来说,其大多数操作都需要部署上链,与资产交互,消耗 gas,且一旦有安全隐患会造成严重的后果。因此,我们需要对智能合约进行详细的测试。

Hardhat 提供了完备的测试调试工具,可以在 tests 目录中编写测试脚本,通过 yarn hardhat test 命令运行测试。

编写测试脚本

为我们的部署脚本编写 test-deploy.js 测试程序,首先需要导入必要包:

const { assert } = require('chai')
const { ethers } = require('hardhat')

然后编写测试逻辑:

describe('SimpleStorage', () => {
  let simpleStorageFactory, simpleStorage
  beforeEach(async () => {
    simpleStorageFactory = await ethers.getContractFactory('SimpleStorage')
    simpleStorage = await simpleStorageFactory.deploy()
  })

  it('Should start with a favorite number of 0', async () => {
    const currentValue = await simpleStorage.retrieve()
    const expectedValue = '0'

    assert.equal(currentValue.toString(), expectedValue)
    // expect(currentValue.toString()).to.equal(expectedValue)
  })

  it('Should update when we call store', async () => {
    const expectedValue = '7'
    const transactionRespense = await simpleStorage.store(expectedValue)
    await transactionRespense.wait(1)

    const currentValue = await simpleStorage.retrieve()

    assert.equal(currentValue.toString(), expectedValue)
    // expect(currentValue.toString()).to.equal(expectedValue)
  })
})

在 Hardhat 的测试脚本中,我们使用 describe 包裹测试类,并且使用 it 包裹测试方法。我们需要保证测试前合约已经部署,因此,我们通过 beforeEach 方法在每个测试方法执行前都会调用 simpleStorageFactory.deploy(),并且将返回的 simpleStorage 对象赋值给 simpleStorage 变量。

我们使用 assert.equal(currentValue.toString(), expectedValue) 来对执行结果与预期结果进行比照,可以用 expect(currentValue.toString()).to.equal(expectedValue) 替代,效果一样。

此外,我们还可以通过 it.only() 来指定仅执行其中一个测试方法。

执行测试脚本

我们通过 yarn hardhat test 运行测试,且可以通过 yarn hardhat test --grep store 来指定测试方法。

hardhat_run_tests

添加 gas-reporter 支持

如上文所述,gas 是我们在开发过程中需要特别关注的资源,尤其在 Ethereum 主网上尤其昂贵。因此,我们需要在测试过程中查看 gas 消耗情况。HardHat 也有一个 gas-reporter 插件,可以很方便地输出 gas 消耗情况。

安装 gas-reporter 插件

我们通过 yarn add --dev hardhat-gas-reporter 命令来安装插件:

yarn_add_gas_reporter

启用 gas-reporter 支持

我们通过在 hardhat.config.js 中添加 gasReporter: true 及额外配置项来启用插件:

require('hardhat-gas-reporter')

const COINMARKETCAP_API_KEY = process.env.COINMARKETCAP_API_KEY || 'key'

module.exports = {
  // ...,
  gasReporter: {
    enabled: true,
    outputFile: 'gas-reporter.txt',
    noColors: true,
    currency: 'USD',
    coinmarketcap: COINMARKETCAP_API_KEY,
    token: 'MATIC',
  },
}

我们可以指定输出文件、是否开启颜色、指定币种、指定代币名称,以及指定代币的 CoinMarketCap API 密钥来根据项目进一步控制输出。

按照以上配置,运行 yarn hardhat test 输出效果如下:

hardhat_add_gas_reporter_support_and_export

添加 solidity-coverage 支持

合约测试对于保障业务逻辑正确性与安全防范至关重要,因此,我们需要对合约进行覆盖率测试。HardHat 也有一个 solidity-coverage 插件,可以很方便地输出覆盖率情况。

安装 solidity-coverage 插件

我们通过 yarn add --dev solidity-coverage 命令来安装插件:

yarn_add_coverage_support

启用 solidity-coverage 支持

我们仅需在 hardhat.config.js 中导入包即可添加覆盖率测试支持:

require('solidity-coverage')

运行覆盖率测试

通过 yarn hardhat coverage 即可运行覆盖率测试:

hardhat_coverage

Task

上文我们对 hardhat 库的基础功能与脚本进行了一些使用。除此之外,我们还可以自定义一些任务供开发调试使用。

编写 Task

Hardhat 中,我们将任务定义在 tasks 目录下,我们将编写一个 block-number.js 的 Task 来获取区块高度:

const { task } = require('hardhat/config')

task('block-number', 'Prints the current block number').setAction(
  async (taskArgs, hre) => {
    const blockNumber = await hre.ethers.provider.getBlockNumber()
    console.log(`Current Block Number: ${blockNumber}`)
  }
)

Task 通过 task() 方法来创建,并通过 setAction() 方法来设置任务的执行函数。其中,taskArgs 是一个包含所有参数的对象,hre 是一个 HardhatRuntimeEnvironment 对象,可以用来获取其他的资源。

运行 Task

定义完成后,在项目命令的 AVAILABLE TASKS 中就有了我们刚定义好的 block-number 任务,可以通过 yarn hardhat block-number 命令来运行任务,同样的,我们可以指定特定网络运行:

yarn hardhat block-number --network rinkeby

hardhat_run_tasks

Hardhat Console

最后,除了通过代码与链/合约进行交互外,我们还可以通过 Hardhat Console 来调试项目,查看链状态,合约的输入、输出等。我们可以通过 yarn hardhat console 命令来打开 Hardhat Console,并进行交互。

hardhat_console

总结

以上就是我对 Hardhat 框架的基础配置与使用,它是一个很强大的开发框架,我后续还将会继续深入了解它的更多特性与使用技巧,如果有兴趣,可以继续关注,希望对大家有所帮助。

参考资料

  1. Learn Blockchain, Solidity, and Full Stack Web3 Development with JavaScript
  2. NomicFoundation/hardhat
  3. Hardhat 官方文档
  4. Solidity 智能合约开发 - 基础
  5. Solidity 智能合约开发 - 玩转 Web3.py
  6. Solidity 智能合约开发 - 玩转 ethers.js