当前位置: 首页 > 工具软件 > hardhat > 使用案例 >

怎样使用 Hardhat 开发 Solidity 智能合约

岳浩穰
2023-12-01

介绍

在本文中,我们将创建一个简单的智能合约并对其进行测试,然后使用 Hardhat 将其部署在测试网上.

如果你对区块链比较陌生,不用担心。 首先,在逐步编写智能合约之前,我们将回顾 Solidity 和 Hardhat 的一些基础知识。 在本教程结束时,您应该能够使用 Solidity 和 Hardhat 重新创建托管智能合约。 让我们开始吧!

什么是 Solidity?

智能合约是一个简单的程序,它按照作者设定的预定义规则在区块链上执行交易。以太坊的智能合约使用特定的编程语言 Solidity。 Solidity 是一种面向对象的编程语言,专为在以太坊虚拟机 (EVM) 上运行智能合约而构建,其语法类似于其他编程语言 C++、Python 和 JavaScript。

Solidity 将您的智能合约编译成一系列字节码,然后再将其部署到以太坊虚拟机中。每个智能合约都有它的地址。要调用特定函数,您需要一个应用程序二进制接口 (ABI) 来指定要执行的函数并返回您期望的格式。

创建智能合约需要一个用于在测试网上测试和部署合约的开发环境。有很多替代品可供选择,例如 Truffle 及其 Ganache 套件或 Remix、Solidity IDE。但是还有第三种选择,Hardhat

什么事 Hardhat

Hardhat 是一个使用 Node.js 构建的 Solidity 开发环境。 它于 2019 年首次发布 Beta 版,此后一直在增长。 使用 Hardhat,开发人员无需离开 JavaScript 和 Node.js 环境即可开发智能合约,就像使用 Truffle 一样。

测试使用 Hardhat 构建的智能合约也很容易,因为 Hardhat 具有即插即用的环境,并且不需要您设置个人以太坊网络来测试您的智能合约。 要连接到智能合约,您可以使用 Ethers.js,并且要测试它们,您可以使用著名的 JavaScript 测试库,例如 Chai

安装 Hardhat

首先安装 nodejs 环境

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.1/install.sh | bash
nvm install 16
nvm use 16
nvm alias default 16
npm install npm --global # Upgrade npm to the latest version

其次创建一个 hardhat 项目

mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat

编写你的第一个 Solidity 智能合约

在此示例中,您将创建一个简单的智能合约,类似于 Tornado Cash。 每个执行智能合约的用户都会向智能合约存入一定数量的代币,智能合约会返回一个哈希值。 您可以使用哈希将代币提取到不同的帐户中。

//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.0;

// We import this library to be able to use console.log
import "hardhat/console.sol";


// 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 balance.
    mapping(address => uint256) balances;

    /**
     * Contract initialization.
     *
     * The `constructor` is executed only once when the contract is created.
     * The `public` modifier makes a function callable from outside the contract.
     */
    constructor() {
        // The totalSupply is assigned to 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");

        // We can print messages and values using console.log
        console.log(
            "Transferring from %s to %s %s tokens",
            msg.sender,
            to,
            amount
        );

        // Transfer the amount.
        balances[msg.sender] -= amount;
        balances[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];
    }
}

使用 Hardhat 测试智能合约

使用 Hardhat 的最大优势之一是测试套件非常简单。 如果您已经熟悉 JavaScript 测试,您可以快速适应 Hardhat 的测试,特别是如果您经常使用 Chai

// 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");
const hre = require("hardhat");

