イーサリアム上に簡単なメッセージアプリを作ってみる(Ethers.js)

8 min read読了の目安(約7400字

はじめに

イーサリアムを使って、中央管理組織のいらない分散型アプリケーション(dApps)を作成することができます。
今回は、dAppsの理解のために、イーサリアム上に下記のようなメッセージアプリを作ってみます。

構成

なるべくシンプルに実装したいと思い、下記構成にしました。

webフロントエンド

  • metamask
    ウォレットアプリ。秘密鍵の管理、コントラクトへの署名を行う。
  • ethers.js
    イーサリアムのノードにアクセスするAPI。
  • browserify
    Node.jsをブラウザでアクセスできるようにするためのモジュールバンドラー。

webバックエンド

  • Node.js

スマートコントラクト開発環境

  • Remix
    Solidity(スマートコントラクトの記述言語)をコンパイル&デプロイするための開発環境。ブラウザベースなのでインストール不要。メタマスクとも接続できるため、コントラクトを直接デプロイすることができる。

Remixを使ったスマートコントラクトのコンパイル&デプロイ

まず、イーサリアム上で稼働するスマートコントラクトは、Solidityで記述することができます。Solidityをブラウザ上でコンパイルからデプロイまでできるRemixという便利な開発環境がありますので、これを使います。

https://remix.ethereum.org/
Remixの使い方は、過去記事も参考にしてください。
https://zenn.dev/zzz/articles/5dcaecacefaf1c

コントラクトの作成とコンパイル

まず、solidityコントラクトを作成します。適当なディレクトリに、"message.sol"を新規作成して、"message.sol"を編集し、以下のようなコントラクトを書きます。

message.sol
pragma solidity >=0.7.0 <0.8.0;

contract Message {

    string message;

    function store(string memory msg_in) public {
        message = msg_in;
    }

    function retrieve() public view returns (string memory){
        return message;
    }
}

store(string memory msg_in) publicは、送信したメッセージをmessageに格納する関数です。
retrieve() public view returns (string memory)は、message変数に格納されている値を呼び出します。
次に、作成したコントラクトをコンパイルします。「Solidity Compiler」タブから、「Compile message.sol」をクリックします。

エラーがでなければ、コンパイル成功です。

コントラクトのデプロイ

Remixの「DEPLOY & RUN TRANSACTIONS」のタブから、デプロイを行います。

Environmentは、「Injected Web3」を選択します。選択すると、メタマスクの連携が行われます。メタマスクのアカウントとEtherの残高はあらかじめ準備しておきます。ここでは、テストネットであるRopstenネットワークを使います。
この設定で「Deploy」ボタンを押すと、メタマスクの画面がポップアウトします。

テストネットなので、ガスプライスは1GWEIとしましょう。ガスリミットは提示されている値のままにします。
この設定で、「確認」ボタンを押します。
これで、トランザクションが送信されました。コンソールにトランザクションのEtherscanアドレスが表示されます。トランザクションを送信してから少し待った後このアドレスからEtherscanを確認すると、「Status:success」となっており、無事にトランザクションが承認されたことが分かります。

コントラクトを操作するインターフェイスの作成

次に、デプロイしたコントラクトを操作するアプリを作成します。
まずは、npmを使って必要なモジュール(ethers/browserify)をインストールします。

npm init
npm install ethers
npm install browserify

ディレクトリ構成は下記のようにしました。

main.js
server.js
public/
 ├ index.html
 └ contract.js

本体はpublic/以下のスクリプトです。今回はNode.jsでサーバを立ててテストするため、サーバ用のスクリプト(main.js,server.js)を別途用意しています。
アプリの全体はGithubにあげたのでそちらを参考にしてもらうとして、ここではポイントをかいつまんで説明します。

https://github.com/zen7676/ethers-basic-example

HTMLインターフェイス

HTMLのインターフェイスを次のように書きます。
読み込むjsファイルは、後述するbrowserifyでビルドしたもの(contract_browser.js)になります。

index.html
<html>
<head>
  <meta charset="UTF-8">
  <title>Ethereum test dapp</title>
</head>

<body>
      <h1>Ethereum test dapp</h1>
      <h3>Status</h3>
          <p>Accounts: <span id="accounts"></span></p>
              <h3 class>Actions</h4>
              <button id="connectButton" disabled></button>
              <button id="retrieveButton" disabled>Retrieve</button>
              <p id="messageStatus">no status</p>
              <button id="storeButton" disabled>Store</button>
              <input type="text" id="inputMessage">
  <script src="contract_browser.js" defer></script>
</body>

</html>

見た目は、以下のようになります。"Connect"ボタンでメタマスクに接続、"Retrieve"ボタンでretrieve関数(メッセージの値の呼び出し)を実行、"Store"ボタンでstore関数(メッセージの書き込み)の実行をします。

JavaSctipt

最初にethers.jsをインポートします。

contract.js
const { ethers } = require("ethers");

まず、メタマスクのインストールのチェックを行います。メタマスクがインストールされていなければインストールを促し、すでにインストールされていれば"Connect"ボタンを有効にします。

contract.js
const MetaMaskClientCheck = () => {
    if (!isMetaMaskInstalled()) {
        onboardButton.innerText = 'Please install MetaMask';
    } else {
        onboardButton.innerText = 'Connect';
        onboardButton.onclick = onClickConnect;
        onboardButton.disabled = false;
    }
};
const isMetaMaskInstalled = () => {
    const { ethereum } = window;
    return Boolean(ethereum && ethereum.isMetaMask);
};

次に、"Connect"ボタンを押したときの動作を記述します。
ethereum.request({method: 'eth_requestAccounts'})でアカウントへの接続を要求します。接続できたら、アドレスをボックスに表示、"retrieve"/"store"ボタンを有効にして、ethers.jsを使ってコントラクトのインスタンスを生成します。

contract.js
const onClickConnect = async () => {
    try {
        //アカウントへの接続を要求
        const newAccounts = await ethereum.request({
            method: 'eth_requestAccounts',
        })
        accounts = newAccounts;
	//アカウントのアドレスを表示
        accountsDiv.innerHTML = accounts;
        if (isMetaMaskConnected()) {
	    //retrieve・storeボタンを有効化
            retrieveButton.disabled = false;
            retrieveButton.onclick = onClickRetrieve;
            storeButton.disabled = false;
            storeButton.onclick = onClickStore;
	    //provider(Metamask)を設定
            const provider = new ethers.providers.Web3Provider(ethereum);
	    //signerの設定
            const signer = provider.getSigner(0);
	    //コントラクトのインスタンスを生成
            myContract = new ethers.Contract(ContractAddress, ContractAbi, signer);
        }
    } catch (error) {
        console.error(error);
    }
};

コントラクトのアドレス(ContractAddress)とABI(ContractAbi)は、

contract.js
const ContractAddress = "0x...";
const ContractAbi = [
        {
            "inputs": [
                {
                    "internalType": "string",
                    "name": "msg_in",
                    "type": "string"
                }
            ],
            "name": "store",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "retrieve",
            "outputs": [
                {
                    "internalType": "string",
                    "name": "",
                    "type": "string"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        }
    ];

とあらかじめ設定しておきます(アドレスはここでは伏せました)。
デプロイ済みのコントラクトのアドレスは、Remixの「DEPLOY & RUN TRANSACTIONS」タブの「Deployed Contracts」から、

ABIは、Remixは「SOLIDITY COMPILER」タブから、コピーすることができます。

コントラクトのretrieve関数を呼び出す部分は次のように書きます。
read-onlyの関数であるため、トランザクションは不要です。

contract.js
const onClickRetrieve = async () => {
    try {
        let res = await myContract.retrieve();
        messageStatus.innerHTML = res;
    } catch (error) {
        console.error(error);
    }
}

コントラクトのstore関数を呼び出す部分は次のように書きます。
こちらは値を書き込むため、実行するためにはトランザクションが必要になります。
テキストボックスに入力した値を取得し、store関数を呼び出してトランザクションに送信します。

contract.js
const onClickStore = async () => {
    try {
        let message = inputMessage.value;
        myContract.store(message);
        messageStatus.innerHTML = 'Your message has been sent';
    } catch (error) {
        console.error(error);
    }
}

browserifyでビルド

browserifyでバンドルし、ブラウザで利用できるようにします。

npx browserify .\public\contract.js -o .\public\contract_browser.js

Node.jsでアプリを起動

Node.jsでサーバーを起動し、

node .\main.js

http://localhost:3000/ にアクセスすれば、アプリが立ち上がります。

まとめ

イーサリアムのdAppsを理解するために、メッセージアプリをSolidityによるコントラクト作成から、テストネットへのデプロイ、ethers.jsによるコントラクトを操作するインターフェイスの作成までを行いました。インターフェイスを作って自分でいじってみることができると楽しいですね。

参考

https://docs.metamask.io/guide/create-dapp.html#project-setup