Dapps作成手順をイチから学べる!Truffleの「イーサリアム・ペットショップ」

Dapps作成手順をイチから学べる!Truffleの「イーサリアム・ペットショップ」

Dapps開発のフレームワークである「Truffle」の公式サイトで紹介されているチュートリアル「ETHEREUM PET SHOP」をやってみました。

「ETHEREUM PET SHOP」のチュートリアルは英語であるため、英語が苦手な方は躊躇されるかもしれませんね。

そこで「ETHEREUM PET SHOP」をやり終えた私の体験を記事にまとめました。

本家の「ETHEREUM PET SHOP」と合わせて読んでいただければ、英語が苦手な方でもDappsの開発を進めやすくなりますので、是非参考にしてみてください。

目次

Truffleとは?

Dapps開発を効率化するフレームワーク

プログラムのコンパイル、テスト、ブロックチェーンへのデプロイなど、これらをスムーズに行うことができるツールがTruffleです。

セキュアなDappsの作成を支援するZeppelinや、メッセージングDappsのStatusなどで既に利用されています。

Dapps開発に必須となりうるポテンシャルを持ったツールですので、Dapps開発に興味のある方は早めに使い慣れておくことをオススメします。

Truffleを利用すると簡単にテストを記述できます。

デプロイやマイングレーションもスクリプト管理できるので、これからのSolidityでスマートコントラクトを開発する上では、デファクトスタンダードになり得るフレームワークです。

加嵜 長門(著)・篠原 航(著)・丸山 弘詩(編)(2018)『ブロックチェーン・アプリケーション開発の教科書』(マイナビ出版)P.210

スポンサーリンク


Truffleをインストールしよう!

事前準備

Truffleをインストールする前に以下2つのものが必要ですので事前にインストールしておきましょう。

Truffleのインストール

Truffleは下記のコマンドをターミナルで実行すればインストールされます。

npm install -g truffle

インストールが終了したら、確認のため下記のコマンドを実行してみましょう。

truffle version

ターミナルで上記のコマンドを実行した結果、Truffleのバージョンが表示されれば正常にインストールが完了しています。

公式サンプル「ETHEREUM PET SHOP」

「ETHEREUM PET SHOP」のダウンロード

Truffleをインストールできたところで、Truffleのチュートリアルをやっていきます。

まずはお好きなところにTruffleのプロジェクトファイルを保存するディレクトリを作りましょう。

ここでは「pet_shop」と名前をつけたディレクトを作りました。

次に「pet_shop」の中にTruffleのサンプルプロジェクトである「ETHEREUM PET SHOP」をダウンロードしていきます。

まずターミナルを開き「pet_shop」ディレクトを作成した場所まで移動します。

・cd [ディレクトリ名]で任意のディレクトリに移動します。

・pwdコマンドは現在いる場所のディレクトリを表示します。

次に下記のコマンドを実行して、「ETHEREUM PET SHOP」のプロジェクトをダウンロードします。

truffle unbox pet-shop

ダウンロードが完了すると「pet-shop」ディレクトリの中にプロジェクトファイルが展開されています。

スマートコントラクトを作成しよう

「ETHEREUM PET SHOP」のプロジェクトがダウンロードできたら、次にスマートコントラクトを書いていきます。

まず「contracts」ディレクトリの中に「Adoption.sol」というファイルを作りましょう。

「Adoption.sol」の中身のコードは以下のようにします。

コントラクトの解説

ペットを採用する処理
pragma solidity ^0.4.17;

コントラクトのはじめには、Solidityのバージョンを指定します。

ここでは「0.4.17」以上のバージョンで動作するように指定しています。

Ethereum Smart Contract Security Best Practices」によれば、Solidityのバージョン指定は固定にすることを推奨しています。

pragma solidity ^0.4.17; //bad
pragma solidity 0.4.17; //good
address[16] public adopters;

address型はイーサリアム上のアドレスを意味し、20 byteの値を保持することができます。

今回の場合は、address型で定義した配列「adopters」には16個までのアドレスと、そのアドレスに対応したキーを格納できるようにしています。

function adopt(uint petId) public returns (uint) {
        require(petId >= 0 && petId <= 15);

        adopters[petId] = msg.sender;

        return petId;
}

adopt関数はペットを採用する時に呼び出される関数です。

16匹のペットに振り分けられている0~15までの数字の「petId」を引数とします。

require(petId >= 0 && petId <= 15);

まず上記のコードで正しいぺットIDがどうかを確認します。

ペットIDに問題がなければ、adopters配列にぺットIDとコントラクトを呼び出したユーザーのイーサリアムアドレスを格納します。

コントラクトを呼び出したユーザーのイーサリアムアドレスは「msg.sender」から取り出すことができます。

そして最後にペットIDを返します。

adopters[petId] = msg.sender;

return petId;
採用されたペットを取得する処理

採用されたペットは、すでに採用されているとわかるように画面に表示する必要があります。

function getAdopters() public view returns (address[16]) {
 return adopters;
}

「getAdopters()」関数を呼び出すことで、採用されたペットIDと採用者のイーサリアムアドレスが格納されている配列データを出力します。

このデータを使うことで、採用されたペットには「採用済み」といった表示をすることが可能になります。

スマートコントラクトをデプロイしよう

コンパイル

作成したスマートコントラクトは、EVM(Ethereum Virtual Machine)が実行できるようにバイトコード(bytecode)へ変換する必要があります。

このSolidityからバイトコードへ変換する作業を「コンパイル」と言います。

コンパイルはターミナルから実行します。

まずターミナルでプロジェクトのディレクトリへ移動しましょう。

今回の場合は「pet-shop」ディレクトリとなります。

目的のディレクトリまで移動したら下記のコマンドを実行します。

truffle compile

コンパイルが完了すると、「build」ディレクトリが自動で作成されます。

その中に「.json」形式で、スマートコントラクトのバイトコードが作成されています。

これでコンパイルは完了です。

マイグレーション

コントラクトをイーサリアムネットワークへデプロイするために、マイグレーションファイルを作成します。

「migrations」ディレクトリの中に「2_deploy_contracts.js」というマイグレーションファイルを作成しましょう。

「2_deploy_contracts.js」のコードは以下のようにします。

これでデプロイの設定は完了です。

イーサリアムクライアントを起動する

スマートコントラクトをブロックチェーンにデプロイするには、ブロックチェーンネットワークが必要ですね。

とは言え、いきなり全世界に公開されているメインネットへデプロイするわけにはいきません。

まずは個人のPC内のみのブロックチェーンネットワーク(プライベートネット)を作り、そこへデプロイして動作を確認していきます。

プライベートネットを作るツールとして、ここでは「Ganache」を利用します。

以下のサイトから「Ganache」をダウンロードしてみましょう。

「Ganache」のダウンロードが完了したら起動しましょう。

起動すると100ETH持った10個のテストアカウントが作成されます。

これらを利用して、スマートコントラクトをプライベートネットで動かしていきます。

デプロイ

「Ganache」を起動した状態でターミナルに戻りましょう。

ターミナルで「pet-shop」ディレクトリまで移動して、下記のコマンドを実行します。

truffle migrate

コマンドを実行するとマイグレーションが実行されます。

「Ganache」を確認すると、ブロック高が0から4となっています。

またアカウント0のETHが99.94となっていますね。

これはマイグレーションを実行した結果、コントラクトがブロックチェーンにデプロイされ、そのコストとしてETHが支払われたということです。

これでコントラクトをブロックチェーンにデプロイすることができました。

スマートコントラクトのテスト方法

テストコードの作成

Truffleでは作成したスマートコントラクトのテストコードを、SolidityまたはJavaScriptで作成することができます。

スマートコントラクトは厳密にはデプロイしたらコードを修正できないので、テストをして致命的なバグは無いようにすることを求められます。

コントラクトが仕様通りの動きが出来ているかを検証する際にはユニットテストが非常に便利なので、ここでテストの書き方を学んでおきましょう。

今回は作成したAdoptionコントラクトのテストを作ります。

テストコードは「test」ディレクト内に作っていきます。

それでは「test」ディレクトリ内に「TestAdoption.sol」というテストコードを作成しましょう。

「TestAdoption.sol」の中身は以下のようにします。

テストコードの解説

テストに必要なファイルをインポートする
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

「Assert.sol」は変数の値を判定する際に条件分岐などをしてくれる関数がまとまったものです。

いちいちif文とか書くの面倒なので大変便利ですね。

「DeployedAddresses.sol」はテストを動かすためにデプロイした時のコントラクトのアドレスを取得します。

「Adoption.sol」はスマートコントラクトが記述されたファイル、つまり今回テストするコードです。

ペットを採用する処理をテストする
function testUserCanAdoptPet() public {
  uint returnedId = adoption.adopt(8);

  uint expected = 8;

  Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
}

上記のコードはペットを採用する処理(adopt関数)をテストするコードです。

adopt関数は0~15までのペットIDを引数にとり、ペットを採用したユーザーのアドレスと一緒に配列に格納し、最後はそのペットIDを返します。

もし正常な動作をしていれば、returndedIdには指定された数字「8」が入り、expectedに格納された「8」と等しくなるはずです。

Assert.equal()で「returndedId」と「expected」が同じ数字かどうかを判定しています。

テスト通りの処理ができれば、ペットを採用する処理は正しく動作していると確認することができます。

