【Truffleチュートリアル】Ethereum Pet Shopを作成してみた

# 概要

約1年振りくらいにDAppsの開発環境について調べてみたら、Truffleのチュートリアルが一新されていた。
開発環境の整備からDAppsの作成までを簡単に作成できたので、試した結果のメモをまとめる。

参照先: https://truffleframework.com/tutorials/pet-shop

# 実行内容
## 全体の流れ

1. 開発環境の整備
2. Truffle Box(Truffleのサンプル)によるプロジェクト作成
3. スマートコントラクト(Solidity)の作成
4. スマートコントラクト(Solidity)のビルドとマイグレーション
5. スマートコントラクト(Solidity)のテスト
6. スマートコントラクト(Solidity)のUI(JavaScript)作成
7. ブラウザでの動作確認

## 開発環境の整備

### 構築環境

Mac上の環境を汚したくないので、なるべくDockerコンテナ上で作業する。

* OS: Mac OS X
* 事前インストールツール: Docker for Mac

### コマンド

Truffleコマンドをインストール

```zsh
$ mkdir petshop
$ cd petshop
$ docker run -v ${PWD}:/usr/src/app -it node:9.11 bash
root@b3be80c38001:/# cd /usr/src/app/
root@b3be80c38001:/usr/src/app# npm install -g truffle
/usr/local/bin/truffle -> /usr/local/lib/node_modules/truffle/build/cli.bundled.js
+ truffle@4.1.13
added 81 packages in 7.684s
```

## Truffle Box(Truffleのサンプル)によるプロジェクト作成

### コマンド
```bash
root@b3be80c38001:/usr/src/app# truffle unbox pet-shop
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test
  Run dev server: npm run dev
root@b3be80c38001:/usr/src/app# exit
exit
```

### プロジェクトのディレクトリ構成

```zsh
$ tree -L 1
.
├── box-img-lg.png:    Pet Shopの画像
├── box-img-sm.png:    同上
├── bs-config.json:    lite-serverの設定ファイル
├── contracts:         Solidityの格納ディレクトリ
├── migrations:        SolidityをEthereumにローンチする手順
├── node_modules:      JavaScriptの依存ライブラリ群
├── package-lock.json: package.jsonの変更履歴
├── package.json:      パッケージ管理
├── src:               Webアプリのソースコード
├── test:              Solidityのテストコード
└── truffle.js:        Truffleの設定(Ethereumの接続設定等)

5 directories, 6 files
```

## スマートコントラクト(Solidity)の作成
```solidity:petshop/contracts/Adoption.sol
// pragma: コンパイル時のみ読み込まれる設定
pragma solidity ^0.4.17;

// contract: クラスと同等の概念
contract Adoption {
    // address: Ethereumのアドレス用の型
    // public: 公開権限(private/public/internal/external)
    address[16] public adopters;

    // adopt(): 指定したペットを採用扱いにする
    // function 関数名(引数) 公開権限 return 返り値の型
    function adopt(uint petId) public returns (uint) {
        // require: 変数の確認
        require(petId >= 0 && petId <= 15);

        // msg.sender: トランザクションの実行者アドレス
        adopters[petId] = msg.sender;

        return petId;
    }

    // getAdopters(): 現在のペットの採用状況を返す
    //                初期値以外のアドレスが格納されている箇所が採用済み
    function getAdopters() public view returns (address[16]) {
        return adopters;
    }
}
```

## スマートコントラクト(Solidity)のビルドとマイグレーション

###  Ganacheのインストール

```zsh
$ brew cask install ganache
```

### マイグレーション手順の作成

```javascript:petshop/migrations/2_deploy_contracts.js
// artifacts.require: マイグレーション対象のcontractの指定
var Adoption = artifacts.require('Adoption');

module.exports = function(deployer) {
  deployer.deploy(Adoption);
};
```

### ビルド・マイグレーション環境の設定

