【作って学ぶ】hardhat + ethers.js で簡易なカウンターdAppsを作る【Ethereum】

2022/01/25に公開

概要

簡易なカウンターdAppsを作りながら、dAppsについて学びます。
今回はEthereum基盤のdAppsを作ります。

これから作るものを ざっくりと 説明すると、ブロックチェーン上で動作する小さなミニアプリを作り、それと対話するWebサーバーを作ります。
ブロックチェーン上でWebサーバーが動いているわけではありません。

さっそく作っていきます

事前準備

環境

  • node 8.3.0
  • macOS 12.0.1

プロジェクト構成

  • packages フォルダを作成し、その中に contracts frontend フォルダを作ります。
  • contracts フォルダでは hardhatを用いてスマートコントラクト開発環境を整備します
  • frontend フォルダでは Webページ開発環境を整備します (今回はReactやVue等フロントエンドライブラリやフレームワークは一切使いません)
.
├── packages
│   ├── contracts
│   └── frontend

このような構成を monorepo と呼びます

https://engineering.mercari.com/blog/entry/20210817-8f561697cc/

実装

コントラクト

hardhatを用いてスマートコントラクト開発環境を構築します

/packages/contracts フォルダ内で下記コマンドを入力

npm init -y
npm install --save-dev hardhat @nomiclabs/hardhat-ethers @nomiclabs/hardhat-waffle
npx hardhat

# このあとに出てくる選択肢は以下を選択
# > Create an empty hardhat.config.js

configファイルを編集します。

hardhat.config.js

// /packages/contracts/hardhat.config.js
// 以下を追加
require("@nomiclabs/hardhat-waffle");

module.exports = {
  // solidityのバージョンを指定
  solidity: "0.8.0",
};

package.json

// /packages/contracts/package.json
{
  // ... 以下を追加
  
  "scripts": {
    "build": "hardhat compile",
    "dev": "hardhat node",
    "deploy": "hardhat run scripts/deploy.js --network localhost"
  },
  
  // ...
}

contracts フォルダを作り、 Counter.sol ファイルを作成します。

// /packages/contracts/contracts/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
    uint256 public count;
    event UpdatedCount(uint256 count);

    function get() public view returns (uint256) {
        return count;
    }

    function inc() public {
        count += 1;
        emit UpdatedCount(count);
    }

    function dec() public {
        count -= 1;
        emit UpdatedCount(count);
    }
}

ローカルチェーンにデプロイする用のスクリプトを作ります。
(追加で設定を加えればテストネットワーク、メインネットワークにもデプロイできます。)

scripts フォルダを作成し、 deploy.js ファイルを作成します。

// /packages/contracts/scripts/deploy.js
const hre = require("hardhat");

