JavaScript初心者が、Dappsとそのフロントエンドを作りたい2

前回は、ローカルで動作するブロックチェーンのスマートコントラクトを作成しました。

今回は、前回作成したスマートコントラクトをWebサイト上から操作するためのフロントエンド構築を行います。

前回↓

MetaMaskのインストール

ブラウザにインストールするタイプのイーサウォレットです。MetaMaskの詳細は以下のWebサイトが詳しいです。

Dappsでは、コントラクトの状態を変更したりする場合、トランザクションを発行する必要があり、その時にイーサを消費します。トランザクションを発行する時、トランザクションにはアカウントの秘密鍵で署名する必要があります。しかし、この秘密鍵の管理には、セキュリティの高度な知識が必要になってきます。それを代わりに行ってくれるのがMetaMaskです。MetaMaskはchromeとFireFoxの拡張機能として準備されています。

私のイメージは以下の図の通りです。Web3周りの部分が曖昧なところがありますが、いろいろ調べた感じ、以下のようなイメージに落ち着きました。

それでは、MetaMaskをインストールして使用できる状態としておきましょう。

Webサーバーを構築する

ローカルマシン上に、http://localhost:8080でブラウザからアクセスができるサーバーを動作させます。動作の際には、live-serverを使用します。

まずは、Webサイトデータを格納するためのフォルダを作成します。サーバーのデータフォルダは、前回のtruffleプロジェクトフォルダの外に作成しましょう。

もし、前回の続きで行う場合、truffleプロジェクトのフォルダから出てください。私は、ホームディレクトリ直下に移動します。

cd ~/

そして、Webサイトデータを格納するためのデータフォルダを作成、作成フォルダの階層に移動します。

mkdir test-server && cd test-server

必要なファイル

コントラクト操作のためのサーバーに必要なファイルとしては以下の通りです。

①carshare_abi.js
truffleでコンパイルしたときに生成されるjsonファイルから、abiのコードを抜き出したものです。

コントラクトにどんな変数があるのか、どんなメソッドがあるのか、また、引数はどんなもので・・・等、コントラクトとのインターフェイスを定義するファイルになります。

②web3.js(1.0.0系を使用します。)
javascriptで作成されたイーサリアムとやり取りするためのプログラムライブラリです。

③index.html
ユーザーへデータを表示したり、ボタンを表示したりします。javascriptとhtmlでコーディングを行います。

サーバー環境準備

ここでは、先に紹介した①と②のファイルを準備します。

carshare_abi.js

truffleでコンパイルしたフォルダ階層の

build->contracts

内に、CarShare.jsonというファイルが存在します。以下の図のようになっていて、選択部分がabiとなります。これをコピーしましょう。

これを以下のように、Javascriptファイルとして作成します。

abiのデータは、「CarShareABI」変数に格納します。