```docker:petshop/docker-compose.yaml
version: '3.6'

services:
    dapps:
        image: node:9.11
        container_name: dapps
        volumes: 
            - ./:/usr/src/app
        working_dir: /usr/src/app
        ports:
            - 3000:3000
        command: bash -c -x "npm install && npm run dev"
```
```diff:petshop/package.json
diff --git a/petshop/package.json b/petshop/package.json
index 128b067..633f167 100644
--- a/petshop/package.json
+++ b/petshop/package.json
@@ -7,12 +7,17 @@
     "test": "test"
   },
   "scripts": {
-    "dev": "lite-server",
+    "dev": "nodemon --watch contracts --watch migrations -e js,sol --exec 'truffle compile && truffle migrate && lite-server'",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author": "",
   "license": "ISC",
   "devDependencies": {
-    "lite-server": "^2.3.0"
-  }
+    "lite-server": "^2.3.0",
+    "nodemon": "^1.17.5",
+    "solc": "^0.4.24",
+    "truffle": "^4.1.11"
+  },
+  "dependencies": {}
 }
```
```diff:petshop/truffle.js
diff --git a/petshop/truffle.js b/petshop/truffle.js
index 8cc069f..a32cd88 100644
--- a/petshop/truffle.js
+++ b/petshop/truffle.js
@@ -3,7 +3,7 @@ module.exports = {
   // for more about customizing your Truffle configuration!
   networks: {
     development: {
-      host: "127.0.0.1",
+      host: "host.docker.internal",
       port: 7545,
       network_id: "*" // Match any network id
     }
```

### 実行ログ

**※:Ganache起動後に実行すること**

```zsh
$ docker-compose up
Creating network "petshop_default" with the default driver
Creating dapps ... done
Attaching to dapps
dapps    | + npm install
dapps    | npm WARN pet-shop@1.0.0 No description
dapps    | npm WARN pet-shop@1.0.0 No repository field.
dapps    | npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules/fsevents):
dapps    | npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
dapps    | 
dapps    | up to date in 11.486s
dapps    | + npm run dev
dapps    | 
dapps    | > pet-shop@1.0.0 dev /usr/src/app
dapps    | > nodemon --watch contracts --watch migrations -e js,sol --exec 'truffle compile && truffle migrate && lite-server'
dapps    | 
dapps    | [nodemon] 1.17.5
dapps    | [nodemon] to restart at any time, enter `rs`
dapps    | [nodemon] watching: /usr/src/app/contracts/**/* /usr/src/app/migrations/**/*
dapps    | [nodemon] starting `truffle compile && truffle migrate && lite-server`
dapps    | Using network 'development'.
dapps    | 
dapps    | Running migration: 1_initial_migration.js
dapps    |   Deploying Migrations...
dapps    |   ... 0x3358aae8d79ec25804cbe01ba2473c606d11624191c101ba625ce7a331e4a9f7
dapps    |   Migrations: 0xe014011611ac1adf528a643b68d2c35dbd614822
dapps    | Saving successful migration to network...
dapps    |   ... 0xe3f224f8abf6cbdeb59004eef623053ebb68be6754bb731586e2242ff8762697
dapps    | Saving artifacts...
dapps    | Running migration: 2_deploy_contracts.js
dapps    |   Deploying Adoption...
dapps    |   ... 0xafc383cb10a4b4c281ef9b68a5196db30eab48709174ba8e5da050b5fef478e5
dapps    |   Adoption: 0x1066369d8aad2ef3ec1d646de107977f60817ad4
dapps    | Saving successful migration to network...
dapps    |   ... 0x7a58e1f3064eeb3eedf900193583de454b4e9ec81f41db1793932dac6f0323bb
dapps    | Saving artifacts...
dapps    | ** browser-sync config **
dapps    | { injectChanges: false,
dapps    |   files: [ './**/*.{html,htm,css,js}' ],
dapps    |   watchOptions: { ignored: 'node_modules' },
dapps    |   server: 
dapps    |    { baseDir: [ './src', './build/contracts' ],
dapps    |      middleware: [ [Function], [Function] ] } }
dapps    | [Browsersync] Access URLs:
dapps    |  -----------------------------------
dapps    |        Local: http://localhost:3000
dapps    |     External: http://172.18.0.2:3000
dapps    |  -----------------------------------
dapps    |           UI: http://localhost:3001
dapps    |  UI External: http://172.18.0.2:3001
dapps    |  -----------------------------------
dapps    | [Browsersync] Serving files from: ./src
dapps    | [Browsersync] Serving files from: ./build/contracts
dapps    | [Browsersync] Watching files...
dapps    | [Browsersync] Couldn't open browser (if you are using BrowserSync in a headless environment, you might want to set the open option to false)
``` 

## スマートコントラクト(Solidity)のテスト

### テストプログラム

