Hardhat极简

参考官网教程,以下为macOS环境。

1 环境

先安装(升级类似):

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
nvm install 22
nvm use 22
nvm alias default 22 # 终端打开后默认22
npm install npm --global # 全局升级npm

2 新建项目

mkdir hardhat-tutorial
cd hardhat-tutorial

接下来可以npm init,会问几个问题,最后生成一个package.json。感觉没必要,还是直接安装hardhat(只是针对当前目录):

npm install --save-dev hardhat # 只是dev环境的依赖,去掉--save-dev应该也行

然后初始化:

npx hardhat init

四下回车,就初始化成功了,最后一步的@nomicfoundation/hardhat-toolbox也是推荐使用的。然后npx hardhat命令可以查看当前可用任务(命令)。

3 写合约

还是很容易看懂的,code ./打开VSCode,在contracts目录下新建Token.sol

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract Token {
    string public name = "My Hardhat Token";
    string public symbol = "MHT";

    uint256 public totalSupply = 1000000;
    address public owner;

    mapping(address => uint256) balances;

    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    constructor() {
        balances[msg.sender] = totalSupply;
        owner = msg.sender;
    }

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

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

    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }
}

保存、编译npx hardhat compile

4 测试合约

建一个测试代码文件,如图:


image.png

更专业内容如下:

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

describe("Token contract", function () { // not async
  // 所有测试的共同部分, 相当于一个快照存盘点
  async function deployTokenFixture() {
    const [owner, addr1, addr2] = await ethers.getSigners(); // 框架默认会用第1个账户部署合约, 成为owner
    const hardhatToken = await ethers.deployContract("Token");
    await hardhatToken.waitForDeployment();

    return { hardhatToken, owner, addr1, addr2 };
  }

  // describe嵌套分组
  describe("Deployment", function () {
    it("Should set the right owner", async function () { // async, Mocha will `await` it.
      const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
      expect(await hardhatToken.owner()).to.equal(owner.address); // solidity自动为public状态变量owner生成getter函数, 而不能像访问属性那样
    });

    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);
      await expect(
        hardhatToken.transfer(addr1.address, 50)
      ).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);

      await expect(
        hardhatToken.connect(addr1).transfer(addr2.address, 50) // connect切换账号
      ).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
    });

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

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

      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);

      await expect(
        hardhatToken.connect(addr1).transfer(owner.address, 1)
      ).to.be.revertedWith("Not enough tokens");

      expect(await hardhatToken.balanceOf(owner.address)).to.equal(initialOwnerBalance);
    });
  });
});

接下来跑测试npx hardhat test test/Token.js,注意:如果不写最后一个路径参数,就会跑test目录下所有测试;如果sol文件变动了,会自动编译。
推荐详读:

https://hardhat.org/tutorial/testing-contracts

5 调试

Hardhat最方便的就是console.log(),使用打印调试法,加在Token.sol文件里。

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

用法类似printf。最终结果:

  Token contract
    Deployment
      ✔ Should set the right owner (1077ms)
      ✔ Should assign the total supply of tokens to the owner
    Transactions
Transferring from 0xf39fd6e51aa8f6f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010c7db50e0d17dc79c8 50 tokens
Transferring from 0x70997970c812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b585dd2e03d12fa4293bc 50 tokens
      ✔ Should transfer tokens between accounts
Transferring from 0xf39fd6e51aad88f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010d01b50e0d17dc79c8 50 tokens
Transferring from 0x709979c51812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b58d299e03d12fa4293bc 50 tokens
      ✔ Should emit Transfer events
      ✔ Should fail if sender doesn't have enough tokens (61ms)
  5 passing (1s)

6 部署实时网络

先在ignition/modules目录下编写点火脚本Token.js

const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

const TokenModule = buildModule("TokenModule", (m) => {
  const token = m.contract("Token");

  return { token };
});

module.exports = TokenModule;

然后开始配置网络,点火如果不写network,会调用一个嵌入网,最终失联。

6.1 申请节点API

首先去alchemy节点网站(也可以去infuraquicknode等)申请API Key,比如Sepolia的节点如下:

https://eth-sepolia.g.alchemy.com/v2/aqwowVrbkaqwowVrbGbZhtY3vH-X

