
【Hardhat】実際にSolidityに触ってみる編
※ 画像・動画等のコンテンツが上手く生成されないことがあります。その際はお手数ですが、ページの更新を複数回お願いします。
目次
目的
今回は、Solidityを初めて触ってみてブログに残しました。 "Hello, World!"という文字列を返す簡単なコントラクト作成からテスト、デプロイを行い、デプロイされたコントラクトのメソッドをJSON RPCで呼び出してみる、という一連の流れをやってみる。
前回の環境構築はこちら
不要なファイルを消す
./contracts, ./scripts, ./test の中身を消す。
Shell
$ rm scripts/* $ rm contracts/* $ rm test/*
HelloWorldコントラクトを作る
Shell
$ touch contracts/HelloWorld.sol
./contractにファイル作成。
まずライセンスとバージョンを宣言する
Solidity
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9;
コントラクトの宣言
コントラクト名はUpperCamelcaseで書く
Solidity
contract HelloWorld { // }
HelloWorldコントラクト内に、文字列“Hello World”を取得する関数を作る
関数はcamelcase。
external :外部から呼び出せる宣言 pure :この関数の中ではread,write等の操作をしていない宣言 string memory :stringは可変長であり、memoryとつける。固定長はuint 。
Solidity
contract HelloWorld { function getMessage() external pure returns (string memory) { return "Hello, World!"; } }
コンパイルする。
Shell
$ npx hardhat compile
abiを確認する
コンパイル後に、artifacts/contracts/HelloWorld.sol/HelloWorld.json を見てみる。
abiとはアプリケーションバイナリーインターフェースの略。 先ほど記述したHelloWorld.solコードの情報がjsonで収まっている。 デプロイした後に関数の呼び出しとかで参照するものとなる。
bytecode とかは、EVMが解釈するためのコード。
JSON
{ "_format": "hh-sol-artifact-1", "contractName": "HelloWorld", "sourceName": "contracts/HelloWorld.sol", "abi": [ { "inputs": [], "name": "getMessaage", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "pure", "type": "function" } ], "bytecode": "0x608060405234801561001057600080fd5b5061017c806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80637cf2f75a14610030575b600080fd5b61003861004e565b6040516100459190610124565b60405180910390f35b60606040518060400160405280600d81526020017f48656c6c6f2c20576f726c642100000000000000000000000000000000000000815250905090565b600081519050919050565b600082825260208201905092915050565b60005b838110156100c55780820151818401526020810190506100aa565b838111156100d4576000848401525b50505050565b6000601f19601f8301169050919050565b60006100f68261008b565b6101008185610096565b93506101108185602086016100a7565b610119816100da565b840191505092915050565b6000602082019050818103600083015261013e81846100eb565b90509291505056fea2646970667358221220ac75a2ce55b0fbe1a534ea7ab915b56f482204d22e1af824b361f7e31f387b5b64736f6c63430008090033", "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c80637cf2f75a14610030575b600080fd5b61003861004e565b6040516100459190610124565b60405180910390f35b60606040518060400160405280600d81526020017f48656c6c6f2c20576f726c642100000000000000000000000000000000000000815250905090565b600081519050919050565b600082825260208201905092915050565b60005b838110156100c55780820151818401526020810190506100aa565b838111156100d4576000848401525b50505050565b6000601f19601f8301169050919050565b60006100f68261008b565b6101008185610096565b93506101108185602086016100a7565b610119816100da565b840191505092915050565b6000602082019050818103600083015261013e81846100eb565b90509291505056fea2646970667358221220ac75a2ce55b0fbe1a534ea7ab915b56f482204d22e1af824b361f7e31f387b5b64736f6c63430008090033", "linkReferences": {}, "deployedLinkReferences": {} }
TypeScriptでテストを書く
Shell
$ touch test/HelloWorld.ts
テスト書くためにテストライブラリの「chai」を利用。 chaiはHardhatと一緒にインストール済み。
TypeScript
import { expect } from 'chai'; import { ethers } from 'hardhat';
ethersはether.jsのスマートコントラクトとインタラクトするためのライブラリ。
TypeScript
describe('HelloWorld Contract', function () { it('getMessage returns Hello, World!', async function () { // テスト内容 }); });
describeのarg1に今回のテストのタイトル。arg2にテスト関数
itのarg1に小テストタイトル、arg2にテスト関数。asyncは他のテストを待たずに非同期でやる。
TypeScript
import { expect } from 'chai'; import { ethers } from 'hardhat'; describe('HelloWorld Contract', function () { it('getMessage returns Hello, World!', async function () { const HelloWorld = await ethers.getContractFactory('HelloWorld'); const helloworld = await HelloWorld.deploy();
getContractFactory("コントラクト名") でコントラクトのファクトリを生成。
await ethers.getContractFactory('HelloWorld');でファクトリができるのを待つ。 await HelloWorld.deploy();でローカルの仮想的なイーサリアムに生成したコントラクトをデプロイしてその結果をhelloworldに格納。 次に格納したデプロイ結果をdeployed()で検証を待つ。 最後にデプロイされたコントラクト内の関数の返却値と期待してる値を比較する。
実はこのままだとエラーになる。
await helloworld.deployed(); の部分を下記に書き換える。
ethersの v6.xからawait helloworld.waitForDeployment(); でデプロイ結果を待てる。
deploy()はデプロイメントプロセスを開始するために使用され、waitForDeployment()はそのプロセスが完了するのを待つために使用されるイメージ。
Infuraを使ってテストネット(Sepolia)と接続する
APIキーの準備
ここから新しくAPIキーを作成する。
※2024/02/24時点で無料枠だとAPIキーを1個しか作成できないそう。
SeporiaのEndpointを作成

1:Active Endpointsタブにある「ADD ENDPOINTS」をクリック

2:All Endpointsタブにある「Ethereum」カラムの「SEPOLIA」にチェックし、「SAVE CHANGES」を押す

こんな感じにSepolia用のendpoinntが作られる。
curlでエンドポイントを叩いてみる
イーサリアムのブロックナンバー1番をJsonRPC2.0を使って取得してみる。
Shell
$ curl --url https://sepolia.infura.io/v3/c62ce2442a7543099cf9fa4e87043479 -X POST -H "application/json" -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params": [], "id":1}' {"jsonrpc":"2.0","id":1,"result":"0x51adff"}% //結果
resultの値を10進数に直す。
Shell
python3 -c "print(0x51adff)" 5352959
取得できたみたい。
ここで確認してみる。
ありますた。

HelloWorldコントラクトをテストネットSepoliaにデプロイしてみる
秘密鍵の用意

1:MetaMaskから「アカウントの詳細」

2:秘密鍵を表示
環境変数の設定と呼び出し
プロジェクト直下に作成
Shell
$ touch .env
MetaMaskから取得した秘密鍵の値をPRIVATE_KEY
前回Infuraから取得したSepoliaのエンドポイントをSEPOLIA_URL
Shell
PRIVATE_KEY="hogehoge" SEPOLIA_URL="https://sepolia.infura.io/v3/hugahuga"
最後に、それぞれexportする。
Shell
$ export PRIVATE_KEY="hogehoge" $ export SEPOLIA_URL="https://sepolia.infura.io/v3/hugahuga"
設定した環境変数を呼び出してみる。scripts配下にデプロイ用のコードを書いて、今回はそこで環境変数を呼び出す。
Shell
$ touch scripts/deployHelloWorld.ts
TypeScript
import { ethers } from 'ethers'; async function main() { console.log('PRIVATE_KEY:', process.env.PRIVATE_KEY); console.log('SEPOLIA_URL:', process.env.SEPOLIA_URL); } main().catch((error) => { console.error(error); process.exitCode = 1; });
実行して、表示されたら成功。
Shell
$ npx ts-node scripts/deployHelloWorld.ts
私はTypeScriptのコンパイルand実行にts-nodeを利用しています。 導入と使い方はこちら
実際にデプロイするように書き換える
artifacts/contracts/HelloWorld.sol/HelloWorld.json のabiをデプロイ先のテストネットワークSepoliaに送信する。
まず、TypeScriptがローカルのjsonファイルのインポートを許容するように、tsconfig.jsonを変更する。
JSON
{ "compilerOptions": { "target": "es2020", "module": "commonjs", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "resolveJsonModule": true //これをtrue } }
scripts/deployHelloWorld.ts で、今回作ったHelloWolrdコントラクトのabiをデプロイ先に送信するトランザクションの作成と実行を行う。
TypeScript
// scripts/deployHelloWorld.ts import { ethers } from 'ethers'; import helloWorldArtifact from '../artifacts/contracts/HelloWorld.sol/HelloWorld.json'; async function main() { const privateKey: string = process.env.PRIVATE_KEY ?? ''; if (privateKey === '') throw new Error('PRIVATE_KEY is not set'); const rpcURL: string = process.env.SEPOLIA_URL ?? ''; if (rpcURL === '') throw new Error('SEPOLIA_URL is not set'); const provider = new ethers.JsonRpcProvider(rpcURL); const signer = new ethers.Wallet(privateKey, provider); const factory = new ethers.ContractFactory( helloWorldArtifact.abi, helloWorldArtifact.bytecode, signer ); const contract = await factory.deploy(); console.log('contract deploy address:', contract.target); // コントラクトアドレスの確認 console.log( 'contract deploy transaction URL:', `https://sepolia.etherscan.io/tx/${await contract.deploymentTransaction() ?.hash}` // トランザクションHashを取得し、トランザクション確認サイトURLを表示 ); await contract.waitForDeployment(); console.log('deploy completed'); } main().catch((error) => { console.error(error); process.exitCode = 1; });
トランザクションを送る時は、new ethers.JsonRpcProvider(rpcURL) で引数に渡したネットワークのプロバイダーを作成する。 次に自身のプライベートキーとさっき作ったプロバイダーでnew ethers.Wallet()を使い、signer(署名者)を設定する。 そして、new ethers.ContractFactory(コントラクトのabi,コントラクトのbytecode,設定したsigner) という形で、デプロイするコントラクトのトランザクションデータのファクトリを作成する。
あとは、ethers v6.xの場合、const contract = await factory.deploy(); でトランザクションを送信するまで待つ。そしてawait contract.waitForDeployment(); でトランザクションが実行されるまで待つ。
deploy()はデプロイメントプロセスを開始するために使用され、waitForDeployment()はそのプロセスが完了するのを待つために使用されるイメージ。
それでは実行。。の前に、、
イーサリアムテストネットワークSepoliaに作ったコントラクトのデプロイを行うには、コントラクトabiのトランザクション送信と実行が伴い、「SepoliaETH」という仮想通貨でトランザクション手数料が必要になる。 これは無料で手に入れられる(マイニング)ので下記から自身のMetaMaskアドレスを入力して、マイニングしましょう。

放置時間15分くらいで、私は一旦このくらい集めました。
SepoliaETHを入手したところで、本題に戻りデプロイする。
Shell
$ npx ts-node scripts/deployHelloWorld.ts
すると、、
Shell
contract deploy address: 0x1db3Cc84F3DDBfF433e1bea27Dc1c9EC900DA65A contract deploy transaction URL: https://sepolia.etherscan.io/tx/0xcc930ae6b8bf9cd7dead747842a5b5b9896fb9fef6028bffbe377220c1703e6d deploy completed
URLを確認してみると。。

トランザクションに関わる色々なデータが表示されてあり、「To」にはコントラクドアドレス、一番下の「Input Data」にはコントラクトのバイトコードがある。
これでデプロイは完了
JSON RPCを利用して、デプロイしたコントラクトのメソッドを呼び出す
JSON RPCを利用し、HelloWorldコントラクトのABIに問い合わせる。 JSON RPCとはクライアントとサーバーに置かれた関数の2間でJSONの形式に従ってデータのエンコードとメッセージの送受信を行うリモートで関数を呼び出す通信方法の一種。 今回だと、クライアントが私たち自身のPC、サーバーがテストネットに置かれたgetMessage()メソッドとなる。
Shell
$ touch scripts/callHelloWorld.ts
デプロイ時のコードと似ているが、
TypeScript
import { ethers } from 'ethers'; import helloWorldArtifact from '../artifacts/contracts/HelloWorld.sol/HelloWorld.json'; async function main() { const rpcURL: string = process.env.SEPOLIA_URL ?? ''; if (rpcURL === '') throw new Error('SEPOLIA_URL is not set'); const provider = new ethers.JsonRpcProvider(rpcURL); const contract = new ethers.Contract( address, helloWorldArtifact.abi, provider ); const message = await contract.getMessage(); console.log('message:', message); } main(adress).catch((error) => { console.error(error); process.exitCode = 1; });
new ethers.Contract(コントラクトアドレス, コントラクトのabi, 通信先のprovider) をそれぞれパラメータとして渡す。
そして、await contract.getMessage() でcontracts/HelloWorld.solで定義したgetMessage() メソッドを呼び出す。
確認してみると
Shell
$ npx ts-node scripts/callHelloWorld.ts message: Hello, World!
これで無事にJSON RPCを利用して、デプロイしたコントラクトのメソッドを呼び出せた。
まとめ
簡単なコントラクト作成からテスト、デプロイを行い、デプロイされたコントラクトのメソッドをJSON RPCで呼び出してみる、という一連の流れをやってみた。
ethers v6からネット記事にある書き方と変わっている部分があるため、気をつけましょう。