DApps開発環境の構築と実装

概要

f:id:adrenaline2017:20190706100955j:plain

Solidity, Truffle, Ganache, Metamaskを組み合わせてEhereumで実行出来きるDappsを開発します。

開発環境の構築

以下の項目が開発をする際に必要となる環境になります。

Solidity

Ethereumでスマートコントラクトを開発する為の言語です。構文がJavaScriptに似てるので、非常に親みやすい言語だと思います。 solidityはsolcと呼ばれるSolidityコンパイラによってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換されます。

Truffle

SolidityでDappsを開発する際のフレームワークです。solidityで書いたコードのコンパイル、デプロイ、テストを簡単に行うことが出来ます。

Ganache

プライベートブロックチェーン環境を構築することができるGUIアプリケーションです。本番環境ではgethが必要になりますが、開発環境ではプライベートチェーン使うのでGanacheが向いています。

Visual Studio Code

言わずと知れたコードエディタです。Solidityのプラグインを利用することができます。ターミナルからgit, truffle, Ganacheが利用でき、コントラクトのコンパイル、デプロイ、テストが簡単に出来るのでRemixよりも開発がスムーズに行えます。

Metamask

Ethereum のウォレットで、Chrome拡張機能として提供されています。dappsの秘密鍵管理やトランザクション確認の機能をmetamaskで利用します。

インストール

開発環境の導入方法になります。

Truffle

最新版をインストール

$ npm install -g truffle

バージョンの確認

$ truffle version

Truffle v5.0.13 (core: 5.0.13)
Solidity v0.5.0 (solc-js)
Node v10.14.1
Web3.js v1.0.0-beta.37

ganache

ganacheの公式サイトからファルをダウンロードしてインストールします。

Metamask

Chrome拡張機能なのでMetaMaskから「+CHROMEに追加」をクリックしてプラグインをインストールします。

実装

今回はTruffleの公式サイトに紹介されているチュートリアル「pet shop」を実装していきたいと思います。pet shopではペットの飼い主を決定するためのペットショップのシステムを構築します。

開発の流れ

バックエンド 1. サンプルコードをダウンロード 2. コントラクトの作成 3. コンパイル 4. マイグレションの作成 5. デプロイ 6. テストの作成 7. テスト

フロントエンド 1. app.jsを編集 2. metamaskを起動しプライベートネットに接続

それではまずバックエンドの実装を行います。

バックエンドの実装

1. サンプルコードをダウンロード

pet-shopフォルダを作成

$ mkdir pet-shop && cd pet-shop

pet-shopのサンプルをダウンロード

$ truffle unbox pet-shop

2. コントラクトの作成

「pet-shop」ディレクトリにある「contracts」内に「Adoption.sol」というファイルを作成し、そこにコントラクトを作成します。 $ truffle create contract Adoption コントラクトの内容は以下の通りです。

pragma solidity ^0.5.0;

contract Adoption {
    address[16] public adopters;
    function adopt(uint petId) public returns (uint) {
    require(petId >= 0 && petId <= 15);
    adopters[petId] = msg.sender;
    
    return petId;
    }
    function getAdopters() public view returns (address[16] memory) {
        
        return adopters;
    }
}

Adoption.solについて説明します。

pragma solidity ^0.5.0;

Solidityはsolcと呼ばれるSolidiコンパイラによってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換されます。そのコンパイルする際に使用するコンパイラのバージョンを指定しています。