var CarShareABI = [
  {
    "constant": true,
    "inputs": [
      {
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "cars",
    "outputs": [
      {
        "name": "rentalOwner",
        "type": "address"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "constant": false,
    "inputs": [
      {
        "name": "carId",
        "type": "uint256"
      }
    ],
    "name": "adopt",
    "outputs": [
      {
        "name": "",
        "type": "uint256"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

carshare_abi.js

ABIをみると、コントラクトで作成した変数や関数がそれぞれ定義されていることがわかると思います。

このファイルを「carshare_abi.js」として「test-server」ディレクトリに保存します。

web3.js

npmコマンドでインストールしたり、取得方法はいろいろありますが、ここではシンプルにweb3.jsファイルをダウンロードしてディレクトリに配置します。

以下URLからWeb3.jsを取得します。(1.0.0系を使用します。)

 GitHub 
ethereum/web3.js
https://github.com/ethereum/web3.js/tree/1.0
web3.js - Ethereum JavaScript API

画面右のclone & downloadでzip形式ダウンロードをして、解凍したファイルの中の、「dist」フォルダ内、「web3.min.js」をtest-serverディレクトリにコピーします。

コーディングと検証

ここから③のindex.htmlに実際に動作を確認するためのフロントエンドをコーディングしていきます。ステップ毎にコードを確認しながら記載していこうと思います。

検証に際して、

・Ganacheを起動しておいてください。

・MetaMaskにログインして、ネットワーク設定をhttp://localhost:7545に設定しておいてください。

・MetaMaskにGanacheのインデックス0のアカウントをインポートしておいてください。

Web3プロバイダの設定

まずは、秘密鍵を管理していてくれるMetaMaskがすでにWeb3プロバイダの接続をしているため、Web3オブジェクトを自分で作成しないで、MetaMaskが作成してくれているものを使用します。以下の図でいう、電話回線をすでにつないでくれているところに便乗するみたいなイメージでしょうか。

さて、次のコードを検証してみます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>CarShare front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="./carshare_abi.js"></script>
  </head>
  <body>
    <div id="shares"></div>

    <script>
      window.addEventListener('load', function() {

      function startApp() {}

      //MetaMaskがインストールされているかのチェック
      if (typeof web3 !== 'undefined') {
        // メタマスクのWeb3プロバイダを使用する。
        web3js = new Web3(web3.currentProvider);
        alert("起動");
      } else {
        // メタマスクがインストールされていない場合、メタマスクのインストールを促す
        alert("メタマスクをインストールしてください。");
      }

      // ユーザーのアプリ開始
      startApp()

      })
    </script>
  </body>
</html>

<head>内でjQueryと、先ほど追加した「web3.min.js」、「carshare_abi.js」を読み込みます。

実際コントラクトとやり取りをするコードはbodyタグ内のscriptタグになります。

//MetaMaskがインストールされているか、という部分は、MetaMaskがインストールされているかどうかを判定して、インストールされていれば、現在のWeb3プロバイダを使用する、という処理になります。

これを実行すると、メタマスクがインストールされているブラウザで起動したときにWeb3の接続ができ、それ以外の場合は、メタマスクのインストールを促します。alertはテスト用ですので、任意にログ出力などに変更したほうがやりやすいかもしれません^^;

そして、最後のstartApp()関数を呼び出しています。

startApp関数の定義は、scriptタグのすぐ下にあります。

これで、Dappを起動する準備ができました。

live-server .

でサーバーを起動してみましょう。

コントラクトの作成~アカウント取得

メタマスクをインストールしたブラウザが無事起動できたでしょうか?

次に、プログラムでコントラクトのメソッド等が使用できるように、コントラクトのオブジェクトを作成します。

そして、アカウント情報をJavaScriptの変数に取得します。

    <script>
      window.addEventListener('load', function() {
        var CarShare;//追加
        var userAccount;//追加

        function startApp() {
          //この関数に追加します。
          //コントラクトの作成
          var CarShareAddress = "0x任意のコントラクトアドレスです。デプロイ時のアドレス。";
          CarShare = new web3js.eth.Contract(CarShareABI, CarShareAddress);

          //ユーザーの登録
          web3.eth.getAccounts(function(err, accounts) {
            console.log(accounts[0]);
            userAccount = accounts[0];
          });
          //userAccount = web3.eth.accounts[0]; //CryptoZombiesなどはこれで取得しているが、、、
        }

        //MetaMaskがインストールされているかのチェック
        if (typeof web3 !== 'undefined') {
          // メタマスクのWeb3のプロバイダを使用する。
          web3js = new Web3(web3.currentProvider);
          alert("起動");
        } else {
          // メタマスクがインストールされていない場合、メタマスクのインストールを促す
          alert("メタマスクをインストールしてください。");
        }

        // ユーザーのアプリ開始
        startApp()
      })
    </script>

追加したところは、

2つのvar、CarShareとuserAccountを追加しました。

また、startAppsにコントラクトと、ユーザーアカウントを取得する処理を追加しました。

CarShareというコントラクトをJavaScriptで使えるようにnewしたいので、CarShareAddress変数にコントラクトのアドレスを入れます。(デプロイ時に控えておいたあれですね)

そして、CarShareABI(これはcarshare_abi.jsの中で定義したあれです)とCarShareAddressを使用してCarShareを作成しました。

次に、現在のMetaMaskで有効にしているユーザーアカウントアドレスをuserAccount変数に取得します。

ここで少してこずりました。

CryptoZombiesではweb3.eth.accounts[0]が現在MetaMaskで指定しているアカウントアドレスが取得できる、とあるのですが、できない。。。

いろいろ調べた結果、web3.eth.getAccountsで上記のコードのようにすることで、アカウントのアドレスを無事取得することができました。

ここでも、サーバーを起動し、ブラウザのコンソールに現在有効にしているMetaMaskのアカウントアドレスが表示されれば成功です!

コントラクトのデータを取得する

さて、次はコントラクトのデータを取得してブラウザに表示したいと思います。

以下のコード、「//コントラクトから内容の取得」部分を追加してください。

function startApp() {
  //コントラクトの作成
  var CarShareAddress = "0x任意のコントラクトアドレス。";
  CarShare = new web3js.eth.Contract(CarShareABI, CarShareAddress);

  //ユーザーの登録
  web3.eth.getAccounts(function(err, accounts) {
    console.log(accounts[0]);
    userAccount = accounts[0];
  });
  //userAccount = web3js.eth.accounts[0]; //CryptoZombiesなどはこれで取得しているが、、、

  //コントラクトから内容の取得
  CarShare.methods.cars(0).call().then(function(cars) {
  $("#shares").append(`<div class="share">
      <ul>
        <li>rentalOwner: ${cars}</li>
      </ul>
    </div>`);
  });
}

先ほど作成したCarShareというコントラクトは上記のようにコントラクトの変数にアクセスすることができます。

※内容をcars型の構造体carsから、値を取得する時、今回はrentalOwnerという一つの変数しかないため、${cars}で値が取得しています。通常、構造体の変数にアクセスするには、cars.rentalOwnerとするのですが、今回、一つだけの場合、carsとしないと取得できませんでした。cars構造体の中に、2つ以上の変数が存在する場合、cars.rentalOwnerなどとして取得ができます。

ほんとはgetメソッドを作成して、直接変数を読みだせないようにしたほうが良いのですが、まずはシンプルな形で作成してみたかったので、このようにしました。carsの配列の0番の情報をhtmlにインジェクト(↓の※部分)しています。これはCryptoZombiesのコードを参考にさせていただきました。

※バッククオーテーションで囲んだ部分はJavaScriptのECMAScript2016というやつで追加された「テンプレートリテラル」というもので、${…}というプレースホルダーを使って変数等を埋め込むことができるんです。

前回作成したコントラクトはコンストラクタでデプロイするときにデプロイしたアドレスがrentalOwner変数に書き込まれる為、ブラウザにはデプロイしたときのアカウントアドレスが表示されると思います。Ganacheで動作させていると、インデックス0のアカウントアドレスになるんですかね。

アカウントアドレスの書き込み

次に、ボタンを押すことで、コントラクトのadopt関数を走らせ、現在MetaMaskで設定しているアカウントのアドレスを書き込みます。

ということで、Ganacheのインデックス1のアカウントをMetaMaskにインポートしておきましょう。

<body>
  <div id="shares"></div>
  <button type="button" id="button_set">レンタル</button><!-- 追加 -->

  <script>
    window.addEventListener('load', function() {
      var CarShare;
      var userAccount;

      function startApp() {
        //コントラクトの作成
        var CarShareAddress = "0x任意のコントラクトアドレス。";
        CarShare = new web3js.eth.Contract(CarShareABI, CarShareAddress);

        //ユーザーの登録
        web3.eth.getAccounts(function(err, accounts) {
          console.log(accounts[0]);
          userAccount = accounts[0];
        });
        //userAccount = web3.eth.accounts[0]; //CryptoZombiesなどはこれで取得しているが、、、

        //コントラクトから内容の取得
        CarShare.methods.cars(0).call().then(function(cars) {
          $("#shares").append(`<div class="share">
            <ul>
              <li>rentalOwner: ${cars}</li>
            </ul>
          </div>`);
        });
        
        //追加 ボタンを押した時の動作
        document.getElementById("button_set").onclick = () => {
          setVal();
        }
      }

      //追加 ボタンを押した時の動作内容
      function setVal(){
        return CarShare.methods.adopt(0)
               .send({ from: userAccount })
             //.send({ from: "0xアカウントアドレス" })//直接アカウントアドレス叩けばsendできる
               .on("receipt", function(receipt){
                 console.log("success");
               })
              .on("error", function(error){
                 console.log("error");
              });
      }

      //MetaMaskがインストールされているかのチェック
      if (typeof web3 !== 'undefined') {
        // メタマスクのWeb3のプロバイダを使用する。
          web3js = new Web3(web3.currentProvider);
          alert("起動");
      } else {
        // メタマスクがインストールされていない場合、メタマスクのインストールを促す
        alert("メタマスクをインストールしてください。");
      }

      // ユーザーのアプリ開始
      startApp()
    })
  </script>
</body>

buttonタグを追加しました。

また、そのボタンが押された時、setVal関数を呼び出すようにstartApp関数に追加しました。

setVal関数では、コントラクトのadopt関数を呼び出しています。sendによってアカウントを送信し、その結果、成功すればsuccess、エラーの場合errorとコンソールに表示しています。

それでは、MetaMaskで先ほど追加したアカウントに変更し、レンタルボタンを押してください。

そうすると、MetaMaskNotificationのウィンドウが表示されますので、Submitしてくださいね。

消費するGasは非常に微量なので、Ganacheでは100etherのままですが、MetaMaskのアカウント残高は減少していますね。

ブラウザをリロードすると、ボタンを押した時のアクディブなアカウントのアドレスが書き込まれていることがわかると思います。

追記

RopstenネットワークとXサーバーにて実装しました↓

まとめ

フロントエンドとしては、汚いコードなのかもしれませんが、コントラクトのデータ取得とデータ書き込みを検証する、という点では、とりあえずCryptoZombiesで学んだことができていると思います。JavaScriptをガシガシ書くことができる人はPetShopのフロントエンドを見ればどこがどのように動作しているか一目でわかると思いますが、jQueryをファッション感覚で触っている程度の自分には難しかったので、原始的なやり方でトライしてみようと思いました。これを機にJavaScript、もっと勉強してみようと思います。

追伸、これを買いました。