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 => 消息 => 以太坊签名消息
以太坊签名消息 + 签名(通过私钥) => 恢复出公钥 与 公钥进行比对
相同,则之前的消息是由签名者所签发

够用了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容