// `describe` is a Mocha function that allows you to organize your tests. It's
// not actually needed, but 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 () {
  // Mocha has four functions that let you hook into the the test runner's
  // lifecycle. These are: `before`, `beforeEach`, `after`, `afterEach`.

  // They're very useful to setup the environment for tests, and to clean it
  // up after they run.

  // A common pattern is to declare some variables, and assign them in the
  // `before` and `beforeEach` callbacks.

  let Token;
  let hardhatToken;
  let owner;
  let address1;
  let address2;
  let address;

  // `beforeEach` will run before each test, re-deploying the contract every
  // time. It receives a callback, which can be async.
  beforeEach(async function () {
    // Get the ContractFactory and Signers here.
    Token = await hre.ethers.getContractFactory("Token");
    [owner, address1, address2, ...address] = await hre.ethers.getSigners();

    // To deploy our contract, we just have to call Token.deploy() and await
    // for it to be deployed(), which happens onces its transaction has been
    // mined.
    hardhatToken = await Token.deploy();

    // We can interact with the contract by calling `hardhatToken.method()`
    await hardhatToken.deployed();
  });

  // You can nest describe calls to create subsections.
  describe("Deployment", function () {
    // `it` is another Mocha function. This is the one you use to define 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 () {
      // Expect receives a value, and wraps it in an assertion objet. 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 ownerBalance = await hardhatToken.balanceOf(owner.address);
      expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
    });
  });

  describe("Transactions", function () {
    it("Should transfer tokens between accounts", async function () {
      // Transfer 50 tokens from owner to address1
      await hardhatToken.transfer(address1.address, 50);
      const address1Balance = await hardhatToken.balanceOf(
        address1.address
      );
      expect(address1Balance).to.equal(50);

      // Transfer 50 tokens from address1 to address2
      // We use .connect(signer) to send a transaction from another account
      await hardhatToken.connect(address1).transfer(address2.address, 50);
      const address2Balance = await hardhatToken.balanceOf(
        address2.address
      );
      expect(address2Balance).to.equal(50);
    });

    it("Should fail if sender doesn’t have enough tokens", async function () {
      const initialOwnerBalance = await hardhatToken.balanceOf(
        owner.address
      );

      // Try to send 1 token from address1 (0 tokens) to owner (1000 tokens).
      // `require` will evaluate false and revert the transaction.
      await expect(
        hardhatToken.connect(address1).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
      );
    });

    it("Should update balances after transfers", async function () {
      const initialOwnerBalance = await hardhatToken.balanceOf(
        owner.address
      );

      // Transfer 100 tokens from owner to address1.
      await hardhatToken.transfer(address1.address, 100);

      // Transfer another 50 tokens from owner to address2.
      await hardhatToken.transfer(address2.address, 50);

      // Check balances.
      const finalOwnerBalance = await hardhatToken.balanceOf(
        owner.address
      );
      expect(finalOwnerBalance).to.equal(initialOwnerBalance - 150);

      const address1Balance = await hardhatToken.balanceOf(
        address1.address
      );
      expect(address1Balance).to.equal(100);

      const address2Balance = await hardhatToken.balanceOf(
        address2.address
      );
      expect(address2Balance).to.equal(50);
    });
  });
});

智能合约部署到测试网络

// This is a script for deploying your contracts. You can adapt it to deploy
// yours, or create new ones.

const hre = require("hardhat");

async function main() {
  // This is just a convenience check
  if (hre.network.name === "hardhat") {
    console.warn(
      "You are trying to deploy a contract to the Hardhat Network, which" +
      "gets automatically created and destroyed every time. Use the Hardhat" +
      " option '--network localhost'"
    );
  }

  // ethers is available in the global scope
  const [deployer] = await hre.ethers.getSigners();
  console.log(
    "Deploying the contracts with the account:",
    await deployer.getAddress()
  );

  console.log("Account balance:", (await deployer.getBalance()).toString());

  const Token = await hre.ethers.getContractFactory("Token");
  const token = await Token.deploy();
  await token.deployed();

  console.log("Token address:", token.address);

  // We also save the contract's artifacts and address in the frontend directory
  saveFrontendFiles(token);
}

function saveFrontendFiles(token) {
  const fs = require("fs");
  const contractsDir = __dirname + "/../frontend/src/contracts";

  if (!fs.existsSync(contractsDir)) {
    fs.mkdirSync(contractsDir);
  }

  fs.writeFileSync(
    contractsDir + "/contract-address.json",
    JSON.stringify({ Token: token.address }, undefined, 2)
  );

  const TokenArtifact = hre.artifacts.readArtifactSync("Token");

  fs.writeFileSync(
    contractsDir + "/Token.json",
    JSON.stringify(TokenArtifact, null, 2)
  );
}

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

使用 Hardhat 运行本地以太坊网络

npx hardhat node

上面的命令将在端口 8545 上本地启动一个新的 Ethereum RPC 服务器。您可以创建一个前端应用程序并使用 Metamask 连接到您的本地 RPC 服务器。

部署智能合约

在本地或像 Rinkeby 这样的测试网上部署您的智能合约非常相似。 您可以使用 --network 标志定义要将智能合约部署到哪个网络。 如果要部署到本地网络,命令如下:

npx hardhat run scripts/deploy.js --network localhost
 类似资料: