Ethereum コントラクト開発 Hello world テストネット編

19 min read

この記事について

株式会社CauchyEのメンバーが、社内の開発や勉強会等に関する技術的なアウトプットを行い、弊社の開発における取組や、弊社メンバーの学びに対する取組等をお伝えしていく予定です。弊社の取組の内容や、弊社の雰囲気等が伝わりますと幸いです。

記事作成者自己紹介

株式会社CauchyEのエンジニアの松岡靖典です。
ブロックチェーンの技術的な面白さに魅了され、Web開発の世界を歩み始めました。
NPO法人NEM技術普及推進会NEMTUSの理事としてブロックチェーン技術の普及推進活動に従事したり、個人開発等の取り組みも行っています。
JavaScript, TypeScript, Angular, Firebase, NEM, Symbol, Cosmosが好きですが、チャンスがあれば様々な技術に触れていきたいと思っています。

記事内容要約

前回の記事の続編として、Ethereumのコントラクト開発のHello world的な内容をテストネット環境で試す内容です。(機会があれば、NFTが盛り上がりを見せているERC721等の記事もいずれ作成してみたいと思っています。 -> ERC721のコントラクト開発の記事も公開しましたのでぜひ併せてご参照ください。)
なお、この記事の作成においては、こちらの記事[1]を参考にさせて頂きました。

この記事での目標

  • hardhat[2]を用いて自動的に生成されたHello world的なコントラクトを使って、テストネットでのコントラクト開発等を行い、コントラクトのデプロイ、テストを実行した後、hardhatに依存せずethers[3]を適切に設定してNode.jsでコントラクトを実行し、最後にコントラクトを第三者が利用しやすいようコントラクトの情報を公開するところまで行います。

環境構築

前提環境

  • OS
    • Ubuntu 20.04 (on WSL2)
  • Node.js
    • v14.17.4 (記事作成時点で最新のLTS)
  • npm
    • 7.20.5 (記事作成時点で最新のnpm)
  • Ethereum Goerliテストネット
  • hardhatの初期化で自動生成されたコントラクトの開発をローカル環境で前回の以下記事の通り実施済

その他のLinuxやMac等でも可能と思いますが未検証です。

テストネットでのコントラクト開発

前回記事でローカルでのコントラクト開発の流れはだいぶつかめたと思うので、パブリックなテストネットでのコントラクトの開発に進みます。
ローカルでhardhatを利用して開発していた時と異なり、コントラクトをデプロイするためのアカウントの秘密鍵や、接続先ノード等を準備して設定しておく必要がありますので、それらについての説明や、デプロイしたコントラクトのソースコードをEtherscanを通じて検証・公開する方法を説明します。

テストネットの種類

Ethereumではテストネットが複数存在します。今回の技術調査では、Polygonとの連携も視野に入っていたため、PolygonのMumbaiテストネットがEthereumのGoerliテストネットとの連携が可能であるように見受けられたことから、テストネットとしてGoerliネットワークを利用することにしました。

どのテストネットを使用すべきかは内容にもよるとは思いますが、Ropsten, Rinkeby, Kovan, Goerliいずれのテストネットの場合でも、ソースコードの中ではほとんどネットワークの違いを意識することなく、作業や検証を進めていくことができたので、とりあえずコントラクトを試してみるという今回の記事のようなレベルであれば、どのテストネットを使うかをあまり悩みすぎる必要はないかもしれません。

また、少し話がそれますが、Ethereum互換の様々なネットワークは、各ネットワーク毎に異なるChain IDを持っています。hardhatのローカル開発環境のChain IDはローカル開発用ネットワークとして使われることの多かったGeth private chainsの1337と異なり31337となっているようで、MetaMask等をはじめとするウォレットによっては、localhostが接続先のノードの設定の場合、暗黙的にChain IDとして1337が設定されていて、hardhatのローカル開発環境向けにトランザクションを発行しようとした際、Chain IDが異なるエラーになるといったことがありがちなので、頭の片隅に入れて覚えておくとよいと思います。

https://hardhat.org/metamask-issue.html

各ネットワークのChain IDの情報は、以下リンクがシンプルでわかりやすいと思います。

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#list-of-chain-ids

今回の記事では、記事のボリュームが膨れ上がってしまったため、バックエンドのNode.js環境上でコントラクトのデプロイや、コントラクトの関数を呼び出して実行する方法に絞って説明しており、フロントエンドでのMetaMaskとの連携のための実装等については触れません。いずれ機会があれば、続編的な記事として、どこかで別のタイミングでフロントエンドのWebアプリにおけるMetaMaskとの連携等についても記事を作成してみたいと思います。