ペットの採用者はひとりになっているかをテストする

1匹のペットを採用している人が二人もいたら取引としておかしいことになってしまいます。

そこで「1匹のペットに対して採用者は特定の一人」という処理になっているかをテストしましょう。

function testGetAdopterAddressByPetId() public {
      address expected = this;

      address adopter = adoption.adopters(8);

      Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
}

expectedには現在のコントラクトを呼び出しているアドレスが格納されます。

adopterにはID「8」のペットを採用したユーザーのアカウントが格納されます。

Assert.equal()でexpectedとadopterのアドレスが同じものかを判定します。

テスト通りに動けば、ぺットを採用した人と採用されたペットが正しく関連付けされていることを確認できます。

採用された全てのペットを正しく出力できているかをテストする

すでに採用されているペットが採用されていないと表示されると、これまたおかしなことになります。

そこで採用された全てのペットを正しく出力できているかをテストします。

function testGetAdopterAddressByPetIdInArray() public {
      address expected = this;

      address[16] memory adopters = adoption.getAdopters();

      Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
}

expectedには現在のコントラクトを呼び出しているアドレスが格納されます。

adoptersには採用された全てのペットデータが一時的に記録されます。

Assert.equal()でID8のペットを採用したアドレスと、現在のコントラクトを呼び出しているアドレスが同じかを判定します。

このテストが問題なければ、採用された全てのペットを正しく出力できていることを確認できます。

テストを実行する

テストはターミナルから実行します。

プロジェクトディレクトリ(pet-shop)に移動して、下記のコマンドを実行しましょう。

truffle test

コマンドを実行するとテストが実行されます。

問題なければ3つのテストをクリアしたと表示されます。

以上でスマートコントラクトのテストが完了しました。

スマートコントラクトと連動したUIの作成

スマートコントラクトのデプロイとテストが完了し、バックエンドの実装は終わりました。

あとはユーザーが操作できる画面を実装していくだけです。

画面のデザインと素材はすでに用意されているので、スマートコントラクトの連携する部分を実装していきます。

フロントエンドのロジックは、「src/js/app.js」の中に作っていきます。

この「app.js」はユーザーがサイトに訪れた瞬間(index.htmlを開いた時)に自動で実行されます。

app.jsの完成コードは以下のようになります。

フロントエンドの解説

ペットデータをロードする

「app.js」の中身は、1行〜101行まで「App」という名前のどこからでも呼び出せるグローバルなオブジェクト型になっています。

このAppオブジェクトの中にブロックチェーンと連携するロジックを書いていきます。

init: function() {
    // Load pets.
    $.getJSON('../pets.json', function(data) {
      var petsRow = $('#petsRow');
      var petTemplate = $('#petTemplate');

      for (i = 0; i < data.length; i ++) {
        petTemplate.find('.panel-title').text(data[i].name);
        petTemplate.find('img').attr('src', data[i].picture);
        petTemplate.find('.pet-breed').text(data[i].breed);
        petTemplate.find('.pet-age').text(data[i].age);
        petTemplate.find('.pet-location').text(data[i].location);
        petTemplate.find('.btn-adopt').attr('data-id', data[i].id);

        petsRow.append(petTemplate.html());
      }
    });

    return App.initWeb3();
  },

まず一番最初に呼び出される関数が「init()」関数です。

「init()」関数はペットデータをロードして、画面に16匹のペットを表示します。

ロードが完了したところで、次に「initWeb3()」関数を呼び出します。

Web3を立ち上げる

Web3とは、ここではweb3 JavaScript Libraryを指します。

web3 JavaScript Libraryを利用することで、イーサリアムブロックチェーンとの連携が可能になります。

initWeb3: function() {
      if (typeof web3 !== 'undefined') {
          App.web3Provider = web3.currentProvider;
      } else {
          // If no injected web3 instance is detected, fall back to Ganache
          App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
      }
      web3 = new Web3(App.web3Provider);

      return App.initContract();
  },

「initWeb3()」関数が呼び出されると、Web3オブジェクトをインスタンス化(実体化)して、イーサリアムブロックチェーンとの連携を可能にします。

次に「initContract()」関数を呼び出します。

コントラクトのインスタンス化

「initContract()」では、ビルドしたコントラクト(Adoption.json)の情報をWeb3に渡す処理を行い、コントラクトを呼び出せるようにしています。

$.getJSON('Adoption.json', function(data) {
          // Get the necessary contract artifact file and instantiate it with truffle-contract
          var AdoptionArtifact = data;
          App.contracts.Adoption = TruffleContract(AdoptionArtifact);

          // Set the provider for our contract
          App.contracts.Adoption.setProvider(App.web3Provider);

          // Use our contract to retrieve and mark the adopted pets
        return App.markAdopted();
      });
    return App.bindEvents();