可以把API Key设置到Hardhat的环境变量里,同时钱包私钥也可以设置下,注意这一步千万小心,不要把主网测试网弄混淆

npx hardhat vars set ALCHEMY_API_KEY
# 输入申请到的aqwowVrbkaqwowVrbGbZhtY3vH-X
npx hardhat vars set SEPOLIA_PRIVATE_KEY
# 输入测试网的钱包私钥

当然也可以后续直接明文写在hardhat.config.js里,但是显得不安全不专业。几个常用命令:

npx hardhat vars get TEST_API_KEY
npx hardhat vars list
npx hardhat vars delete TEST_API_KEY
npx hardhat vars path # 查看存储路径,明文有点风险,更好的方法我暂时没想到
HARDHAT_VAR_MY_KEY=123 npx hardhat some-task # 临时改变量

6.2 配置网络

编辑hardhat.config.js

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

const { vars } = require("hardhat/config");

const ALCHEMY_API_KEY = vars.get("ALCHEMY_API_KEY");
const SEPOLIA_PRIVATE_KEY = vars.get("SEPOLIA_PRIVATE_KEY");

module.exports = {
  solidity: "0.8.28",
  networks: {
    sepolia: {
      url: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
      accounts: [SEPOLIA_PRIVATE_KEY]
    }
  }
};

有的配置还会写chainId,不知道有没有用。
钱包、水龙头领免费币什么的这里不赘述。
最后部署到Sepolia

npx hardhat ignition deploy ./ignition/modules/Token.js --network sepolia

成功后:

✔ Confirm deploy to network sepolia (11155111)? … yes
Hardhat Ignition 🚀

Deploying [ TokenModule ]

Batch #1
  Executed TokenModule#Token

[ TokenModule ] successfully deployed 🚀

Deployed Addresses

TokenModule#Token - 0x222732222222222222222222

ignition目录下也有json文件可以看到地址,Sepolia上可以查到。

7 融合remix

先安装remixd

npm install -g @remix-project/remixd

开个终端,运行:

remixd # 默认使用当前目录
# 或者
remixd -s ./myproject -u https://remix.ethereum.org

然后去remix里连接就行了。

image.png

一般习惯是VS code开发,remix调试,记住,在vscode编辑的时候,一定要先把remix的代码tab给关掉,否则切回remix的时候,代码会被网页里的旧代码秒覆盖掉!
还可以使用Hardhat自带的节点:

cd myproject
npx hardhat node # 会给出20个账号

然后再remix部署环境里就可以选择dev-hardhat provider了。其实用remix自带的VM调试就挺方便的,建议选择shanghai节点,在编译里也选择一下默认的evmshanghai。而cancun有时候真的卡,不用编译和部署时选择不同的VM,会有bug,比如什么opcode错误。
当然也可以使用命令行部署,需要编辑hardhat.config.js,添加localhost

module.exports = {
  solidity: "0.8.28",
  networks: {
    localhost: {
      url: "https://localhost:8545"
    }
  }
};

感觉还是remix里调试方便。

8 其它

ABI导出

安装插件npm install --save-dev hardhat-abi-exporter,然后配置hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");
require('hardhat-abi-exporter');

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.28",
  abiExporter: [
    {
      path: './abi/pretty',
      pretty: true,
    },
    {
      path: './abi/ugly',
      pretty: false,
    }
  ]
};

运行:

npx hardhat export-abi --no-compile # 参考https://www.npmjs.com/package/hardhat-abi-exporter

扁平

把合约整理成一个独立文件:

npx hardhat flatten

Code关系

部署测试的结论:

// 以下input和output只针对部署时
input = bytecode + 构造实参
        bytecode = initCode + runtimeCode
output = runtimeCode

address(this).code = runtimeCode
type(Test).creationCode = bytecode // 需从另一个合约函数读取

另有一个interfaceId是接口的函数签名keccak256后再做异或。

签名

地址 + tokenId => 消息 => 以太坊签名消息
以太坊签名消息 + 签名(通过私钥) => 恢复出公钥 与 公钥进行比对
相同,则之前的消息是由签名者所签发

够用了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容