接続先ノード(Infura,Alchemy,Etherscan)

Ethereumのノードを自分自身でたてて管理するには、(新しく生成されていくブロックに、同期が追いつけず、遅れていってしまう状態にならないよう、)CPU, メモリ, ネットワーク, 大容量のSSDストレージといった点でハイスペックなサーバーが必要で、各自でノードをセットアップして維持するのはハードルが高いと思います。

多くの場合、Infura, Alchemy, Etherscan等のノードのAPIを提供してくれているサービスを利用するのが合理的でしょう。

それらのサービスのアカウントを作成し、InfuraのProject ID, AlchemyのAPI Token, EtherscanのAPI Token等を取得しておきましょう。

これらすべてのサービスを利用しなければならないわけではありませんが、いずれかのサービスの利用上限に足してしまったり、いずれかのサービスで大規模障害が発生してしまったりといった問題の際に、複数種類のサービスの接続先ノードが設定されていれば、より堅牢で安定的な動作が見込めると思うので、複数のサービスを設定しておくと手堅いと思います。

なお、これらの情報は、公開してしまうと、リクエスト上限限界まで第三者に勝手に使用されてしまうリスクが高いと思いますし、意図しない形で悪用されてしまう可能性もあると思うので、安全に秘密を保つ形で利用するのが良いでしょう。

本記事では、以下のような環境変数に設定して利用することとします。

  • INFURA_PROJECT_ID
  • ALCHEMY_API_TOKEN
  • ETHERSCAN_API_TOKEN

秘密鍵等のアカウント情報

パブリックなテストネットにコントラクトをデプロイしたり、コントラクトを呼び出して実行したりするためには、パブリックなテストネット上で使えるテスト用のETHを保有したアカウントを準備し、そのアカウントの秘密鍵等の情報を安全に設定して利用する必要があります。

まずは、MetaMask等のウォレットでアカウントを作成し、Faucetからテスト用の少量のETHを取得しましょう。ご自身のtwitterアカウントでアドレスを含むtweetを行い、そのtweetのリンクを入力することで、テスト用のETHを入手することができます。

テスト用のETHが入手できたことが確認できたら、そのアカウントの秘密鍵の情報を安全に秘密を保つ形で利用しましょう。

本記事では以下のような環境変数に設定して利用することとします。

  • PRIVATE_KEY

hardhatを使ってコントラクトのデプロイ、テストを実行

hardhatがすべてを良い感じに暗黙的に必要なグローバル変数を設定してくれるローカル開発用ネットワークへのデプロイやテストと異なり、パブリックなテストネットへのコントラクトのデプロイやテストの実行では、接続先ネットワークやアカウント等の情報をhardhatの設定ファイルに適切に指定する必要があります。

contracts/hardhat.config.jsに、接続先ノードとアカウントの秘密鍵の情報を以下のように追記します。(ここから~ここまで追記 の部分です。)

require("@nomiclabs/hardhat-waffle");

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();
  
  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
  // ここから
  networks: {
    goerli: {
    url: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
    accounts: [process.env.PRIVATE_KEY]
    }
  },
  // ここまで追記
  solidity: "0.8.4",
};

これで、パブリックなテストネットにコントラクトをデプロイしてテストを実行できるようになりますが、以下のように実行してみるとエラーになってしまいました。

$ npx hardhat test --network goerli

Greeter
    1) Should return the new greeting once it's changed

0 passing (20s)
1 failing

1) Greeter
        Should return the new greeting once it's changed:
    Error: Timeout of 20000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/test/sample-test.js)
    at listOnTimeout (internal/timers.js:557:17)
    at processTimers (internal/timers.js:500:7)

エラーの原因は、chaiのデフォルトのタイムアウトが20秒であるのに対し、トランザクションを2回実行するこのテストだと、テストが正常に実行された場合でも約60秒程度時間がかかってしまい、タイムアウトになってしまうためです。
そこでデフォルトのタイムアウトを20秒から60秒に変更して再度テストを実行してみます。

contracts/test/sample-test.jsのテストコードのdescribe内の冒頭に、タイムアウトをデフォルトから変更するコードを追加し、コントラクトのデプロイのトランザクションの情報や、生成されたコントラクトアドレスの情報や、コントラクト呼び出しのトランザクションの情報等のログ出力を追加で実装し、

const { expect } = require("chai");

