Hardhatで始めるDapps開発

どうも、次世代デジタル基盤開発事業部の土田です。

最近までLINE Blockchainを触ってましたが、Solidity関連のご相談も増えてますので、置いていかれないように頑張ります。 これまではTruffleSuiteで開発環境を構築してきましたが、今回はHardhatという開発環境を使ってみたいと思います。

TruffleSuiteで始めるDapps開発 - テコテック開発者ブログ

TruffleSuiteで始めるDapps開発2 - テコテック開発者ブログ

TruffleSuiteで始めるDapps開発3 - テコテック開発者ブログ

筆者環境

Windows10
WSL2(Windows Subsystem for Linux)
Ubuntu 20.04
MetaMask
Chrome
Hardhat 2.10.1

Hardhatをインストール

公式ドキュメントをご参照下さい。npmもyarnも特に問題なくインストールできました。

hardhat.org

早速起動

下記コマンドを実行し、サンプルプロジェクトを作成します。

npx hardhat

タスクとは

A task is a JavaScript async function with some associated metadata.

hardhat.org

Hardhatで何かをする時にはタスクとして定義して実行する必要があるそうです。

サンプルプロジェクトで実行できるタスクは下記コマンドを実行すると確認できます。

npx hardhat

JavaScriptのサンプル作成時にはaccountsという独自タスクが定義されていたのですが、TypeScriptでプロジェクトを作成すると無かったので作成してみます。

タスクはルートディレクトリのhardhat.config.tsに定義します。

import { HardhatUserConfig, task } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import { HardhatRuntimeEnvironment } from "hardhat/types";

const accounts = async (args: string, hre: HardhatRuntimeEnvironment) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
};

task("accounts", "Prints the list of accounts", accounts);

const config: HardhatUserConfig = {
  solidity: "0.8.9",
};

export default config;

再度タスクを確認すると・・・

accountsが追加されていますね!

早速実行してみましょう。

npx hardhat accounts

デフォルトで用意されているアカウントを確認することができました。

ノード起動

ノード起動はデフォルトのタスクが用意されていますので、そのまま使いましょう。

npx hardhat node

ついでにMetaMaskで起動したノードに接続し、アカウントもインポートしてみます。

ChainIDはデフォルトで31337だそうです。

hardhat.org

無事、接続&インポート完了しました。

送金も試してみます。

問題なく送金できましたね!
MetaMaskで送金を行った場合、ノードを再起動すると下記エラーが発生することがありますので、MetaMaskのアカウントリセットをお試し下さい。

## Nonce too high. Expected nonce to be 0 but got X. Note that transactions can't be queued when automining.

参考

medium.com

クリプトゾンビを進める

チャプター1の13まで進めます。

cryptozombies.io

当時、特に出なかったエラーが表示されたので、備忘として残しておきます。

Different number of components on the left hand side (1) than on the right hand side (0).

0.6以降でpushの戻り値が変わったそうです。

• Syntax: push(element) for dynamic storage arrays do not return the new length anymore.

github.com

該当箇所を下記の通り修正します。

        // 以下はエラー
        // uint id =  zombies.push(Zombie(_name, _dna)) - 1;

        zombies.push(Zombie(_name, _dna));
        uint id = zombies.length - 1;

コンパイルしてみる

コンパイルされたソース一式はルートディレクトリ直下のartifactsフォルダに生成されます。

npx hardhat compile

また、abiからTypeScriptも生成されます。HardhatではデフォルトでTypeChainを採用しているようです。

hardhat.org

github.com

生成後はルートディレクトリ直下のtypechain-typesに生成されます。 ソースコードはこんな感じ。

これで型安全にコントラクトの開発を行う準備ができました!

テストしてみる

テストコードの配置先のデフォルトはルートディレクトリ直下のtestフォルダになります。今回はZombieFactory.tsとしてテストコードを作成しました。

import { expect } from "chai";
import { ethers } from "hardhat";

describe("ZombieFactory.sol", function () {
  async function deployZombieFactory() {
    const ZombieFactory = await ethers.getContractFactory("ZombieFactory");
    const zombieFactory = await ZombieFactory.deploy();

    return { zombieFactory };
  }

  describe("createRandomZombie", function () {
    it("Verify that the name and DNA of the created zombie has 16 digits.", async function () {
      const { zombieFactory } = await deployZombieFactory();

      const testZombieName = "test";
      await zombieFactory.createRandomZombie(testZombieName);

      const zombie = await zombieFactory.zombies(0);
      expect(zombie.name).to.equal(testZombieName);
      expect(zombie.dna.toString().length).to.equal(16);
    });
  });

  describe("Events", function () {
    it("Should emit an event on createRandomZombie", async function () {
      const { zombieFactory } = await deployZombieFactory();

      await expect(zombieFactory.createRandomZombie("test")).to.emit(
        zombieFactory,
        "NewZombie"
      );
    });
  });
});