```solidity:petshop/test/TestAdoption.sol
pragma solidity ^0.4.17;

import "truffle/Assert.sol"; // テスト結果の成否を返すためのモジュール
import "truffle/DeployedAddresses.sol"; // コントラクトのテスト用インスタンスを新規生成するためのモジュール
import "../contracts/Adoption.sol"; // 

contract TestAdoption {
    // テスト時のコントラクト実行者のEthereumアドレスを取得
    Adoption adoption = Adoption(DeployedAddresses.Adoption());

    // adopt()関数の動作確認
    function testUserCanAdoptPet() public {
        uint returnedId = adoption.adopt(8);

        uint expected = 8;

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

    // 指定したペットIDにオーナーが存在することを確認
    function testGetAdopterAddressByPetId() public {
        // テスト時のコントラクト実行者のEthereuemアドレスを取得
        address expected = this;

        // ペットIDが8番に格納されているEthereumアドレスを取得
        address adopter = adoption.adopters(8);

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

    // getAdopters()の動作確認
    function testGetAdopterAddressByPetIdInArray() public {
        // テスト時のコントラクト実行者のEthereuemアドレスを取得
        address expected = this;

        // コントラクトに格納されているペットIDとそのオーナーを全件取得
        address[16] memory adopters = adoption.getAdopters();

        // ペットID 8番にテスト時のコントラクト実行者のEthereumアドレスが格納されているか確認
        Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
    }
}
```

### テスト実行環境の作成

```diff:petshop/package.json
diff --git a/petshop/package.json b/petshop/package.json
index 44736c0..0d93e11 100644
--- a/petshop/package.json
+++ b/petshop/package.json
@@ -8,7 +8,7 @@
   },
   "scripts": {
     "dev": "nodemon --watch contracts --watch migrations -e js,sol --exec 'truffle compile && truffle migrate && lite-server'",
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "truffle test"
   },
   "author": "",
   "license": "ISC",
```

### 実行

```zsh
$ docker exec dapps npm test

> pet-shop@1.0.0 test /usr/src/app
> truffle test

Using network 'development'.

Compiling ./contracts/Adoption.sol...
Compiling ./test/TestAdoption.sol...
Compiling truffle/Assert.sol...
Compiling truffle/DeployedAddresses.sol...


  TestAdoption
    ✓ testUserCanAdoptPet (107ms)
    ✓ testGetAdopterAddressByPetId (80ms)
    ✓ testGetAdopterAddressByPetIdInArray (145ms)


  3 passing (1s)
```

## スマートコントラクト(Solidity)のUI(JavaScript)作成

```diff:petshop/src/js/app.js
diff --git a/petshop/src/js/app.js b/petshop/src/js/app.js
index 32d4d15..fdc49ee 100644
--- a/petshop/src/js/app.js
+++ b/petshop/src/js/app.js
@@ -24,18 +24,31 @@ App = {
   },
 
   initWeb3: function() {
-    /*
-     * Replace me...
-     */
+    // web3のインスタンスが存在するか確認
+    if (typeof web3 !== 'undefined') {
+      App.web3Provider = web3.currentProvider;
+    } else {
+      // Ganacheに接続してweb3のインスタンスを作成
+      App.web3Provider = new Web3.providers.HttpProvider('http://host.docker.internal:7545');
+    }
+    web3 = new Web3(App.web3Provider);
 
     return App.initContract();
   },
 
   initContract: function() {
-    /*
-     * Replace me...
-     */
+    $.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();
   },
 
@@ -43,20 +56,49 @@ App = {
     $(document).on('click', '.btn-adopt', App.handleAdopt);
   },
 
+  // ペットの情報を取得し、オーナーがいる場合は、ApdoptボタンをSuccessに変更する
   markAdopted: function(adopters, account) {
-    /*
-     * Replace me...
-     */
+    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;
 
-    /*
-     * Replace me...
-     */
+    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);
+      });
+    });
   }
 
 };
```

## ブラウザでの動作確認

### MetaMaskのインストール

参照先: https://truffleframework.com/tutorials/pet-shop#installing-and-configuring-metamask

### 動作確認

アクセス先: http://localhost:3000/

# まとめ

Solidityの構成や作り方については、理解できた気がするが、web3ライブラリを利用したフロント部分については、理解がまだ浅い。
特に、Solidityのコードベースとフロントのコードベースのリポジトリを分けて管理した際に、フロント側が実行できるのかが不明。(/Adopt.jsonって読み込めるの？)
アプリケーションを書く場合、そういった部分も別途調査する必要あるなぁ・・・