describe("Greeter", function () {
  this.timeout(60 * 1000); // タイムアウトを延ばすため追加
  it("Should return the new greeting once it's changed", async function () {
    const Greeter = await ethers.getContractFactory("Greeter");
    const greeter = await Greeter.deploy("Hello, world!");
    await greeter.deployed();
    // コントラクトをデプロイしたトランザクションをブロックチェーンエクスプローラーで確認するための情報出力追加
    console.log(`Greeter Deploy Tx: https://goerli.etherscan.io/tx/${greeter.deployTransaction.hash}`);
    console.log(`Greeter Contract Address: ${greeter.address}`);

    expect(await greeter.greet()).to.equal("Hello, world!");

    const setGreetingTx = await greeter.setGreeting("Hola, mundo!");

    // wait until the transaction is mined
    await setGreetingTx.wait();
    // コントラクトを呼び出して挨拶文を変更したトランザクションをブロックチェーンエクスプローラーで確認するための情報出力追加
    console.log(`setGreetingTx: https://goerli.etherscan.io/tx/${setGreetingTx.hash}`);

    expect(await greeter.greet()).to.equal("Hola, mundo!");
  });
});

再度テストを実行してみます。

$ npx hardhat test --network goerli

Greeter
Greeter Deploy Tx: https://goerli.etherscan.io/tx/0x22e08e22911cb3ea938a9dbd8deae003dad8682d5aede3c034d9ecd576953892
Greeter Contract Address: 0xbb285cA1f2Ffd6C49b6A1Ab01d7694049c81d74a
setGreetingTx: https://goerli.etherscan.io/tx/0xefa8a4963785442468f8fa0bd36b155db4b08fd1e8b04ba6eef2dc300c9e41b9
    ✓ Should return the new greeting once it's changed (56691ms)

1 passing (57s)

無事テストがpassしました!🎉

hardhatを使わないコントラクトのデプロイ・呼び出し

開発やテストの際には、hardhatはとても便利なのですが、実際に何らかのサービスのバックエンドで動作するコントラクトのデプロイやコントラクトの呼び出しを実装したい場合、hardhatコマンドを介してすべてを実行するのは不便そうです。hardhatコマンドを介さずに、Node.jsで直接コントラクトのデプロイや、デプロイ済のコントラクトを呼び出して実行できるプログラムを起動する方法もやはり必要だと思います。

そのためには、hardhatがこれまでhardhat.config.jsの中で設定した内容や暗黙の仮定をもとに自動的に接続先ノードやトランザクションを発行するアカウントの情報を設定してくれていた部分を、ethersでプログラム中で適切に指定する必要があります。

コントラクトを呼び出すために必要な情報の再構成のためには、前回の記事の前半で登場したcontracts/artifacts/contracts/Greeter.sol/Greeter.jsonの中のabiとbytecodeを使用します。

接続先ノードやアカウント情報の設定のためには、以下のような内容で、contracts/scripts/sample-script-without-hardhat.jsファイルを作成します。

結果的に、接続先ノードの指定を行うprovider, アカウントの秘密鍵の情報(と接続先ノードの情報)の指定を行うwallet, これからデプロイするコントラクトの必要な情報を構成するcontractFactory, 既にデプロイ済のコントラクトを呼び出して実行する際に必要な情報の構成方法deployedContract, コントラクトの呼び出しにトランザクション発行が必要な場合のconnect(wallet)の箇所等にコツがあったのですが、参考となる実装の情報を調査しつつ、手探りで実装を試してトライ&エラーするのに結構時間がかかった印象がありました。

const { ethers } = require("ethers");
const contractJsonData = require("../artifacts/contracts/Greeter.sol/Greeter.json");