実行してみます。

npx hardhat test

無事、テストも通りました!

また、未コンパイルの状態ではコンパイルとテストを合わせて実行するようでした。

デプロイしてみる

デプロイするコントラクトを定義します。scripts/deploy.tsを修正しましょう。

import { ethers } from "hardhat";

async function main() {
  const ZombieFactory = await ethers.getContractFactory("ZombieFactory");
  const zombieFactory = await ZombieFactory.deploy();
 
  await zombieFactory.deployed();

  console.log("ZombieFactory deployed to:", zombieFactory.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

デプロイは下記コマンドを実行します。

npx hardhat run scripts/deploy.ts --network localhost

hardhat consoleでコントラクトを実行する

デプロイしたコントラクトをhardhat consoleでも確認してみます。

tsuchida@DESKTOP-PBCJR7M:~/workspace/hardhat-cryptzombie$ npx hardhat console --network localhost
Welcome to Node.js v16.15.1.
Type ".help" for more information.
> const ZombieFactory = await ethers.getContractFactory('ZombieFactory')
undefined
> const zombiefactory = await ZombieFactory.attach("コントラクトアドレス")
undefined
> await zombiefactory.createRandomZombie("test")

hardhat nodeを実行中のターミナルにて、実行されていることが確認できます。

今回のトラブルまとめ

Different number of components on the left hand side (1) than on the right hand side (0).solidity(7364)View Problem

0.6以降で配列が参照を返すようになったため、要素数を別途取得する必要があります。

参考

ethereum.stackexchange.com

github.com

error: ProviderError: Error: Transaction reverted without a reason string

hardhat consoleでコントラクトを実行した際に、下記のようなエラーが発生しました。この場合は、hardhat.config.tsに設定を追加する必要があります。

> await zombiefactory.createRandomZombie('test')
Uncaught:
Error: cannot estimate gas; transaction may fail or may require manual gas limit [ See: https://links.ethers.org/v5-errors-UNPREDICTABLE_GAS_LIMIT ] (reason="Error: Transaction reverted without a reason string", method="estimateGas", transaction={"from":"アドレス","to":"アドレス","data":"データ","accessList":null}, error={"name":"ProviderError","code":-32603,"_isProviderError":true,"data":{"message":"Error: Transaction reverted without a reason string","data":"0x"}}, code=UNPREDICTABLE_GAS_LIMIT, version=providers/5.6.8)
    at step (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:48:23)
    at EthersProviderWrapper.<anonymous> (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/src.ts/json-rpc-provider.ts:603:20)
    at checkError (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/src.ts/json-rpc-provider.ts:78:20)
    at Logger.throwError (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/logger/src.ts/index.ts:273:20)
    at Logger.makeError (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/logger/src.ts/index.ts:261:28) {
  reason: 'Error: Transaction reverted without a reason string',
  code: 'UNPREDICTABLE_GAS_LIMIT',
  method: 'estimateGas',
  transaction: {
    from: 'アドレス',
    to: 'アドレス',
    data: 'データ',
    accessList: null
  },
  error: ProviderError: Error: Transaction reverted without a reason string
      at HttpProvider.request (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/http.ts:78:19)
      at AutomaticSenderProvider.request (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/accounts.ts:351:34)
      at AutomaticGasProvider.request (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/gas-providers.ts:136:34)
      at AutomaticGasPriceProvider.request (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/gas-providers.ts:153:36)
      at BackwardsCompatibilityProviderAdapter.send (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/hardhat/src/internal/core/providers/backwards-compatibility.ts:36:27)
      at EthersProviderWrapper.send (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@nomiclabs/hardhat-ethers/src/internal/ethers-provider-wrapper.ts:13:48)
      at EthersProviderWrapper.<anonymous> (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/src.ts/json-rpc-provider.ts:601:31)
      at step (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:48:23)
      at Object.next (/home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:29:53)
      at /home/tsuchida/workspace/hardhat-cryptzombie/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:23:71
}

対策抜粋

const config: HardhatUserConfig = {
  solidity: "0.8.9",
  networks: {
    localhost: { allowUnlimitedContractSize: true },
    hardhat: { allowUnlimitedContractSize: true },
  },
};

参考

github.com

おわり

これでひと通りHardhatに触れることが出来ました。

Truffle、Ganacheも簡単でしたがHardhatも同じくらい簡単ですし、プラグインも充実していてTypeScriptの開発が捗りそうで良さそうです。動作も軽くていい感じなので、今後はHardhatでやると思います。

www.tecotec.co.jp