hardhat 教程及 hardhat-deploy 插件使用

hardhat 教程及 hardhat-deploy 插件使用

本文在官方 hardhat 教程的基础上,加入了hardhat-deploy 插件的使用介绍,本文代码的 GitHub:https://github.com/wighawag/tutorial-hardhat-deploy

登链社区翻译了 Hardhat 开发者文档,欢迎大家参考, 地址:https://learnblockchain.cn/docs/hardhat/tutorial/

1. 设置环境

大多数以太坊库和工具都是用JavaScript编写的,Hardhat也是如此。 如果你不熟悉Node.js,它是一个建立在Chrome的V8 JavaScript引擎上的JavaScript运行时。 它是在Web浏览器之外运行JavaScript的最流行的解决方案,Hardhat也是基于Node.js开发的。

安装Node.js

如果你已经安装了Node.js>=12.0,你可以跳过本节。 如果没有,这里介绍如何在Ubuntu、MacOS和Windows上安装Node.js。

Linux

Ubuntu

在终端中复制并粘贴这些命令:

sudo apt update
sudo apt install curl git
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt install nodejs

MacOS

确保你已经安装了git。 否则,请遵循这个安装说明安装 git

在MacOS上安装Node.js有多种方法。 我们将使用node版本管理器(nvm), 在终端中复制并粘贴这些命令:

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

Windows

在Windows上安装Node.js需要几个手动步骤, 我们将安装git、Node.js 12.x和npm, 下载并运行:

  1. Git的Windows安装程序
  2. 安转此处node-v12.XX.XX-x64.msi

升级你的Node.js装置

如果你的Node.js版本比12.0低,请按照下面的说明进行升级。

Linux

Ubuntu

  1. 在终端中运行sudo apt remove nodejs来删除Node.js。
  2. 这里找到你要安装的Node.js版本,然后按照说明操作。
  3. 在终端运行sudo apt update && sudo apt install nodejs再次安装Node.js。

MacOS

你可以使用nvm更改你的Node.js版本。 要升级到Node.js 12.x,请在终端运行这些。

nvm install 12
nvm use 12
nvm alias default 12
npm install npm --global # Upgrade npm to the latest version

Windows

你需要遵循相同的安装说明和之前一样,但要选择不同的版本。 你可以查看所有可用版本的列表这里

安装yarn

在本教程中,我们将使用yarn

要安装它,请执行以下操作:

npm install -g yarn

2. 创建一个Hardhat项目

我们将使用npm CLI安装HardhatNode.js package manager是一个包管理器和JavaScript代码的在线存储库。

打开一个新的终端,运行这些命令。

mkdir hardhat-deploy-tutorial
cd hardhat-deploy-tutorial
yarn init --yes
yarn add -D hardhat

安装 Hardhat会安装一些以太坊 JavaScript依赖,所以要耐心等待。

在安装Hardhat的同一个目录下,添加一个hardhat.config.ts(我们将使用typescript和solidity 0.7.6编译器)。

import {HardhatUserConfig} from 'hardhat/types';
const config: HardhatUserConfig = {
  solidity: {
    version: '0.7.6',
  }
};
export default config;

Hardhat的架构

Hardhat是围绕任务(Tasks)插件(plugs)的概念设计的。 Hardhat的大部分功能来自于插件,作为开发者可以自由选择你想使用的插件。

任务

每次你从CLI运行Hardhat时,你都在运行一个任务,例如npx hardhat compile就是在运行compile任务。 要查看项目中当前可用的任务,运行npx hardhat。 可以通过运行 npx hardhat help [task]来自由探索任何任务。

你可以创建自己的任务。 查看创建任务指南。

插件

Hardhat不限制你使用什么工具,但它也内置了一些默认的工具。 所有这些都可以替换掉。 大多数时候,使用某个工具的方式是通过插件集成到Hardhat中。

在本教程中,我们将使用hardhat-deploy-ethers和hardhat-deploy插件。 它们将允许你与以太坊交互,并测试合约。 后面我们会解释如何使用的。 我们还安装了ethers chai和Mocha以及typescript。在项目目录下运行以下命令安装它们:

yarn add -D hardhat-deploy hardhat-deploy-ethers ethers chai chai-ethers mocha @types/chai @types/mocha @types/node typescript ts-node dotenv

编辑hardhat.config.ts,使其看起来像这样:

import {HardhatUserConfig} from 'hardhat/types';
import 'hardhat-deploy';
import 'hardhat-deploy-ethers';

const config: HardhatUserConfig = {
  solidity: {
    version: '0.7.6',
  },
  namedAccounts: {
    deployer: 0,
  },
};
export default config;

我们还创建以下tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist"
  },

  "include": [
    "hardhat.config.ts",
    "./deploy",
    "./test",
  ]
}

3. 编写和编译智能合约

我们创建一个简单的智能合约,实现一个可以转让的代币。 代币合约最常用来交换或储存价值。 在本教程中,我们不会深入讲解合约的Solidity代码,但你应该知道实现的逻辑:

  • 代币的发行总量是固定的,不能更改。
  • 所有发行的代币都分配到部署合约的地址。
  • 任何人都可以接受代币。
  • 拥有至少一块代币的人,都可以转让代币。
  • 代币是不可分割的。 你可以转让1、2、3或37枚代币,但不能转让2.5枚。

你可能听说过ERC20,它是以太坊中的一种代币标准。 DAI、USDC、MKR和ZRX等代币遵循ERC20标准,这使得它们都能与任何可以处理ERC20代币的软件兼容。 为了简单起见,我们要建立的代币不是ERC20

编写智能合约

虽然默认情况下,hardhat使用 contracts作为合约代码源文件夹,但我们更倾向于将其改为 src

因此,你需要用新的配置来编辑你的hardhat.config.ts文件。

import {HardhatUserConfig} from 'hardhat/types';
import 'hardhat-deploy';
import 'hardhat-deploy-ethers';

const config: HardhatUserConfig = {
  solidity: {
    version: '0.7.6',
  },
  namedAccounts: {
    deployer: 0,
  },
  paths: {
    sources: 'src',
  },
};
export default config;

首先创建一个名为 src的新目录,并在该目录内创建一个名为 Token.sol的文件。

将下面的代码粘贴到文件中,建议花一分钟时间阅读代码及注释。

要获得Solidity语法高亮支持, 我们推荐使用Visual Studio Code或Sublime Text 3,并安装对应的 Solidity 或 以太坊插件。

// SPDX-License-Identifier: MIT
// The line above is recommended and let you define the license of your contract
// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.7.0;