async function main() {
  const counterFactory = await hre.ethers.getContractFactory("Counter");
  const counter = await counterFactory.deploy();

  await counter.deployed();

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

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

実装とデプロイ用のスクリプトが作れたので、一度コンパイルして問題ないか確認しておきます。

# /packages/contracts
yarn build # コンパイルして問題ないかチェック

> yarn run v1.22.17
> $ hardhat compile
> Compiling 1 file with 0.8.0
> Solidity compilation finished successfully
> ✨  Done in 1.18s.

今回のコントラクトでは以下の機能を実装しました

  • 現在のカウントを保持 count
  • カウントを+1 inc()
  • カウントを-1 dec()
  • カウントが変更されるたびに発生するイベント UpdatedCount

これらをWeb側で表示できるように実装します。

フロントエンド

/packages/frontend にてコマンドを実行

# /packages/frontend
npm init -y
npm install --save-dev serve # 手軽にWebサーバーを建てられるライブラリ

configファイルを修正します package.json

// /packages/frontend/package.json
{
  // ... 以下を修正
  
  "scripts": {
    "dev": "serve"
  },
  
  // ...
}

画面表示用の index.html ファイルを作成します

// /packages/frontend/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Counter</title>
    <script
      src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"
      type="application/javascript"
    ></script>
  </head>
  <body>
    <h1>counter dapp</h1>
    <p id="account_address"></p>
    <p id="count_text"></p>
    <button id="connect">connect</button>
    <button id="inc">increment</button>
    <button id="dec">decrement</button>
    <script src="script.js"></script>
  </body>
</html>

index.html の内部で動くスクリプトを作ります script.js ファイルを作成

// /packages/frontend/script.js
(async () => {
  const abi = [
    {
      anonymous: false,
      inputs: [
        {
          indexed: false,
          internalType: "uint256",
          name: "count",
          type: "uint256",
        },
      ],
      name: "UpdatedCount",
      type: "event",
    },
    {
      inputs: [],
      name: "count",
      outputs: [
        {
          internalType: "uint256",
          name: "",
          type: "uint256",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [],
      name: "dec",
      outputs: [],
      stateMutability: "nonpayable",
      type: "function",
    },
    {
      inputs: [],
      name: "get",
      outputs: [
        {
          internalType: "uint256",
          name: "",
          type: "uint256",
        },
      ],
      stateMutability: "view",
      type: "function",
    },
    {
      inputs: [],
      name: "inc",
      outputs: [],
      stateMutability: "nonpayable",
      type: "function",
    },
  ];
  // ↑↑ /packages/contracts/artifacts/contracts/Conter.sol/Counter.json内からコピペしています
  
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const address = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
  const signer = provider.getSigner(0);
  const contract = new ethers.Contract(address, abi, signer);

  contract.on("UpdatedCount", (from, to, amount, event) => {
    // コントラクト上の emit UpdatedCount(count); が呼ばれるたびにここが自動的に更新される(ページのリロード不要)
    document.getElementById("count_text").textContent = `count: ${from}`;
  });

  // dAppsでよくあるWallet Connectのボタン
  document.getElementById("connect").addEventListener("click", async () => {
    const accounts = await window.ethereum.request({
      method: "eth_requestAccounts",
    });
    document.getElementById(
      "account_address"
    ).textContent = `address: ${accounts[0]}`;
  });

  document.getElementById("inc").addEventListener("click", async () => {
    // コントラクトで定義されている inc メソッドを呼び出す
    await contract.inc();
  });

  document.getElementById("dec").addEventListener("click", async () => {
    await contract.dec();
  });
})();

Webページ側の作成はこれでOKです。確認のためサーバーを立ててみます。

yarn dev

> yarn run v1.22.17
> $ serve

   ┌─────────────────────────────────────────────────────┐
   │                                                     │
   │   Serving!                                          │
   │                                                     │
   │   - Local:            http://localhost:3000         │
   │   - On Your Network:  http://XXX.XXX.XXX.XXX:3000   │
   │                                                     │
   │   Copied local address to clipboard!                │
   │                                                     │
   └─────────────────────────────────────────────────────┘

↑のような表示になれば成功です。

ルートディレクトリ

いまのままだとコントラクトをビルドしたりテストしたり、Webサーバーを立ち上げるために毎回 packages/contractspackages/frontend ディレクトリでコマンドを入力しなくてはいけません。
これでは面倒なので、ルートディレクトリにてまとめて実行できるように整備します。

プロジェクトのルートディレクトリにて下記のコマンドを実行

# /
npm init -y
npm install --save-dev npm-run-all wait-on

package.json を開き設定を変更します

// package.json
{
  // ...以下を追加
  "private": true,
  "scripts": {
    "dev": "run-p dev:*",
    "dev:run-node": "yarn workspace contracts dev",
    "dev:deploy-node": "wait-on http://localhost:8545 && yarn workspace contracts deploy",
    "dev:frontend": "wait-on http://localhost:8545 && yarn workspace frontend dev"
  },
  "workspaces": [
    "packages/**"
  ],
  // ...
}

実行

そのままプロジェクトのルートディレクトリにて下記コマンドを実行

yarn dev

http://localhost:3000/ にアクセス

↑このような画面が表示されればOK

メタマスクの設定

サーバー起動ログと同時にローカルネットワークで使えるウォレットとその秘密鍵がログに流れるのでコピーし、メタマスク側でアカウントのインポートを行っておきましょう

// ...

Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

// ...

メタマスク側の接続先ネットワークを localhost 8545 に設定します。

ウォレットの接続

connect ボタンを押すと接続ダイアログがでるので接続する

カウンターの操作

increment ボタンや decrement ボタンを押すとトランザクションの確認ダイアログが出るので確認を押す。

少し待つと、トランザクション成功アラートと共に画面が自動的に変わるのが確認できます


今回作成したプロジェクトを公開しました。
うまくいかない等あったらソースコードを参考にしてみてください。直接DM頂いてもOKです!

https://github.com/ryo-takahashi/example-counter-dapps

トラブルシューティング

Nonce too high. エラー

MetaMask - RPC Error: [ethjs-query] while formatting outputs from RPC '{"value":{"code":-32603,"data":{"code":-32000,"message":"Nonce too high. Expected nonce to be 1 but got 2. Note that transactions can't be queued when automining."}}}' 

↑ incrementやdecrementのトランザクションの確認ボタンを押したのにトランザクションが失敗する!

Metamaskの設定からアカウントのリセットを行い、トランザクションの履歴をクリアしてください

まとめ

  • dAppsの全体像を掴むために簡易なカウンターアプリをhardhatを用いて作った
  • 表示側にはethers.jsを用いて簡易なHTMLファイルとJSファイルを作った

参考

https://hardhat.org/getting-started/#installation

https://docs.ethers.io/v5/getting-started/#getting-started--connecting

https://zenn.dev/zzz/articles/368367ec20aa42

https://solidity-by-example.org/events/

https://qiita.com/ksuhara/items/55296e5098bc27061d13

https://solidity-by-example.org/first-app/

etc

Solidityについてワイワイ学ぶコミュニティ「solidity-jp」を作りました!
いまから学んでみたい/学習中だけどの日本語の情報が少ない/古くて時間がかかっているという方、一緒に学びましょう〜!!

https://solidity-jp.dev/

また、TwitterにてSolidityに関する技術情報を発信しています。良ければフォローお願いします!

https://twitter.com/k0uhashi

Discussion