Ethereum のスマートコントラクト開発について調べてみたところ、Hardhat を使って開発するのが楽そうだったのでメモしておく。
Hardhat はスマートコントラクト開発に必要なものが揃った開発環境であり、プロジェクトテンプレートの生成、コントラクトのコンパイルやシミュレーター上でのテスト、デプロイのいずれをも hardhat
コマンドを通して行うことができる。さらに、TypeScript をサポートしており、TypeScript 版のテンプレートを生成できる上に、TypeChain を使って、コントラクトに対応する型定義ファイルを出力して開発に利用できる。
プロジェクトの作成
npm init
した新しいディレクトリで、npm install --save-dev hardhat
として hardhat をインストールする。その後、npx hardhat
とすると、初期設定のためにいくつか項目を質問される。
👷 Welcome to Hardhat v2.9.1 👷
? What do you want to do? …
Create a basic sample project
Create an advanced sample project
❯ Create an advanced sample project that uses TypeScript
Create an empty hardhat.config.js
Quit
TypeScript を使いたいので、Create an advanced sample project that uses TypeScript
を選ぶ。advanced といっても、そこまで複雑なテンプレートが生成されるわけではないので、TypeScript を使いたくない場合でも、Create an advanced sample project
を選んでしまって問題ないと思う。
続けて
? Hardhat project root: › /path/to/hardhat_example
? Do you want to add a .gitignore? (Y/n) › y
との質問によしなに答える。普通はデフォルトのまま Enter 連打で構わないだろう。最後に
? Do you want to install this sample project's dependencies with npm (hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-etherscan dotenv eslint eslint-config-prettier eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise hardhat-gas-reporter prettier prettier-plugin-solidity solhint solidity-coverage @typechain/ethers-v5 @typechain/hardhat @typescript-eslint/eslint-plugin @typescript-eslint/parser @types/chai @types/node @types/mocha ts-node typechain typescript)? (Y/n) · y
と、必要なライブラリのインストールを求められるので y と答えインストールすると、環境構築は終了だ。
コントラクトの開発とテスト
作成したコントラクトを contracts/
以下に配置する。サンプルとして Greeter.sol
が最初から置いてあるのでこれを流用する。
$ npx hardhat compile
とするとSolidity のコンパイラがダウンロード(初回のみ)され、コンパイルされる。コンパイルに成功すると、バイトコードやABIが含まれた artifacts/Greeter.sol/Greeter.json
が生成される。
テストを行うには
$ npx hardhat test
とする。このコマンドは test
ディレクトリ以下の全てのファイルを Mocha のテストとして実行する。Hardhat と一緒に Mocha がインストールされているため、改めてインストールする必要はないので嬉しい。こちらにもサンプルとして Greeter.sol
をテストするための index.ts
が配置されている。
$ npx hardhat test
No need to generate any newer typings.
Greeter
Deploying a Greeter with greeting: Hello, world!
Changing greeting from 'Hello, world!' to 'Hola, mundo!'
✔ Should return the new greeting once it's changed (710ms)
1 passing (712ms)
と、サンプルのテストが実行される。
簡単にテストコードを見てみよう。
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, world!");
await greeter.deployed();
ブロックチェーンとのインタラクションは ethers ライブラリで行う。getContractFactory()
にコントラクト名を渡すと CotractFactory
を得ることができ、これの deploy()
メソッドを呼ぶことでコントラクトをデプロイすることができる。ContractFactory
はデプロイ前のコントラクトコードそのものを表しており、deploy()
で得られたオブジェクト greeter
がネットワークにデプロイされたコントラクトに対応している。
コントラクトは、マイナーによってブロックに取り込まれることでそのデプロイが完了し、ブロックチェーン上で認識される。デプロイの完了を確かなものにするためには、greeter.deployed()
を待つ。
あとは、greeter
オブジェクトを使ってコントラクトを操作してテストを行う。Greeter コントラクトは、greet()
を呼ぶと、デプロイ時に設定されたか、またはその後 setGreeting()
で設定された挨拶文を返すという単純なものだ。以下でその通りの挙動をするか確認している。
expect(await greeter.greet()).to.equal("Hello, world!");
const setGreetingTx = await greeter.setGreeting('Hola, mundo!');
// wait until the transaction is mined
await setGreetingTx.wait();
expect(await greeter.greet()).to.equal("Hola, mundo!");
データの取得のみで、チェーンの状態を変えない(view
または pure
な)関数を呼ぶためには、単純に対応したメソッドを呼ぶ。
const greet: string = await greeter.greet();
上の例では、greet
にはコントラクトが返す文字列が直接代入される。
一方、チェーンの状態を変える(=ネットワークに対してトランザクションを発行する)関数を呼ぶ場合、トランザクションを表すオブジェクトが返される。このオブジェクトの wait()
を待つことで、トランザクションの完了を待機できる。
const setGreetingTx: ContractTransaction = await greeter.setGreeting('Hello');
await setGreetingTx.wait()
テストは EVM のシミュレーター上で実行される。その上、実行時の自分自身のアドレスもテストの実行時に hardhat が準備してくれるため、何も自分で準備することなく気軽にテストを実行できる [2]。
コントラクトに対する TypeScript サポート
ところで、Greeter コントラクトは、デプロイ時に string
の引数を取っていた。
constructor(string memory _greeting) {
console.log("Deploying a Greeter with greeting:", _greeting);
greeting = _greeting;
}
驚くべきことに、Greeter.deploy()
の型は Greeter__factory.deploy(_greeting: string, overrides?: (略): Promise<Greeter>
と、_greeting: string
を取るようになっており、コントラクトに対応している。また、greeter
オブジェクトにもコントラクトの各関数に対応するメソッドが生えている。これは、コントラクトのコンパイル時に TypeChain によって対応する型定義が生成されるためだ。このため、エディタによる補完もなされ、快適にコントラクトとのインタラクションを書くことができる。
コントラクトのデプロイ
テストが完了したところで、コントラクトを実際にブロックチェーンネットワークにデプロイする。これは実際にネットワークと通信する必要があるため、ネットワークの情報や、自分自身のアドレスについて事前に設定する必要がある。(デプロイしようとするネットワーク上で、自分のアドレスが十分なガス代を持っている必要がある。これについては本項では説明しない。)
hardhat.config.ts
に設定を記述する。サンプルとして ropsten テストネットの設定が最初から記述されているので、自分が使いたいチェーンの情報を追加する。例えば Polygon Mumbai テストネット[3]を利用したいなら
const config: HardhatUserConfig = {
//...
networks: {
ropsten: {
url: process.env.ROPSTEN_URL || "",
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
mumbai: {
url: process.env.MUMBAI_URL || "https://rpc-mumbai.maticvigil.com",
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
},
//...
}
といった形になる。ここでキー(mumbai
)は何でも良いが、ここで書いたキーを後でネットワークの指定に使う。具体的なパラメータは .env
ファイルで設定する [4]。.env.sample
を .env
にリネームし、各項目を設定する。
MUMBAI_URL=https://rpc-mumbai.maticvigil.com
PRIVATE_KEY=<自分の秘密鍵>
設定後、
$ npx hardhat run scripts/deploy.ts --network mumbai # hardhat.config.ts で設定したネットワーク名を指定する
とすると、scripts/deploy.ts
を、mumbai ネットワークに対して実行する。このスクリプトを見ると、
async function main() {
const Greeter = await ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, Hardhat!");
await greeter.deployed();
console.log("Greeter deployed to:", greeter.address);
}
となっており、テストの時と同様に、ethers.getContractFactory()
でコントラクトを読み込み、contractFactory.deploy()
でデプロイしている。もちろん、必要であれば以下のようなコードを追加して、コントラクトの機能を呼び出し簡単なテストを行うこともできる。
const contract = await greeter.deployed();
assert(contract.greet(), "Hello, Hardhat!");
テストの時同様、秘密鍵やネットワークの設定は Hardhat によって自動で行われているため、このスクリプト内ではデプロイに集中することができる。また、network に渡す値を変えることで、複数のネットワークにデプロイすることも簡単だ。
まとめ
本記事では、Ethererum スマートコントラクトの開発環境を Hardhat を使って構築する方法と、この環境下でコントラクトのテストとデプロイを行う方法を紹介した。Hardhat はコントラクト開発時に必要なことの多くをカバーしてくれており、面倒な準備無しにコントラクトの開発を始めることができてとても生産性が高いと感じる。さらに TypeChain を使ってコントラクトに対応する型情報を生成してくれるため、TypeScript との親和性が高い点も良い。本記事が、今からスマートコントラクトを開発してみようと考えている人の助けになれば嬉しい。
実際にコントラクトを開発する中で気づいた点を続く記事にまとめたので、気になる方はそちらも参考にしてほしい。
分かりやすさのために変数の型を明示したが、推論されるため書く必要はない。 ↩︎
テストコードにおいて、ethers モジュールを、そのもの直接ではなく
import { ethers } from "hardhat"
として Hardhat からインポートしていることに注意してほしい。ここで export されている ethers は、Hardhat によって様々なお膳立てがされている。 ↩︎Ethererum じゃなくて Polygon じゃん!という突っ込みは無しで...。 ↩︎
見て分かるとおり、環境変数から値が取得されるので、実行時に環境変数として渡しても良い。今回の場合、インポートされる dotenv モジュールのおかげで、
.env
ファイルに設定した値もprocess.env
から取得できるようになっている。 ↩︎