【作って学ぶ】hardhat + ethers.js で簡易なカウンターdAppsを作る【Ethereum】
概要
簡易なカウンター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
と呼びます
実装
コントラクト
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/contracts
や packages/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です!
トラブルシューティング
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ファイルを作った
参考
etc
Solidityについてワイワイ学ぶコミュニティ「solidity-jp」を作りました!
いまから学んでみたい/学習中だけどの日本語の情報が少ない/古くて時間がかかっているという方、一緒に学びましょう〜!!
また、TwitterにてSolidityに関する技術情報を発信しています。良ければフォローお願いします!
Discussion