// This is the main building block for smart contracts.
contract Token {
    // Some string type variables to identify the token.
    // The `public` modifier makes a variable readable from outside the contract.
    string public name = "My Hardhat Token";
    string public symbol = "MBT";

    // 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.
     */
    constructor(address _owner) {
        // The totalSupply is assigned to transaction sender, which is the account
        // that is deploying the contract.
        balances[_owner] = totalSupply;
        owner = _owner;
    }

    /**
     * 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");

        // 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];
    }
}

*.sol是Solidity文件的后缀,同时建议文件名与其包含的合约名称进行匹配,这是一种常见的做法。

编译合约

要编译合约,请在你的终端运行yarn hardhat compilecompile是内置任务之一。

$ yarn hardhat compile
Compiling 1 file with 0.7.3
Compilation finished successfully

合约已经编译成功,可以使用了。

4. 部署脚本

在能够测试或部署合约之前,你需要设置部署脚本,以便在测试和准备部署时使用。 部署脚本让你可以专注于合约的最终形式,设置它们的参数和依赖关系,并确保你的测试的是将要部署的内容。 部署脚本也省去了重复部署的烦恼。

编写部署脚本

在我们的项目根目录下创建一个名为 deploy的新目录,并创建一个名为 001_deploy_token.ts的新文件。

将下面的代码粘贴到001_deploy_token.ts中:

import {HardhatRuntimeEnvironment} from 'hardhat/types';
import {DeployFunction} from 'hardhat-deploy/types';

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
  const {deployments, getNamedAccounts} = hre;
  const {deploy} = deployments;

  const {deployer, tokenOwner} = await getNamedAccounts();

  await deploy('Token', {
    from: deployer,
    args: [tokenOwner],
    log: true,
  });
};
export default func;
func.tags = ['Token'];

你注意到了 getNamedAccounts吗?

插件 hardhat-deploy允许你命名你的账户,这里有2个记名账户:

  • deployer 将是用于部署合约的账户
  • tokenOwner,这可能是传递给Token.sol构造函数的另一个账户,它将接受最初的发行的代币。

这些账户需要在hardhat.config.ts中进行设置。

修改一下,让它看起来像这样:

import {HardhatUserConfig} from 'hardhat/types';
import 'hardhat-deploy';
import 'hardhat-deploy-ethers';

const config: HardhatUserConfig = {
  solidity: {
    version: '0.7.6',
  },
  namedAccounts: {
    deployer: 0,
    tokenOwner: 1,
  },
  paths: {
    sources: 'src',
  },
};
export default config;

deployer 被设置为使用第一个账户(index = 0),tokenOwner是第二个账户。

在你的终端上运行yarn hardhat deploy。 你应该看到以下输出。

Nothing to compile
deploying "Token" (tx: 0x259d19f33819ec8d3bd994f82912aec6af1a18ec5d74303cfb28d793a10ff683)...: deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 592983 gas
Done in 3.66s.

此次部署是在 内存中的hardhat网络中进行的,上面的提示表明部署成功。

现在我们可以针对这个合约编写测试了,它的名称设置为与合约名称相同:Token

不过我们先给上面的部署脚本中添加注释,解释每一行的重要性:

import {HardhatRuntimeEnvironment} from 'hardhat/types'; // this add the type from hardhat runtime environment
import {DeployFunction} from 'hardhat-deploy/types'; // this add the type that a deploy function is expected to fullfil

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { // 部署函数把hardhat运行时作为参数
  const {deployments, getNamedAccounts} = hre; // we get the deployments and getNamedAccounts which are provided by hardhat-deploy
  const {deploy} = deployments; // the deployments field itself contains the deploy function

  const {deployer, tokenOwner} = await getNamedAccounts(); // we fetch the accounts. These can be configured in hardhat.config.ts as explained above

  await deploy('Token', { // this will create a deployment called 'Token'. By default it will look for an artifact with the same name. the contract option allows you to use a different artifact
    from: deployer, // deployer will be performing the deployment transaction
    args: [tokenOwner], // tokenOwner is the address used as the first argument to the Token contract's constructor
    log: true, // display the address and gas used in the console (not when run in test though)
  });
};
export default func;
func.tags = ['Token']; // this setup a tag so you can execute the script on its own (and its dependencies)

5. 测试合约

在构建智能合约时,编写自动化测试是至关重要的,因为这关系到用户的资金。 为此我们将使用Hardhat网络,这是一个为开发而设计的本地以太坊网络,是内置的,也是Hardhat中的默认网络。 在我们的测试中,将使用ethers.js与前面部署的以太坊合约进行交互,并使用Mocha作为我们的测试运行器。

编写测试

在项目根目录下创建一个名为 test的新目录,并创建一个名为 Token.test.ts的新文件。

把下面的代码粘贴到Token.test.ts中:

import {expect} from "./chai-setup";

import {ethers, deployments, getNamedAccounts} from 'hardhat';

describe("Token contract", function() {
  it("Deployment should assign the total supply of tokens to the owner", async function() {
    await deployments.fixture(["Token"]);
    const {tokenOwner} = await getNamedAccounts();
    const Token = await ethers.getContract("Token");
    const ownerBalance = await Token.balanceOf(tokenOwner);
    const supply = await Token.totalSupply();
    expect(ownerBalance).to.equal(supply);
  });
});

依赖的chai-setup 同样在测试文件夹中,内容如下:

import chaiModule from 'chai';
import {chaiEthers} from 'chai-ethers';
chaiModule.use(chaiEthers);
export = chaiModule;

然后在终端上运行npx hardhat test。 你应该看到以下输出。

$ npx hardhat test

  Token contract
    ✓ Deployment should assign the total supply of tokens to the owner (654ms)

  1 passing (663ms)

这意味着测试通过了。 现在我们来解释一下每一行代码。

await deployments.fixture(["Token"]);

还记得你之前写的deploy脚本,这一行允许在测试前执行。 它还会自动生成一个evm_snapshot,所以如果你写了很多测试,并且它们都指向那个fixture,那么在背后就不会一次次重复部署。 而是恢复到以前的状态,自动加快你的测试速度。

const {tokenOwner} = await getNamedAccounts();

这样你就可以访问tokenOwner的地址,也就是部署脚本中使用的地址。

const Token = await ethers.getContract("Token");

由于我们已经执行了部署脚本,所以我们可以很容易地通过名称访问已部署的合约。 这就是这一行的作用,感谢hardhat-deploy-ethers插件,很容易得到一个可被调用的ethers合约。 如果需要将该合约关联到一个特定的签名者,可以将地址作为额外的参数传递,比如const TokenAsOwner = await ethers.getContract('Token', tokenOwner);

const ownerBalance = await Token.balanceOf(tokenOwner);

可以在Token上调用合约方法,通过调用balanceOf()来获取所有者账户的余额。

const supply = await Token.totalSupply();

在这里,再次使用Token实例调用一个智能合约函数, totalSupply()返回代币的发行量。

expect(ownerBalance).to.equal(supply);

最后,我们检查它是否等于ownerBalance

为此,我们使用Chai,它是一个断言库。 这些断言函数被称为 matchers,在这里使用的函数实际上来自chai-ethers包(它本身就是Waffle chai matchers的一个fork,没有不必要的依赖)。

使用不同的账户

如果你需要从默认账户以外的账户发送交易来测试你的代码,你可以使用getContract的第二个参数。

import {expect} from "./chai-setup";

import {ethers, deployments, getNamedAccounts, getUnnamedAccounts} from 'hardhat';

describe("Token contract", function() {
  it("Deployment should assign the total supply of tokens to the owner", async function() {
    await deployments.fixture(["Token"]);
    const {tokenOwner} = await getNamedAccounts();
    const users = await getUnnamedAccounts();
    const TokenAsOwner = await ethers.getContract("Token", tokenOwner);
    await TokenAsOwner.transfer(users[0], 50);
    expect(await TokenAsOwner.balanceOf(users[0])).to.equal(50);

    const TokenAsUser0 = await ethers.getContract("Token", users[0]);
    await TokenAsUser0.transfer(users[1], 50);
    expect(await TokenAsOwner.balanceOf(users[1])).to.equal(50);
  });
});

全面覆盖测试

现在我们已经介绍了测试合约所需的基础知识,这里有一个完整的代币测试Case,其中有很多关于Mocha的附加信息以及如何构建测试。 我们建议通过阅读。

但首先我们要添加一些实用函数,我们将在该测试套件中使用。

test文件夹中创建一个 utils文件夹,并在其中创建一个 index.ts文件,内容如下:

import {Contract} from 'ethers';
import {ethers} from 'hardhat';

export async function setupUsers<T extends {[contractName: string]: Contract}>(
  addresses: string[],
  contracts: T
): Promise<({address: string} & T)[]> {
  const users: ({address: string} & T)[] = [];
  for (const address of addresses) {
    users.push(await setupUser(address, contracts));
  }
  return users;
}

export async function setupUser<T extends {[contractName: string]: Contract}>(
  address: string,
  contracts: T
): Promise<{address: string} & T> {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const user: any = {address};
  for (const key of Object.keys(contracts)) {
    user[key] = contracts[key].connect(await ethers.getSigner(address));
  }
  return user as {address: string} & T;
}

通过utils 可以方便的创建账号,让测试简洁和容易阅读,例如下面的Test.test.ts:

// We import Chai to use its asserting functions here.
import {expect} from "./chai-setup";

// we import our utilities
import {setupUsers, setupUser} from './utils';

// We import the hardhat environment field we are planning to use
import {ethers, deployments, getNamedAccounts, getUnnamedAccounts} from 'hardhat';

// we create a stup function that can be called by every test and setup variable for easy to read tests
async function setup () {
  // it first ensure the deployment is executed and reset (use of evm_snaphost for fast test)
  await deployments.fixture(["Token"]);

  // we get an instantiated contract in teh form of a ethers.js Contract instance:
  const contracts = {
    Token: (await ethers.getContract('Token')),
  };

  // we get the tokenOwner
  const {tokenOwner} = await getNamedAccounts();
  // get fet unnammedAccounts (which are basically all accounts not named in the config, useful for tests as you can be sure they do not have been given token for example)
  // we then use the utilities function to generate user object/
  // These object allow you to write things like `users[0].Token.transfer(....)`
  const users = await setupUsers(await getUnnamedAccounts(), contracts);
  // finally we return the whole object (including the tokenOwner setup as a User object)
  return {
    ...contracts,
    users,
    tokenOwner: await setupUser(tokenOwner, contracts),
  };
}

// `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() {

  // 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 object. These
      // objects have a lot of utility methods to assert values.

      // before the test, we call the fixture function.
      // while mocha have hooks to perform these automatically, they force you to declare the variable in greater scope which can introduce subttle errors
      // as such we prefers to have the setup called right at the beginning of the test. this also allow yout o name it accordingly for easier to read tests.
      const {Token} = await setup();

      // This test expects the owner variable stored in the contract to be equal to our configured owner
      const {tokenOwner} = await getNamedAccounts();
      expect(await Token.owner()).to.equal(tokenOwner);
    });

    it("Should assign the total supply of tokens to the owner", async function () {
      const {Token, tokenOwner} = await setup();
      const ownerBalance = await Token.balanceOf(tokenOwner.address);
      expect(await Token.totalSupply()).to.equal(ownerBalance);
    });
  });

  describe("Transactions", function () {
    it("Should transfer tokens between accounts", async function () {
      const {Token, users, tokenOwner} = await setup();
      // Transfer 50 tokens from owner to users[0]
      await tokenOwner.Token.transfer(users[0].address, 50);
      const users0Balance = await Token.balanceOf(users[0].address);
      expect(users0Balance).to.equal(50);

      // Transfer 50 tokens from users[0] to users[1]
      // We use .connect(signer) to send a transaction from another account
      await users[0].Token.transfer(users[1].address, 50);
      const users1Balance = await Token.balanceOf(users[1].address);
      expect(users1Balance).to.equal(50);
    });

    it("Should fail if sender doesn’t have enough tokens", async function () {
      const {Token, users, tokenOwner} = await setup();
      const initialOwnerBalance = await Token.balanceOf(tokenOwner.address);

      // Try to send 1 token from users[0] (0 tokens) to owner (1000 tokens).
      // `require` will evaluate false and revert the transaction.
      await expect(users[0].Token.transfer(tokenOwner.address, 1)
      ).to.be.revertedWith("Not enough tokens");

      // Owner balance shouldn't have changed.
      expect(await Token.balanceOf(tokenOwner.address)).to.equal(
        initialOwnerBalance
      );
    });

    it("Should update balances after transfers", async function () {
      const {Token, users, tokenOwner} = await setup();
      const initialOwnerBalance = await Token.balanceOf(tokenOwner.address);

      // Transfer 100 tokens from owner to users[0].
      await tokenOwner.Token.transfer(users[0].address, 100);

      // Transfer another 50 tokens from owner to users[1].
      await tokenOwner.Token.transfer(users[1].address, 50);

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

      const users0Balance = await Token.balanceOf(users[0].address);
      expect(users0Balance).to.equal(100);

      const users1Balance = await Token.balanceOf(users[1].address);
      expect(users1Balance).to.equal(50);
    });
  });
});

下面是 yarn hardhat test的输出。

$ yarn hardhat test

  Token contract
    Deployment
      ✓ Should set the right owner
      ✓ Should assign the total supply of tokens to the owner
    Transactions
      ✓ Should transfer tokens between accounts (199ms)
      ✓ Should fail if sender doesn’t have enough tokens
      ✓ Should update balances after transfers (111ms)

  5 passing (1s)

请记住,当你运行 yarn hardhat test时,如果合约在你上次运行测试后发生了变化,合约将被重新编译。

6. 使用Hardhat网络进行调试

Hardhat内置了Hardhat网络,这是一个专为开发设计的本地以太坊网络。 它允许你部署合约,运行测试和调试代码。 这是Hardhat连接到的默认网络,所以你不需要设置任何东西就可以工作,只需要简单运行测试。

Solidity console.log

当你在Hardhat Network上运行合约和测试时,可以在Solidity代码中调用console.log()打印日志信息和合约变量。 要使用它,必须在合约代码中导入Hardhatconsole.log

例如:

pragma solidity 0.7.6;

import "hardhat/console.sol";

contract Token {
  //...
}

transfer()函数中添加一些console.log,就像在JavaScript中使用一样。

function transfer(address to, uint256 amount) external {
    console.log("Sender balance is %s tokens", balances[msg.sender]);
    console.log("Trying to send %s tokens to %s", amount, to);

    require(balances[msg.sender] >= amount, "Not enough tokens");

    balances[msg.sender] -= amount;
    balances[to] += amount;
}

当你运行测试时,日志输出将显示:

$ yarn hardhat test

  Token contract
    Deployment
      ✓ Should set the right owner
      ✓ Should assign the total supply of tokens to the owner
    Transactions
Sender balance is 1000 tokens
Trying to send 50 tokens to 0xead9c93b79ae7c1591b1fb5323bd777e86e150d4
Sender balance is 50 tokens
Trying to send 50 tokens to 0xe5904695748fe4a84b40b3fc79de2277660bd1d3
      ✓ Should transfer tokens between accounts (373ms)
      ✓ Should fail if sender doesn’t have enough tokens
Sender balance is 1000 tokens
Trying to send 100 tokens to 0xead9c93b79ae7c1591b1fb5323bd777e86e150d4
Sender balance is 900 tokens
Trying to send 100 tokens to 0xe5904695748fe4a84b40b3fc79de2277660bd1d3
      ✓ Should update balances after transfers (187ms)

  5 passing (2s)

查看文档来了解更多关于这个功能的信息。

7. 部署到真实的网络

一旦准备好与其他人分享应用程序,你可能想做的是部署到一个实时网络。 这样其他人就可以访问到。

处理真金白银的以太坊网络被称为 主网(mainnet),还有其他不处理真金白银的网络,但确实能很好地模拟真实世界的场景, 这些被称为 测试网(testnet),以太坊有多个测试网: RopstenKovanRinkebyGoerli

在软件层面,部署到testnet和部署到mainnet是一样的。 唯一不同的是连接的网络。

由于我们使用了hardhat-deploy插件,并且我们已经编写了部署脚本,现在只需要对部署到的网络进行一些配置,就可以部署到真实网络中。

正如我们的部署部分所解释的那样,你可以执行yarn hardhat deploy,但它只部署在内存中模式的默认的网络(hardhat)中,输出如下:

Nothing to compile
deploying "Token" (tx: 0x259d19f33819ec8d3bd994f82912aec6af1a18ec5d74303cfb28d793a10ff683)...: deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 592983 gas
Done in 3.79s.

要部署到特定的网络,你需要添加--network <network-name>,像这样。

yarn hardhat --network <network-name> deploy

部署到远程网络

要部署到远程网络,如mainnet或任何testnet,你需要在你的hardhat.config.js文件中添加一个network条目。 我们将使用Rinkeby来做这个例子,但你可以类似地添加任何网络。

为了更方便地处理私钥和网络配置,在项目的根部创建了一个新的文件夹utils

我们在其中创建一个文件network.ts,内容如下。

import 'dotenv/config';
export function node_url(networkName: string): string {
  if (networkName) {
    const uri = process.env['ETH_NODE_URI_' + networkName.toUpperCase()];
    if (uri && uri !== '') {
      return uri;
    }
  }

  let uri = process.env.ETH_NODE_URI;
  if (uri) {
    uri = uri.replace('{{networkName}}', networkName);
  }
  if (!uri || uri === '') {
    if (networkName === 'localhost') {
      return 'http://localhost:8545';
    }
    return '';
  }
  if (uri.indexOf('{{') >= 0) {
    throw new Error(
      `invalid uri or network not supported by nod eprovider : ${uri}`
    );
  }
  return uri;
}

export function getMnemonic(networkName?: string): string {
  if (networkName) {
    const mnemonic = process.env['MNEMONIC_' + networkName.toUpperCase()];
    if (mnemonic && mnemonic !== '') {
      return mnemonic;
    }
  }

  const mnemonic = process.env.MNEMONIC;
  if (!mnemonic || mnemonic === '') {
    return 'test test test test test test test test test test test junk';
  }
  return mnemonic;
}

export function accounts(networkName?: string): {mnemonic: string} {
  return {mnemonic: getMnemonic(networkName)};
}

然后我们可以修改hardhat.config.ts文件,使其包含以下内容。

import {HardhatUserConfig} from 'hardhat/types';
import 'hardhat-deploy';
import 'hardhat-deploy-ethers';
import {node_url, accounts} from './utils/network';

const config: HardhatUserConfig = {
  solidity: {
    version: '0.7.6',
  },
  networks: {
    rinkeby: {
      url: node_url('rinkeby'),
      accounts: accounts('rinkeby'),
    },
  },
  namedAccounts: {
    deployer: 0,
    tokenOwner: 1,
  },
  paths: {
    sources: 'src',
  },
};
export default config;

最后,我们需要设置环境变量,让utils/networks.ts.env中自动读取。

创建一个.env,在其中写上你自己的alchemy api键和rinkeby的助记词。

ETH_NODE_URI_RINKEBY=https://eth-rinkeby.alchemyapi.io/v2/<alchmey api key>
MNEMONIC_RINKEBY=<mnemonic for rinkeby>

我们使用的是Alchemy,你可以使用任何其他指向以太坊节点或网关的 URL。

要在Rinkeby上部署,你需要把rinkeby-ETH发送到要进行部署的地址。 你可以从水龙头(一个免费分发测试-ETH的服务, 如https://faucet.metamask.io/)那里获得一些ETH的测试网。

你可以通过以下链接获得一些ETH,用于其他测试网。

然后运行:

yarn hardhat --network rinkeby deploy

如果一切顺利,你应该看到这样的内容:

Nothing to compile
deploying "Token" (tx: 0xb40879c3162e6a924cfadfc1027c4629dd57ee4ba08a5f8af575be1c751cd515)...: deployed at 0x8bDFEf5f67685725BC0eD9f54f20A2A4d3FEDA98 with 475842 gas

你还会看到在deployments/rinkeby文件夹中创建了一些文件。

最值得注意的是,你会看到deployments/rinkeby/Token.json,其中包含了你部署的合约信息,包括addres、abi以及用于创建合约的solidity输入。

然后你可以用sourcify或etherscan来验证它。

对于sourceify,你可以执行以下操作:

yarn hardhat --network rinkeby sourcify

这应该给你以下输出(你的地址会不同)。

verifying Token (0x8bDFEf5f67685725BC0eD9f54f20A2A4d3FEDA98 on chain 4) ...
 => contract Token is now verified

对于etherscan,你可以执行以下工作(注意,你也可以通过env变量ETHERSCAN_API_KEY指定api密钥):

yarn hardhat --network rinkeby etherscan-verify --api-key <api-key>

然后你应该看到。

verifying Token (0x8bDFEf5f67685725BC0eD9f54f20A2A4d3FEDA98) ...
waiting for result...
 => contract Token is now verified

本翻译由 Cell Network 赞助支持。

  • 发表于 2021-04-08 12:17
  • 阅读 ( 3290 )
  • 学分 ( 223 )
  • 分类:以太坊

0 条评论

请先 登录 后评论
翻译小组
翻译小组

首席翻译官

75 篇文章, 11946 学分