contract Adoption {

コントラクトの名前はAdoptionとします。

address[16] public adopters;

address型の配列でアドレスを16個まで保持できるadoptersを定義しています。アクセス修飾子はpublicなのでコントラクト内外部及び継承先で参照することが出来ます。

function adopt(uint petId) public returns (uint) {
    require(petId >= 0 && petId <= 15);
    adopters[petId] = msg.sender;
    
    return petId;
    }

adopt関数を定義しています。この関数はpetIdを引数として取り、requireでpetIdが0〜15であるか確認します。この条件に当てはまる場合、msg.senderをadopters配列に追加します。msg.sederは関数(adopt関数)を呼び出したユーザー若しくはスマートコントラクトのアドレスを参照するものです。今回はpetIdをmsg.seder(自分のアドレス)に紐ずけています。そしてpetIdを返しています。

function getAdopters() public view returns (address[16] memory) {

        return adopters;
    }

getAdopters関数ではAdoption関数で追加したpetIdと、それに紐づいたsddressを返します。返すデータはmemoryで宣言された通り一時的に保存されます。

3. コンパイル

作成したコントラクトをコンパイラを使ってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換します。

$ truffle compile

4. マイグレションの作成

変換したバイトコードをEthrereumネットワークにデプロイするために、マイグレーションファイルを作成します。

$ truffle create migration deploy_contracts

「migrations」ディレクトリに「deploy_contracts.js」ファイルを作成します。

var Adoption = artifacts.require("Adoption");
 module.exports = function(deploy) {
 deploy.deploy(Adoption);
}

マイグレーションを行う際にartifacts.require()を介してコントラクトをTruffleに対し指示します。引数はコントラクト名(Adoption)と同じにする必要があります。module.exportsでモジュール化を行いエクスポートします。deploy.deploy(Adoption)では指定した特定のコントラクトをデプロイすることを意味しています。

5. デプロイ

デプロイする際には開発用ネットワークであるプライベートネットワークをGanacheを使用して実行します。インストールしたGanacheを起動してください。

Truffleを使ってデプロイを行ないます。

$ truffle migrate

6. テストの作成

「test」ディレクトリに「TestAdoption.sol」ファイルを作成します。

pragma solidity ^0.5.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

contract TestAdoption {
    Adoption adoption = Adoption(DeployedAddresses.Adoption());
    uint expectedPetId = 8;
    address expectedAdopter = address(this);
    
    function testUserCanAdoptPet() public {
        uint returnedId = adoption.adopt(expectedPetId);
        Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
    }
    
    function testGetAdopterAddressByPetId() public {
        address adopter = adoption.adopters(expectedPetId);
        Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
    }
    
    function testGetAdopterAddressByPetIdInArray() public {
        address[16] memory adopters = adoption.getAdopters();
        Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the expected pet should be this contract");
    }
}

詳細について説明していきます。

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

Assert.solはアサーション関数で、このアサーション関数を使用しテストの合否をチェクします。DeployedAddresses.solはデプロイされたコントラクトのアドレスを取得します。

通常nodeのモジュールは”node modules”にありますが、この2つのファイルは探してもありません。”Assert.sol”はココにあり、”DeployedAddresses.sol”はテスト時に動的に作成されます。

contract TestAdoption {
    Adoption adoption = Adoption(DeployedAddresses.Adoption());

Adoption型のadoption変数にコントラクトのアドレスを格納します。

 uint expectedPetId = 8;
    address expectedAdopter = address(this);

テスト用のpetIdをexpectedPetIdとし8を代入します。またテスト用のアドレスをexpectedAdopterとし、このコントラクトを呼び出したアドレスを代入します。

function testUserCanAdoptPet() public {
        uint returnedId = adoption.adopt(expectedPetId);
        Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
    }

ここではAdoptionコントラクトのadoption関数をテストしています。adoption関数に引数としてexpectedPetId(内容は8)を渡した場合に返ってくる値が同じかどうかチェックしています。つまり指定したpetIdを入れた時に指定したpetIdが返ってくるかテストしています。

function testGetAdopterAddressByPetId() public {
        address adopter = adoption.adopters(expectedPetId);
        Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
    }

ここではAdoptionコントラクトのadopters関数をテストしています。 adopters関数に引数としてexpectedPetId(内容は8)を渡した場合に対応するアドレスを返すかどうかチェックしています。つまり8番目にペットの飼い主のアドレスが登録されているかテストしています。

function testGetAdopterAddressByPetIdInArray() public {
        address[16] memory adopters = adoption.getAdopters();
        Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the   expected pet should be this contract");
    }

ここではAdoptionコントラクトのgetAdopters関数をテストしています。 adopters関数に引数としてexpectedPetId(内容は8)を渡した場合に対応するアドレスを返すかどうかチェックしています。つまり配列の8番目にこのコントラクを呼び出したアドレスが入っているかどうかチェックしています。

7. テスト

作成したtestファイルを使ってテストを実行します。

$ truffle test

以上でバックエンドの実装は終わりになります。 次にフロントエンドの開発になります。

フロントエンドの実装

avaScriptのAPIであるweb3は、イーサリアムのノードと通信することのできます。フロントの実装では、このAPIを用いてブラウザから作成したコントラクトにアクセスすることが可能になります。

それではapp.js(src/js/app.js)を編集していきます。

App = {
   web3Provider: null,
   contracts: {},
   init: function() {
   $.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();
  },
  initWeb3: function() {
  if (typeof web3 !== 'undefined') {
   App.web3Provider = web3.currentProvider;
  } else {
   App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
  }
   web3 = new Web3(App.web3Provider);
   return App.initContract();
  },
  initContract: function() {
   $.getJSON('Adoption.json', function(data) {
   var AdoptionArtifact = data;
   App.contracts.Adoption = TruffleContract(AdoptionArtifact);
   App.contracts.Adoption.setProvider(App.web3Provider);
   return App.markAdopted();
  });
   return App.bindEvents();
  },
  bindEvents: function() {
   $(document).on('click', '.btn-adopt', App.handleAdopt);
  },
  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);
  });
   },
  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;
    return adoptionInstance.adopt(petId, {from: account});
   }).then(function(result) {
    return App.markAdopted();
   }).catch(function(err) {
    console.log(err.message);
   });
  });
   }
  };
  $(function() {
   $(window).load(function() {
    App.init();
   });
  });