async function main() {
  const provider = ethers.getDefaultProvider(
    "goerli",
    {
      infura: process.env.INFURA_PROJECT_ID,
      alchemy: process.env.ALCHEMY_API_TOKEN,
      etherscan: process.env.ETHERSCAN_API_TOKEN,
    }
  );
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
  const contractFactory = new ethers.ContractFactory(contractJsonData.abi, contractJsonData.bytecode, wallet);
  
  console.log("greeting contract deploy start with 'Hello, world!'");
  const contract = await contractFactory.deploy("Hello, world!");
  console.log(`greeting contract deploy tx hash: ${contract.deployTransaction.hash}`);
  console.log(`greeting contract deploy tx link: https://goerli.etherscan.io/tx/${contract.deployTransaction.hash}`);
  console.log("waiting for greeting contract deploy tx confirmed...");
  await contract.deployTransaction.wait([confirms = 1]);
  console.log("greeting contract deploy tx confirmed");
  const contractAddress = contract.address
  console.log(`greeting contract address: ${contractAddress}`);
  
  const deployedContract = new ethers.Contract(contractAddress, contractJsonData.abi, provider);
  
  console.log(`current greeting message is ${await deployedContract.greet()}`);
  
  console.log("send tx to call setGreeting")
  const setGreetingTx = await deployedContract.connect(wallet).setGreeting("Hola, mundo!");
  console.log(`setGreeting tx hash: ${setGreetingTx.hash}`);
  console.log(`setGreeting tx link: https://goerli.etherscan.io/tx/${setGreetingTx.hash}`);
  console.log("waiting for setGreeting tx confirmed...");
  await setGreetingTx.wait([confirms = 1]);
  console.log("setGreeting tx confirmed");
  
  console.log(`after changing, greeting message is ${await deployedContract.greet()}`);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

hardhatを使わず、node scripts/sample-script-without-hardhat.jsコマンドで対象ファイルを実行すると以下のような結果が得られました。成功です!🎉

$ node scripts/sample-script-without-hardhat.js 

greeting contract deploy start with 'Hello, world!'
greeting contract deploy tx hash: 0x532185c0010da275d42bef56d2bc829610a1d8818043bb95d35a25e428dd7eca
greeting contract deploy tx link: https://goerli.etherscan.io/tx/0x532185c0010da275d42bef56d2bc829610a1d8818043bb95d35a25e428dd7eca
waiting for greeting contract deploy tx confirmed...
greeting contract deploy tx confirmed
greeting contract address: 0x105E67BD932058C39c6f5cd3B19eEe2d392d67FC
current greeting message is Hello, world!
send tx to call setGreeting
setGreeting tx hash: 0x3347aa722ed93fd6f5db3427b2405c60d82527c930c92804ff4628a78fa67f5d
setGreeting tx link: https://goerli.etherscan.io/tx/0x3347aa722ed93fd6f5db3427b2405c60d82527c930c92804ff4628a78fa67f5d
waiting for setGreeting tx confirmed...
setGreeting tx confirmed
after changing, greeting message is Hola, mundo!

ログ出力されたコントラクトやトランザクションの情報を記録しておきます。

こちらは、次の記事で説明するverify and publish未実行状態のままキープしておくためのコントラクトです。

こちらは、同じ内容のコントラクトですが、次の記事で説明するverify and publishを実際に実行して試すためのコントラクトです。

コントラクトのverify and publish

Etherscanでコントラクトアドレスのコントラクトの情報のページを見ると、"Are you the contract creator? Verify and Publish your contract source code today!"というメッセージが出ていることが気になりました。

https://goerli.etherscan.io/address/0x105e67bd932058c39c6f5cd3b19eee2d392d67fc#code

調べてみたところ、コントラクトをデプロイしただけの状態では、Etherscanから確認できる情報は、あくまでもブロックチェーンにデプロイされた情報であり、そのままでは、既にデプロイ済のコントラクトをプログラムから呼び出して使用する際に利用したabi等の情報をEtherscanで直接参照できないことがわかりました。

しかし、コントラクトのverify and publishを行うことで、(デプロイ前のコントラクトに関する詳細な情報をEtherscanに渡して、Etherscanがそれをもとに正しくデプロイされているかどうか検証し、正しくデプロイされていることが確認できたらその情報をEtherscanで公開してくれることによって、)自分自身以外の第三者もabiやコンパイル前のソースコード等をEtherscan上で確認できるようになり、誰もがより容易にコントラクトを検証したり、コントラクトを呼び出して実行できるようになるようです。

Etherscan上にGUIのフォームが用意されており、最初はそのフォームからの実行を試みたのですが、hardhat環境下ではフォームにインプットすべき情報をうまく準備できず、結果的にhardhatのpluginのhardhat-etherscanとEtherscanのAPIを通じて使用して実行する必要がありました。

EtherscanのAPIを叩くために、Etherscanにユーザー登録してAPI KEYを登録して、環境変数 ETHERSCAN_API_TOKENに設定しておきましょう。この記事ではテストネットの接続先ノードの設定の際にすでにこの手順は実施済ですが、念のため改めて記載しておきます。

EtherscanのAPIを叩いてhardhat環境でverify and publishを実行するためのpluginをインストールします。

npm install --save-dev @nomiclabs/hardhat-etherscan

EtherscanのAPI Keyをcontracts/hardhat.config.jsに設定します。接続先ノードのネットワーク設定も必要ですが、この記事の流れでは、テストネットの接続先ノードの設定の際にすでに設定済です。結果的に以下のようになります。

require("@nomiclabs/hardhat-etherscan");
require("@nomiclabs/hardhat-waffle");

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();
  
  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
  networks: {
    goerli: {
      url: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
      accounts: [process.env.PRIVATE_KEY]
    }
  },
  solidity: "0.8.4",
  // ここから
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_TOKEN
  },
  // ここまで追加
};

