スマートコントラクトの開発環境 Hardhatに触れる
はじめに
ここ1ヶ月CryptoZombiesでSolidityの基礎を学んできました。
今回は、スマートコントラクトの開発環境であるHardhatに触れていきます。スマートコントラクトの開発環境はHardhat以外にもTruffleやFoundryがありますがチュートリアルがしっかりしてそうなのでHardhatを使っていきます。
Hardhatの概要
まずHardhatは、一言でいうとスマートコントラクトを効率的に開発・テスト・デプロイできるイーサリアム向けの開発環境です。
主に4つのツールから構成されています。
- Hardhat Runner: Hardhatの中心的な役割を果たすコンポーネントで、コンパイル、デプロイ、さまざまなタスクの管理などを行います。
- Hardhat Network: 開発用に設計されたローカルのイーサリアムネットワークです。これにより、コントラクトのデプロイ、テストの実行、コードのデバッグなどをローカル環境で簡単に行うことができます。
- Visual Studio Code拡張機能: VS Code用の拡張機能で、開発効率を大幅に向上させます。この拡張機能は、コード補完、ナビゲーション、名前の変更、ドキュメントのフォーマット、ホバー機能、インラインコード検証、コードアクションなどの機能を提供します。
- Hardhat Ignition: スマートコントラクトのデプロイスクリプトを簡単に作成・管理できる新しいツールです。従来のHardhatでは、scripts/deploy.ts のような形で独自にスクリプトを書く必要がありましたが、Ignitionを使うことでシンプルで再利用可能なデプロイフローを構築できます。
チュートリアル
Hardhatには初心者向けチュートリアルがあるのでそちらを元に触っていきます。
環境設定
Node.jsのインストール
Voltaを使ってNode.jsをインストールします。チュートリアルではnvmを使っていましたが、最近のNode.jsのバージョン管理は早いのとバージョン変更が楽なのでVolta一択な気がします。
# Voltaインストール
curl https://get.volta.sh | bash
# Nodeインストール(バージョンを指定しない場合LTS:Long Term Supportがインストールされる)
volta install node
# v22.14.0(2025/03/24時点)
node -v
HardhatはNodeバージョンのサポート終了から 2 か月後にサポートを終了しこの期間が経過すると、テストは停止され、使用しようとすると警告が表示される(最初Node v23でやろうとしましたがそれでも警告出ました)ので、最新のLTSを基本使っておけば良いのかなと思います。
Hardhatプロジェクトの作成
npmパッケージを利用しHardhatプロジェクトを作成していきます。
Hardhat、 Hardhatツールボックスライブラリのインストールまで
# ディレクトリ作成
mkdir hardhat-tutorial
# ディレクトリ移動
cd hardhat-tutorial
# package.jsonの生成(-yを入れることで全ての質問にデフォルト値をいれを入れて生成される)
npm init -y
# このプロジェクトで利用するNodeバージョンを固定
volta pin node
# hardhatライブラリインストール
npm install --save-dev hardhat
hardhatプロジェクトの作成
# hardhatプロジェクト作成
npx hardhat init
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
👷 Welcome to Hardhat v2.22.19 👷
✔ What do you want to do? · Create a TypeScript project
✔ Hardhat project root: · /Users/br-to/hardhat-tutorial
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)? (Y/n) · y
npm install --save-dev "@nomicfoundation/hardhat-toolbox@^5.0.0"
チュートリアルでは空のhardhat.config.js
を使っていましたが今回はTypeScriptを使いたいのでCreate a TypeScript project
を選択しています。ここで@nomicfoundation/hardhat-toolbox
というhardhat開発に必要なプラグインもインストールできます。
プロジェクトを生成するとこのように展開され、これでスマートコントラクトの開発環境が整いました。
スマートコントラクトの作成
Hardhatの拡張機能
まず最初にHardhatのVS Codeの拡張機能があるのでそちらをインストールします。
Solidityコードのコード補完、関数の定義元へのジャンプが可能なのでVS Codeで開発するなら入れない理由はないと思います。
詳しい機能は、
に記載しています。
Token.sol作成
それではスマートコントラクトを作成していきます。contracts
ディレクトリに元から入っているLock.sol
は削除しToken.sol
を作成します。(Hardhatのチュートリアルのコードからコメントを書き換えたものです。)
ちなみに*.sol
はSolidityファイルに使われる拡張子です。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Token {
// トークンの名前
string public name = "My Hardhat Token";
// トークンのシンボル
string public symbol = "MHT";
// トークンの固定供給量(uint256型の整数で保存)
uint256 public totalSupply = 1000000;
// イーサリアムアカウントを保存するためのアドレス型変数
address public owner;
// 各アカウントの残高を保持するマッピング
mapping(address => uint256) balances;
// イベントを定義 from(送信者アドレス) to(受信者アドレス) amount(送金額)
event Transfer(address indexed _from, address indexed _to, uint256 _value);
// デプロイ時に一度だけ実行
constructor() {
// totalSupplyをコントラクトのデプロイアカウントに割り当てます。
balances[msg.sender] = totalSupply;
owner = msg.sender;
}
// トークン送信用の関数
function transfer(address to, uint256 amount) external {
// 送信者が十分なトークンを持っているか確認します。
// NOTE: unicodeを入れないとInvalid character in string literalエラーが出る
require(balances[msg.sender] >= amount, unicode"トークンが不足しています");
// トークンの送信
balances[msg.sender] -= amount;
balances[to] += amount;
// イベントを発行 送金者、受取人、送金額がログに記録される
emit Transfer(msg.sender, to, amount);
}
// 指定したアカウントのトークン残高を取得する関数
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}
コードの詳細は、私の過去記事の内容見ていただければと思いますので説明しません。🙇🏽♀️
ファイルの先頭にあるSPDX-License-Identifier: UNLICENSED
はSPDXライセンス識別子と言われるものであり、ソースコードにどのライセンスが適用されているかを明示するための標準化された文字列です。指定しないとコンパイル時に警告が出るのでライセンスが必要ない場合でもUNLICENSED
を指定しましょう。
コードを公開して他の人にも使ってほしい場合はMIT
を指定することになります。
コンパイル
# コントラクトのコンパイル
npx hardhat compile
Generating typings for: 1 artifacts in dir: typechain-types for target: ethers-v6
Successfully generated 6 typings!
Compiled 1 Solidity file successfully (evm target: paris).
これでコントラクトがコンパイルできます。コンパイルされた結果はartifacts
ディレクトリに格納されます。
テスト
スマートコントラクトは一度デプロイすると変更できないため、テストは非常に重要なものです。先程作成したToken.solのテストのためにHardhat Toolboxに組み込まれているethers.js
(イーサリアムブロックチェーンとのやり取りを簡単にできるようになるJavaScriptライブラリ)とテストフレームワークのMocha
を利用します。
Hardhatを扱うまでMochaというテストフレームワークは聞いたことなかったですが、オールインワンでテストが可能なJestとは異なり、カスタマイズ性を重視しており、アサーションはchai
というアサーション専用のライブラリを組み合わせて使うことが多いみたいです。
ではテストコードを書いていきます。test
ディレクトリにあるLock.ts
を削除し新たにToken.ts
を作ります。
import { expect } from 'chai';
import { ethers } from 'hardhat';
describe('トークンコントラクト', () => {
it('デプロイ時に全供給量のトークンがオーナーに割り当てられるかどうか', async () => {
const [owner] = await ethers.getSigners();
const hardhatToken = await ethers.deployContract('Token');
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
});
テストコードをそれぞれ詳しくみていきます。
import { expect } from 'chai';
import { ethers } from 'hardhat';
アサーションライブラリであるchai
と、hardhat-ethers
というEthers.jsの機能をラップし、テストやデプロイを簡単に行えるようにするライブラリをimportします。
どちらもhardhat-tookbox
の中にあるものなので新たにインストールする必要はありません。
const [owner] = await ethers.getSigners();
Ethers.jsには、Signer
というイーサリアムアカウントを表すオブジェクトがあり、コントラクトにトランザクションを送信するために使用されます。getSigners
関数はhardhat-ethers
にあるローカルノードのテストアカウントを取得するためのラッパー関数になります。
ここでは、Hardhat Network(仮想のイーサリアムネットワーク)を立ち上げ、getSigners()
でこのネットワーク上で自動生成されるテスト用のアカウントを返します。
[
HardhatEthersSigner {
_gasLimit: 30000000,
address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
provider: HardhatEthersProvider {
_hardhatProvider: [LazyInitializationProviderAdapter],
_networkName: 'hardhat',
_blockListeners: [],
_transactionHashListeners: Map(0) {},
_eventListeners: []
}
},
// 同じように20個分テストアカウントが返ってくる
...
]
最初のアカウントが多くの場合、コントラクトのオーナーと扱われるので配列の最初を取得しています。
const hardhatToken = await ethers.deployContract("Token");
deployContract
もhardhat-ethers
ライブラリにあるスマートコントラクトをデプロイするためのラッパー関数です。第一引数にはデプロイしたいコントラクトの名前を入れることになるので"Token"が入ります。
デプロイすることによってコントラクトのオブジェクトが返却され、コントラクト内の関数を呼び出せるようになります。
const ownerBalance = await hardhatToken.balanceOf(owner.address);
Tokenコントラクトで定義したbalanceOf
関数を呼び出すことによって、コントラクトのオーナーのトークン残高を取得します。
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
chai
のアサーションを利用して、コントラクトが保持しているトークンの全供給量とオーナーのトークン残高が一致しているかを検証します。
最後にターミナルでテストを実施します。
npx hardhat test
Token contract
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 1000000
✔ Deployment should assign the total supply of tokens to the owner (291ms)
1 passing (293ms)
これでテストに成功です!👏
失敗した時はこのようになります。(deployContact
で指定するトークン名をToken2に変更します)
npx hardhat test
Token contract
1) Deployment should assign the total supply of tokens to the owner
0 passing (274ms)
1 failing
1) Token contract
Deployment should assign the total supply of tokens to the owner:
HardhatError: HH700: Artifact for contract "Token2" not found. Did you mean "Token"?
at Artifacts._handleWrongArtifactForContractName (/Users/toikobara/hardhat-tutorial/node_modules/hardhat/src/internal/artifacts.ts:721:11)
at Artifacts._getArtifactPathFromFiles (/Users/toikobara/hardhat-tutorial/node_modules/hardhat/src/internal/artifacts.ts:852:19)
at Artifacts._getArtifactPath (/Users/toikobara/hardhat-tutorial/node_modules/hardhat/src/internal/artifacts.ts:516:21)
at async Artifacts.readArtifact (/Users/toikobara/hardhat-tutorial/node_modules/hardhat/src/internal/artifacts.ts:71:26)
at async getContractFactory (/Users/toikobara/hardhat-tutorial/node_modules/@nomicfoundation/hardhat-ethers/src/internal/helpers.ts:120:22)
at async deployContract (/Users/toikobara/hardhat-tutorial/node_modules/@nomicfoundation/hardhat-ethers/src/internal/helpers.ts:397:19)
at async Context.<anonymous> (/Users/toikobara/hardhat-tutorial/test/Token.ts:8:26)
デバッグ
ローカル環境での開発時にはコントラクト内でログを取りデバッグすることが可能です。
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract Token {
// ...
// デプロイ時に一度だけ実行
constructor() {
// totalSupplyをコントラクトのデプロイアカウントに割り当てます。
console.log("Deploying Token with total supply: %s and owner: %s", totalSupply, msg.sender);
balances[msg.sender] = totalSupply;
owner = msg.sender;
}
}
やり方は簡単で、コントラクトの上部でhardhat/console.sol
をimportし、JavaScriptと同様にログを仕込みたいところにconsole.log
を入れるだけです。%sの箇所に引数を入れることも可能です。
※console.log
に追加できる引数は最大4つまでなのと、本番環境では使えず利用できるのはHardhat Network上であるのは注意してください。
これでテストを実行することによって、ログが出力されます。
npx hardhat test
Compiled 1 Solidity file successfully (evm target: paris).
Token contract
Deploying Token with total supply: 1000000 and owner: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
✔ Deployment should assign the total supply of tokens to the owner (308ms)
1 passing (308ms)
デプロイ
テストでコントラクトの動作も確認できたので次はデプロイです。
Hardhatはローカルのブロックチェーンネットワークを用意しているので、そのネットワークに対してコントラクトをデプロイします。(チュートリアルではSepoliaというテストネットワークにデプロイしていますが、説明が長くなりそうなので今回はlocalhostにしています)
まずローカルのブロックチェーンネットワークを起動します。
npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
Accounts
========
WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.
Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
...
これで起動しました。アクセスポイントはhttp://127.0.0.1:8545/
になり、起動時に20個のアカウントのアドレスと秘密鍵が出力されます。
※ 警告にも書いていますがこれらのアカウントを本番環境や別のブロックチェーンネットワークで使うのは御法度です。
次にこのローカルネットワークにデプロイしていきます。ここで、Hardhatの新しく出たツールであるHardhat Ignition
を利用していきます。
ignition/modulesディレクトリにあるLock.ts
を削除し、Token.ts
を作成します。
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules';
const TokenModule = buildModule('TokenModule', (m) => {
const token = m.contract('Token');
return { token };
});
export default TokenModule;
コードを詳しくみていきます。
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules';
buildModule
はHardhat Ignition
のモジュール作成用関数で、デプロイの設定とロジックをモジュール化するために使用します。この関数にはモジュールIDとコールバック関数が必要になります。
const TokenModule = buildModule('TokenModule', (m) => {
const token = m.contract('Token');
return { token };
})
TokenModule
がモジュールIDでコールバック関数に渡されるm
パラメータがmoduleBuilder
オブジェクトと呼ばれるものでコントラクトのデプロイ設定や、デプロイの順序制御ができます。
これでToken
コントラクトのデプロイを定義し、デプロイされたコントラクトを公開しています。
最後にターミナルでこちらのコマンドを実行します。
npx hardhat ignition deploy ./ignition/modules/Token.ts --network localhost
Hardhat Ignition 🚀
Deploying [ TokenModule ]
Batch #1
Executed TokenModule#Token
[ TokenModule ] successfully deployed 🚀
Deployed Addresses
TokenModule#Token - 0x5FbDB2315678afecb367f032d93F642f64180aa3
ignition/deployments/chain-31337
ディレクトリができていればデプロイ完了です。ここにデプロイに関する内容が全て含まれています。
まとめ
Hardhat
を利用することで簡単にスマートコントラクトをブロックチェーンにデプロイするところまでできました。console.log
でデバッグできるのはすごく便利だなと思いました。
次はチュートリアルの続きとして簡単なフロントエンドアプリを作成して、スマートコントラクトと繋げてみようと思います。
ここまで見ていただきありがとうございました!🙇🏽♀️
Discussion