詳細について説明していきます。

init: function() {
   $.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();
  },

pets.jsonにある15匹の犬のデータを1匹ずつ呼び出し、index.htmlのpetsRowとpetTemplateに渡し、画面上に表示しています。

initWeb3: function() {
  if (typeof web3 !== 'undefined') {
   App.web3Provider = web3.currentProvider;
  } else {
   App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
  }
   web3 = new Web3(App.web3Provider);
   return App.initContract();
  },

ここではweb3のインスタンス化を行なっています。web3.jsはイーサリアムのノードと通信することのできるJavaScriptAPIで、ブラウザからコントラクトにアクセスすることが可能になります。

既存にweb3のインスタンスがあるか確認します。起動している場合はプロバイダを取得しインスタンス化してオブジェクトを作成します。既存にインスタンスがない場合は新規でプロバイダのインスタンスを作成します。

initContract: function() {
   $.getJSON('Adoption.json', function(data) {
   var AdoptionArtifact = data;
   App.contracts.Adoption = TruffleContract(AdoptionArtifact);
   App.contracts.Adoption.setProvider(App.web3Provider);
   return App.markAdopted();
  });
   return App.bindEvents();
  },

ここではコントラクトのインスタンス化を行なっています。Adoptionコントラクトをインスタンス化するために、truffleのtruffle-contractというライブラリのTruffleContract()関数を使用します。この関数の引数にArtifactを入れ、コントラクトをインスタンス化します。そして、web3のインスタンス化で作ったApp.web3Providerをそのコントラクトにセットします。

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);
  });
   },

ここではボタンが押された際にUIを変更する処理を行なっています。 adoptionInstanceにインスタンスを取得しAdoptionコントラクトのgetAdoption関数を呼び出してブロックチェーンに書かれている情報を返します。1匹ずつ状態を確認し、アドレスが紐づけられていた場合には画面のボタン表示を「Adopte」から「Success」に切り替え、ボタンを押してもトランザクションが発生しない様にします。

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;
    return adoptionInstance.adopt(petId, {from: account});
   }).then(function(result) {
    return App.markAdopted();
   }).catch(function(err) {
    console.log(err.message);
   });
  });
   }

ここではボタンが押された際にペットを採用する処理を行なっています。押したボタンのpetIdを確認し成功した場合にはadoptionInstanceを呼び出して画面上のUIの変更を行います。失敗した場合はエラーを表示します。 ボタンを押すことでトランザクションを作成しブロックチェーンに情報を書き込みます。

2. metamaskを起動しプライベートネットに接続

「Ganache」の画面に表示されているINDEX0のPRIVATE KEYをコピーします。インストールしたMetamaskを起動して「アカウントのインポート」から追加を行います。Metamaskのネットワークに新たに「http://127.0.0.1:7545」を追加し選択します。

以上でサーバーサイドとフロントサイドの実装は終わりです。 ターミナルで下記コマンドを実行するとブラウザが立ち上がりdappsを表示することが出来ます。

$ npm run dev