そして次に「markAdopted()」関数を呼び出します。

取引が成功したと画面に表示する処理
markAdopted: function(adopters, account) {
      var adoptionInstance;

      App.contracts.Adoption.deployed().then(function(instance) {
          adoptionInstance = instance;

          return adoptionInstance.getAdopters.call();
      }).then(function(adopters) {
          for (i = 0; i < adopters.length; i++) {
              if (adopters[i] !== '0x0000000000000000000000000000000000000000'){
                  $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
              }
          }
      }).catch(function(err) {
          console.log(err.message);
      });
  },

「markAdopted()」関数は、コントラクトの関数である「getAdopters()」を呼び出します。

「getAdopters()」関数は採用されているペットと採用者のアドレスを返すものです。

つまりここでは、すでに採用されたペットのボタンを「Success」と表示させて、ボタンを押してもトランザクションができなようにしています。

ボタンを押したらペットを採用する

「Adopt」と表示されたボタンを押した時に、ペットを採用する処理をスマートコントラクトに命令するのが「handleAdopt()」関数です。

handleAdopt: function(event) {
    event.preventDefault();

    var petId = parseInt($(event.target).data('id'));

    var adoptionInstance;

    web3.eth.getAccounts(function(error, accounts) {
        if (error) {
            console.log(error);
        }

        var account = accounts[0];

        App.contracts.Adoption.deployed().then(function(instance) {
            adoptionInstance = instance;

            // Execute adopt as a transaction by sending account
            return adoptionInstance.adopt(petId, {from: account});
        }).then(function(result) {
            return App.markAdopted();
        }).catch(function(err) {
            console.log(err.message);
        });
    });
  }

まずクリックされたボタンの要素からペットID取り出します。

var petId = parseInt($(event.target).data('id'));

次に「web3.eth.getAccounts()」関数内で、採用者のアカウントアドレスを取得しています。

var account = accounts[0];

そして最後に、ペットを採用するコントラクトの関数である「adopt()」関数を呼び出して、ペットIDとアドレスを紐付けます。

App.contracts.Adoption.deployed().then(function(instance) { adoptionInstance = instance; // Execute adopt as a transaction by sending account

return adoptionInstance.adopt(petId, {from: account}); }).

紐付けが成功すれば「markAdopted()」関数を呼び出してボタンの表示を「Success」に切り替えます。

失敗すればコンソールにエラーメッセージを出力します。

}).then(function(result) {
            return App.markAdopted();
 }).catch(function(err) {
            console.log(err.message);
 });

イーサリアム・ペットショップを動かそう!

MetaMaskを準備しよう

イーサリアム・ペットショップで取引を試すには「MetaMask」が必要です。

「MetaMask」はGoogle Chromeブラウザで利用できるイーサリアムウォレットです。

持ってされていない場合は下記のサイトを参考に拡張機能に追加しましょう。

MetaMaskをプライベートネットに接続しよう

「Ganache」を起動して、一番初めのアドレス(INDEX 0)の秘密鍵をコピーしましょう。

次にMetaMaskを起動して、アカウント切り替えのアイコンをタップします。

次に「Import Account」をクリックします。

コピーした秘密鍵を貼り付けて、「IMPORT」をクリックします。

これで「Ganache」で利用できるアカウントが新たに追加されました。

次に左上の「下三角マーク」をクリックします。

表示されたメニューの中から「Custom RPC」をクリックします。

次に「Current Network Main Ethereum Network」と表示されたフォームの中に「http://127.0.0.1:7545」と入力して「Save」をクリックしましょう。

これでプライベートネットに接続されました。

アカウント画面に戻るとプライベートネット内のETHがちゃんと反映されています。

これで「MetaMask」のプライベートネットへの接続が完了しました。

ブラウザでイーサリアム・ペットショップを表示する

全ての準備は完了しました。

あとはターミナルで下記のコマンドを実行しましょう。

npm run dev

コマンドを実行すると「lite-server」が立ち上がり、イーサリアム・ペットショップがブラウザに表示されます。

あとは実際にボタンを押してペットを採用してみてください。

ちなみに「lite-server」を終了するにはターミナルで「cotrol」キーと「c」キーを同時押しすれば終了します。

おわりに

Truffleを使用してみた結果、いとも簡単にDappsを作成することができました。

使ってみるとなぜTruffleがDapps開発のデファクトスタンダードになると言われるのかよくわかりますね。

今回は自分のPC内のローカル環境内にDappsを展開しましたが、次回はテストネットへの公開を目指したDapps開発を目指したいですね。

以上、こじりょー(@kojiryoinvestor)でした。

Dapps開発カテゴリの最新記事