この状態で、コントラクトのsolファイルのパス, 名前, hardhat.config.js内で指定した接続先ネットワーク名, コントラクトアドレス, コントラクトデプロイ時に指定するパラメーターの値(=今回の場合"Hello, world!"。コントラクトデプロイ時に指定するパラメーターなければ不要)を以下のようなコマンドで指定して実行し、

$ npx hardhat verify --contract contracts/Greeter.sol:Greeter --network goerli 0x8d9fBC02598e32C8d594bbEF4257846653ff4732 "Hello, world!"

Nothing to compile
Compiling 1 file with 0.8.4
Successfully submitted source code for contract
contracts/Greeter.sol:Greeter at 0x8d9fBC02598e32C8d594bbEF4257846653ff4732
for verification on Etherscan. Waiting for verification result...

Successfully verified contract Greeter on Etherscan.
https://goerli.etherscan.io/address/0x8d9fBC02598e32C8d594bbEF4257846653ff4732#code

Successfully verified contract Greeter on Etherscan.のようなメッセージが表示されたら成功です!🎉

ログに表示された以下リンクを開いてみると、コントラクトのソースコードやabi等といったコントラクトのより詳細な情報を確認できるようになりました!

https://goerli.etherscan.io/address/0x8d9fBC02598e32C8d594bbEF4257846653ff4732#code

まとめ

テストネットでのコントラクト開発の流れ

  • 基本的な流れはローカルでの開発の流れと同じ
  • 接続先ノードを自分自身で準備しておく必要がある(Infura, Alchemy, Etherscan等)
  • 接続先ノードに関するProject IDやAPI Token等の公開すべきでない情報は環境変数等の安全な方法で設定しておく必要がある
  • コントラクトをデプロイしたりコントラクトを呼び出したりするためのアカウントにテスト用ETHをFaucet等から取得して準備しておく必要がある
  • 使用するアカウントの秘密鍵のような絶対に公開してはならない情報は環境変数等の安全な方法で設定しておく必要がある
  • hardhatを用いてテストネットにコントラクトをデプロイしたりコントラクトのテストを行うためにはhardhat.config.jsに設定を追記する必要がある
  • 第三者がコントラクトの検証やコントラクトの呼び出しを容易に行えるよう、コントラクトのverify and publishを行っておくとより良い
  • コントラクトのverify and publishの方法は、hardhat環境を利用している場合、Etherscanのweb上のGUIのフォームからではうまく実行できず、hardhatのpluginを用いてhardhatコマンドで実行することができた

所感

  • 基本的な流れはローカルでの開発と同じで、シンプルに接続先ノードや、アカウントの指定をテストネット向けに明示的に行う必要がある点に違いがあるということが実際に試してみてよくわかりました。
  • パブリックなテストネットではブロック生成間隔の時間だけ、トランザクションが承認されるのを、テストの際にも待たねばならないのに対し、hardhatが提供してくれているローカル開発用環境では、ブロック生成の待ち時間がとても短く、テストを短時間で実行でき、開発やテストが効率よく実行できるよう工夫されていることを改めて感じました。
  • 接続先ノードについて、無料で、開発者が試してみるには十分な機能のノードを、即座に使えるようになる、Infura, Alchemy, Etherscan等のサービスがとても便利だと思いました。
  • アカウントの設定や接続先ノードに関して、今回明示的に処理を書いたことで、以下の点に関して理解が深まりました。
    • hardhatがローカル開発環境で暗黙的に処理してくれていた部分を、テストネットではどのように指定すればよいか?
    • hardhat無しでNode.jsで実行可能なスクリプトとするにはどう指定すればよいか?
  • バックエンドでの実装については概ね把握できたものの、フロントエンドでの実装については、秘密鍵や接続先ノードの管理をMetaMaskを通じて行うのが一般的なようで、それらについては少し実装方法が異なるようでした。(今回の記事では触れませんでしたが、いずれ機会があれば、記事にしてみたいと思います。)

【エンジニア採用中】
ブロックチェーンやデータサイエンスに興味のあるエンジニアを積極的に採用中です!
以下のページから応募お待ちしております。

https://cauchye.com/company/recruit
脚注
  1. https://qiita.com/mogiken/items/54bc24669068ecadf957 ... この記事を作成するのに、こちらの資料がとても参考になりました。この場を借りてお礼申し上げます。 ↩︎

  2. https://hardhat.org/ ↩︎

  3. https://docs.ethers.io/v5/ ↩︎

Discussion

ログインするとコメントできます