Hardhatによるスマートコントラクト開発環境:ローカルネット構築からテスト、デプロイまで
Hardhatとは何か? なぜスマートコントラクト開発に必要か
スマートコントラクト開発は、通常のWebアプリケーション開発とは異なる特有のツールやプロセスを必要とします。特に、コンパイル、デプロイ、テストといった作業は、ブロックチェーン上で行うため、ローカル環境で効率的に反復できる仕組みが不可欠です。
Hardhatは、このようなスマートコントラクト(主にEthereumおよびEVM互換チェーン向け)開発のための包括的なフレームワークです。開発者がローカルでスマートコントラクトを記述し、コンパイルし、テストし、そして実際のブロックチェーンネットワークにデプロイするための一連のツールを提供します。Node.jsベースであり、JavaScriptやTypeScriptでスクリプトやテストを記述できるため、多くのWebエンジニアにとって学習コストが低いという利点があります。
Hardhatを使用することで、以下のような作業を効率的に行うことができます。
- 高速なローカル開発: メインネットやテストネットに接続することなく、メモリ上で動作するブロックチェーンネットワークを立ち上げ、即座にコントラクトのデプロイやテストを実行できます。
- 柔軟なテスト環境: JavaScript/TypeScriptで記述されたテストコードにより、複雑なシナリオや境界条件のテストを自動化できます。
- デバッグの容易さ: トランザクションの実行トレースやエラーログを詳細に確認できます。
- プラグインによる拡張性: コントラクトの検証、コードカバレッジの計測、Gasコスト分析など、様々な機能を追加できます。
この記事では、Hardhatを使った基本的なスマートコントラクト開発環境の構築方法から、ローカル開発ネットワークの利用、コントラクトのコンパイル、テスト、そしてデプロイまでの流れを追って解説します。
Hardhat開発環境のセットアップ
HardhatはNode.js上で動作するため、まずはNode.jsとnpm(またはyarn)がインストールされていることを確認してください。
新しいプロジェクトディレクトリを作成し、その中でnpmまたはyarnを使ってプロジェクトを初期化します。
mkdir my-smart-contract
cd my-smart-contract
npm init -y
次に、Hardhatをプロジェクトにインストールします。
npm install --save-dev hardhat
インストールが完了したら、以下のコマンドでHardhatプロジェクトの初期設定を行います。
npx hardhat
コマンドを実行すると、プロジェクトの種類を選択するよう求められます。「Create an empty hardhat.config.js」以外のオプション(TypeScriptプロジェクトやJavaScriptプロジェクトの作成)を選択すると、サンプルコントラクト、スクリプト、テストなどが自動的に生成され、すぐに開発を始められるようになります。今回は、TypeScriptプロジェクトを選択した場合を想定して説明を進めます。
選択すると、hardhat.config.ts
という設定ファイルや、contracts/
, scripts/
, test/
といったディレクトリが作成されます。
Hardhat Network: ローカル開発ネットワーク
Hardhatの最も便利な機能の一つが、Hardhat Networkと呼ばれる組み込みのブロックチェーンネットワークです。これは、開発やテストのためだけにメモリ上で動作するインスタンスで、実際のイーサリアムネットワークとほぼ同じ振る舞いをします。
Hardhat Networkを使う利点は以下の通りです。
- 高速: トランザクションの承認やブロックの生成が瞬時に行われます。
- 無料: 実際のGasコストがかかりません。
- 隔離性: 他の開発者の作業や実際のネットワークの状態に影響されません。
- デバッグ機能: トランザクション実行の詳細なトレース情報を取得できます。
Hardhatプロジェクトを初期化すると、特に設定をしなくてもこのHardhat Networkがデフォルトで使用されるようになります。ローカルでのコントラクトのデプロイやテストは、基本的にこのネットワークに対して行います。
シンプルなスマートコントラクトの作成
contracts/
ディレクトリに、以下のような簡単なスマートコントラクト(Solidityファイル)を作成してみましょう。ファイル名は Greeter.sol
とします。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// Greeterコントラクト
contract Greeter {
string private greeting;
// コンストラクタ:コントラクトデプロイ時に一度だけ実行される
constructor(string memory _greeting) {
greeting = _greeting;
}
// グリーティングメッセージを設定する関数
function setGreeting(string memory _greeting) public {
greeting = _greeting;
}
// 現在のグリーティングメッセージを取得する関数
function greet() public view returns (string memory) {
return greeting;
}
}
このコントラクトは、一つの文字列変数 greeting
を持ち、コンストラクタで初期値を設定し、setGreeting
関数で値を変更し、greet
関数で現在の値を取得するシンプルなものです。
Solidityファイルの先頭にある SPDX-License-Identifier
や pragma solidity
は、それぞれライセンス識別子とSolidityコンパイラのバージョン指定です。
スマートコントラクトのコンパイル
Solidityで記述されたスマートコントラクトは、ブロックチェーン上で実行できる形式(バイトコード)にコンパイルされる必要があります。Hardhatを使えば、以下のコマンド一つでプロジェクト内の全てのSolidityファイルをコンパイルできます。
npx hardhat compile
このコマンドを実行すると、artifacts/
および cache/
ディレクトリが生成されます。
artifacts/
: コンパイルされたコントラクトの成果物(ABIやバイトコードなど)がJSON形式で保存されます。ABI(Application Binary Interface)は、外部からコントラクトの関数を呼び出すために必要なインターフェース情報です。バイトコードは、EVM(イーサリアム仮想マシン)が解釈・実行できる機械語のようなものです。cache/
: コンパイルのキャッシュが保存され、次回のコンパイル時間を短縮します。
これらの成果物は、コントラクトをデプロイしたり、テストコードやフロントエンドからコントラクトを操作したりする際に使用されます。
コントラクトのデプロイ(ローカル開発ネットワーク)
コンパイルしたコントラクトを、Hardhat Network上にデプロイしてみましょう。デプロイには、JavaScriptまたはTypeScriptで記述されたデプロイスクリプトを使用するのが一般的です。
scripts/
ディレクトリに、deploy.ts
のようなファイル名で以下の内容を作成します(TypeScriptプロジェクトの場合)。
import { ethers } from "hardhat";
async function main() {
// GreeterコントラクトのFactoryを取得
// "Greeter"はSolidityファイル内のcontract名と一致させる
const Greeter = await ethers.getContractFactory("Greeter");
// コンストラクタ引数 ("Hello, Hardhat!") を指定してデプロイ実行
const greeter = await Greeter.deploy("Hello, Hardhat!");
// デプロイ完了まで待機
await greeter.deployed();
// デプロイされたコントラクトのアドレスを表示
console.log("Greeter deployed to:", greeter.address);
}
// main関数を実行
main()
.then(() => process.exit(0)) // 成功時は終了コード0
.catch((error) => {
// エラー発生時はエラーを表示し、終了コード1
console.error(error);
process.exit(1);
});
このスクリプトは、Hardhatが提供する ethers
ライブラリのヘルパー関数 ethers.getContractFactory
を使って、コンパイル済みの Greeter
コントラクトを取得します。これは、スマートコントラクトの「工場」のようなもので、ここから特定の引数(ここでは初期メッセージ "Hello, Hardhat!")を指定して deploy()
を呼び出すことで、ネットワーク上に新しいインスタンスを作成(デプロイ)します。
スクリプトを実行するには、以下のコマンドを使用します。
npx hardhat run scripts/deploy.ts
コマンドを実行すると、Hardhat Networkがバックグラウンドで立ち上がり、スクリプトが実行され、デプロイされたコントラクトのアドレスが表示されます。これで、ローカルのHardhat Network上に Greeter
コントラクトが配置されました。
コントラクトのテスト
スマートコントラクトは一度デプロイされると変更が難しいため、開発段階での厳密なテストが非常に重要です。Hardhatは、MochaというテストフレームワークとChaiというアサーションライブラリを使ったテスト記述をサポートしています。
test/
ディレクトリに、Greeter.ts
のようなファイル名でテストコードを作成します。
import { expect } from "chai";
import { ethers } from "hardhat";
describe("Greeter", function () {
// テスト実行前に一度だけ実行されるフック
// GreeterコントラクトのFactoryとデプロイ済みインスタンスを格納する変数
let Greeter: any; // ここでは簡単のためanyを使用
let greeter: any; // ここでは簡単のためanyを使用
before(async function () {
// テスト開始前にコントラクトFactoryを取得
Greeter = await ethers.getContractFactory("Greeter");
});
beforeEach(async function () {
// 各テストケースの実行前に新しいインスタンスをデプロイ
// これにより、各テストは独立した環境で実行される
greeter = await Greeter.deploy("Hello, world!");
await greeter.deployed();
});
describe("Deployment", function () {
it("Should set the right greeting", async function () {
// デプロイ時に設定されたグリーティングが期待通りか検証
expect(await greeter.greet()).to.equal("Hello, world!");
});
it("Should change the greeting when setGreeting is called", async function () {
// setGreeting関数を呼び出し、グリーティングが変更されたか検証
const newGreeting = "Hola, mundo!";
const setGreetingTx = await greeter.setGreeting(newGreeting);
// トランザクションが完了するのを待つ
await setGreetingTx.wait();
// 新しいグリーティングが期待通りか検証
expect(await greeter.greet()).to.equal(newGreeting);
});
});
// 必要に応じて他のdescribeブロックやitブロックを追加
});
このテストコードでは、beforeEach
フックを使って、各テストケースが実行される前に新しい Greeter
コントラクトを「Hello, world!」という初期値でデプロイしています。これにより、各テストはクリーンな状態で開始できます。
describe
ブロックはテストケースのグループ化に使い、it
ブロックが具体的なテストケースを表します。it
ブロックの中では、デプロイされた greeter
オブジェクトを使ってコントラクトの関数(greet()
, setGreeting()
)を呼び出し、Chaiのアサーション(expect(...).to.equal(...)
)を使って結果が期待通りか検証しています。
テストを実行するには、以下のコマンドを使用します。
npx hardhat test
Hardhat Network上でテストコードが実行され、各テストケースの成否が表示されます。
実ネットワークへのデプロイ(概念)
ローカル開発ネットワークでの開発とテストが完了したら、次はテストネットやメインネットなどの実際のブロックチェーンネットワークにコントラクトをデプロイすることになります。
Hardhatの設定ファイル (hardhat.config.ts
) に、デプロイしたいネットワークの設定を追加します。これには通常、そのネットワークへのRPCエンドポイント(InfuraやAlchemyなどのプロバイダーから取得)と、デプロイに使用するアカウントの秘密鍵が必要になります。秘密鍵は直接ファイルに書き込まず、環境変数などを使って安全に管理することが非常に重要です。
設定例 (hardhat.config.ts
):
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox"; // 便利なプラグイン群
const config: HardhatUserConfig = {
solidity: "0.8.18", // 使用するSolidityのバージョン
networks: {
// デフォルトのhardhat networkは設定不要
sepolia: { // 例えばSepoliaテストネット
url: process.env.SEPOLIA_RPC_URL || "", // RPCエンドポイントを環境変数から取得
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], // 秘密鍵を環境変数から取得
},
// 任意のネットワーク設定を追加...
},
// その他設定...
};
export default config;
上記のようにネットワーク設定を追加した後、デプロイスクリプトを実行する際に、--network <ネットワーク名>
オプションを指定することで、そのネットワークに対してデプロイできます。
npx hardhat run scripts/deploy.ts --network sepolia
これにより、指定したネットワークにコントラクトがデプロイされ、すべての人からアクセス可能になります。
まとめと次のステップ
この記事では、スマートコントラクト開発のための強力なフレームワークであるHardhatの基本的な使い方を解説しました。具体的には、環境セットアップ、ローカル開発ネットワークの利用、Solidityコントラクトのコンパイル、TypeScriptを使ったデプロイスクリプトとテストコードの記述、そして実ネットワークへのデプロイの概念について学びました。
Hardhatを使うことで、スマートコントラクトの開発、テスト、デプロイのワークフローを効率化し、より信頼性の高いコントラクト開発が可能になります。
次の学習ステップとしては、以下のようなトピックに進むことをお勧めします。
- より複雑なスマートコントラクト: ERC-20トークンやNFT (ERC-721/ERC-1155) といった標準規格に準拠したコントラクトの開発。
- コントラクトのアップグレード: デプロイ済みのコントラクトを安全に変更・更新する技術(Proxyパターンなど)。
- セキュリティ: スマートコントラクトの一般的な脆弱性と、それを防ぐためのコーディングパターンやツール(Slitherなど)。
- Gasコストの最適化: コントラクトのGas消費量を削減するためのテクニック。
- フロントエンド連携: Web3.jsやEthers.jsを使って、Webブラウザからデプロイ済みコントラクトと連携する仕組み。
Hardhatには、これらを助ける多くのプラグインが存在します。ぜひ Hardhat の公式ドキュメントも参照しながら、実際の開発を通じて理解を深めてください。