Truffleのpetshopチュートリアルの自分用の要点まとめ

[Truffleのペットショップのチュートリアル](https://truffleframework.com/tutorials/pet-shop)をやって分かった事のメモです。
16匹のペットがいるペットショップで、それぞれ買い手がつく。という想定です。
他の方がやられている[Truffleを使ってEthereumでペットショップアプリを作る〜前半：コントラクトの実装＆テスト〜](https://qiita.com/gaisho81/items/06c58d5eb24ae302f690)が参考になります。

# システム概要

- 機能を限定したペットショップのシステム。
- ペットは16匹いる。
- ブラウザでペット一覧が見れて、ボタンをクリックすると買える。
- ペットの代金は無料。
- 販売のみで買取はしない。
- 飼い主の区別はアカウント(アドレス?)で行う。

# 環境構築
- ```truffle init```ではなく、```truffle unbox pet-shop```で、[Truffle Box](https://truffleframework.com/boxes)に登録済みのひな形を使って初期化する。


# スマートコントラクトの作成

- コントラクト内の関数は2つだけ。
 - 飼い主を登録
 - 全てのペットの飼い主を取得

## contract文

- contracts/Adoption.solを作成して、以下を記述。

```
pragma solidity ^0.4.17;

contract Adoption {

}
```
- ```pragma```は、コンパイラに渡す情報。
- 文はセミコロン```;```で終わる。


## 変数の宣言

- コントラクトの```{}```内に以下を書いて変数宣言。
 - address型
 - publicなので、外部?から参照可。自動的にgetterメソッドが作成される。(ので、変数名そのままで呼べば中身が取得できる)

```
address[16] public adopters;    // 飼い主。address型が16個の配列
```

## 関数を書く
- ペットIDを整数で渡すと、トランザクション実行者のIDを配列内の該当位置に代入し、ペットIDを返す関数。
 - publicなので、外部から呼び出し可能。 
 - 引数だけでなく、戻り値の型も書く。```returns (uint)```の部分。
 - ```require()```で、動作に必要な条件を書く。条件に一致しなければ何もせずreturnする。
 - ```msg.sender```に、関数を呼び出した人のアドレスが入っている。グローバル変数？

```
  // Adopting a pet(ペットを買う)
  function adopt(uint petId) public returns (uint) {
    require(petId >= 0 && petId <= 15);	// ペットは16匹なので、範囲外のpetIdの場合は何もしない

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

    return petId;
  }
```

- 飼い主一覧を返す関数。
 - ```view```を指定して、読み出し専用の関数にしている。(以前は```constant```だったらしい)
 - 戻り値の型に、配列の長さまで書いているのに注意。　

```
  // Retrieving the adopters(飼い主一覧の配列を返す)
  function getAdopters() public view returns (address[16]) {
    return adopters;
  }
```

# コンパイルとデプロイ(マイグレーション)

## コンパイル

- ```truffle compile```でコンパイル
 - ブロックチェーン常にデプロイされたバイトコードは誰でも見れるので、秘密の情報をハードコーディングしないように注意。

## デプロイ(マイグレーション)

- ```migrations/2_deploy_contracts.js```ファイルを作成し、以下を記述。


```
var Adoption = artifacts.require("Adoption");

module.exports = function(deployer) {
  deployer.deploy(Adoption);
};
```
- MacアプリのGanacheで、ローカル環境でブロックチェーンを起動。

![スクリーンショット 2018-09-10 14.24.42.png](https://qiita-image-store.s3.amazonaws.com/0/142577/5c4b5d8b-9cfc-4e26-c38b-9a29c64542fe.png)

![スクリーンショット 2018-09-10 14.26.07.png](https://qiita-image-store.s3.amazonaws.com/0/142577/92edc71e-7b65-6b8f-02aa-7fa97ff17681.png)

- ```truffle migrate```でマイグレーション実施。

- Ganacheのアプリ上で、ブロック高が0から4になったのを確認。上部メニューのBlocksやTransactionsで概要も確認。


# テスト

テストをSolidity言語で書く。JavaScriptでも書ける。

## テストの作成

- test/TestAdoption.solを作成し、以下を記述。

```
pragma solidity ^0.4.17;

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

contract TestAdoption {
  Adoption adoption = Adoption(DeployedAddresses.Adoption());   // テスト用インスタンス

}
```

テストのたびにデプロイが行われるため、```import "truffle/DeployedAddresses.sol";```で、デプロイ先アドレスを取得する。(```DeployedAddresses```という特殊な変数?に入る)

## adopt()関数のテスト

```
  // Testing the adopt() function
  function testUserCanAdoptPet() public {
    uint returnedId = adoption.adopt(8);

    uint expected = 8;

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

- ```Assert.equal(戻り値、期待値、失敗した時のメッセージ)```で判定。
- ペットIDを渡すと(実行者のアドレスを変数に入れて)ペットIDを返す関数なので、8を渡したら8が返る。


## ある一匹のペットの飼い主を確認するテスト

- 上述のテスト関数で、8番のペットを買っているので、その状態を利用して、8番のペットの飼い主が自分であることを確認する。
- ```this```は、テスト実行者のアドレスが入っている。グローバル変数？

```
  // Testing retrieval of a single pet's owner(ある一匹のペットの飼い主を確認するテスト)
  function testGetAdopterAddressByPetId() public {
    // Expected owner is this contract
    address expected = this;

    address adopter = adoption.adopters(8);

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

## 全てのペットの飼い主を得るテスト

- ```memory```属性がつくと、メモリ上に一時的に保存する変数となる。(つけないとコントラクト上の変数になる(ので書き込みにgas代がかかる?))

```
  // Testing retrieval of all pet owners(全てのペットの飼い主を得るテスト)
  function testGetAdopterAddressByPetIdInArray() public {
    // Expected owner is this contract
    address expected = this;

    // Store adopters in memory rather than contract's storage
    address[16] memory adopters = adoption.getAdopters();

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


## テストの実行

- ```truffle test```でテストを実行。
 - 3つのテスト全てにチェックマークがつけばOK。


# ユーザーインタフェースの作成

- ```src``` ディレクトリに、index.htmlと画像やcssなどが一式入っている。　
- ブラウザ画面の主な機能は以下の3つ。
 - ファイルからペットの名前などの情報を読み込んで表示。
 - ブロックチェーン上のコントラクト内の変数を取り出して、飼い主がいるペットはボタンを押せなくする。
 - ボタンが押されたら、ペットを買った飼い主の情報をブロックチェーン上に書き込み、ボタンを押せなくする。

- ```src/js/app.js``` を書き換えていく。
 - ```App```オブジェクトでアプリを操作できる。(グローバル変数?)
 - ```init関数```で、jsonファイルからペット情報を読み出して、```initWeb3```関数を呼び出している。
 - markAdopted関数で、買い手のついたペットを調べ、表示を変えている。
 - handleAdopt関数で、ボタンを押してペットを買った時の処理を行っている。

## web3のインストール

- [web3](https://github.com/ethereum/web3.js/)は、EthereumのノードとHTTPなどでやり取りするためのJavaScriptライブラリ。 ([ドキュメント](https://web3js.readthedocs.io/en/1.0/index.html#)があるので参照)

- ```npm install -g web3```でインストールしておく。


## initWeb3関数の実装

- コメントが入っているのを消して、以下に書き換える。
 - web3インスタンスがあればそれを使い、なければ実体化する処理。

```
  initWeb3: function() {
    // Is there an injected web3 instance?
    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();
  },
```

## コントラクトの実体化

- ```TruffleContract関数```があるので、デプロイ先のアドレスをいちいち書き換えないですむ(?)
- ```initContract関数```を、以下のように書き換え。
 - 関数内で読み込んでいる```Adoption.json```はbuild/contracts/Adoption.jsonにある。
 - Application Binary Interface (ABI)は、コントラクトと対話(関数や変数へのアクセス)するためのアドレス。
 - インスタンス化されると、上述のApp.web3Providerを使ってアクセスできるようになる？
 - 最後の行の```App.markAdopted();```は、すでに買い手のついた(=配列内の所定の位置に飼い主のアドレスが入っている？)ペットを、表示を変えるための関数。(次の段落で実装)

```
  initContract: function() {
    $.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関数```を、コメントを消して以下に書き換える。
 - ```call()```を使ってコントラクトの変数を取り出している(gas代はかからない)
 - Ethereumでは最初に変数をゼロクリアするので、買い手がついた(アドレスが入った)かどうかを、0と比較して判断できる。
 - エラーがあれば、(ブラウザの)consoleログに出力する。

```
  markAdopted: function(adopters, account) {
    var adoptionInstance;

    // デプロイしたコントラクトのgetAdopters関数をcallする
    App.contracts.Adoption.deployed().then(function(instance) {
      adoptionInstance = instance;

      return adoptionInstance.getAdopters.call();
    }).then(function(adopters) {
      // adoptersを順番に取り出し、0以外(=すでに買い手がついている)の場合は、
      // ボタンの文字列を'Success'にして、disabled表示にする
      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関数```を、コメントを消して以下に書き換える。(一番後ろの関数なので、最後の波閉じカッコの後にカンマをつけない)
 - コントラクトのadopt関数を、petIdと自分のアドレスをつけて呼び出している(買った時に、コントラクト内の配列に値を入れるため)(gas代がかかる)
 - うまくいったら、表示を書き換える。


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

    // 選択した(押されたボタンの)idをpetIdにする
    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
        // 現在のアカウントを使って、コントラクトのadopt関数を呼び出し、配列にアドレスを入れる。
        return adoptionInstance.adopt(petId, {from: account});
      }).then(function(result) {
        // 表示を変更(ボタンの文字列を'Success'にして、disabled表示にする)
        return App.markAdopted();
      }).catch(function(err) {
        console.log(err.message);
      });
    });
  }
```

# ブラウザから動作確認

## Metamaskのインストールと設定

- chromeブラウザに[Metamask](https://metamask.io/)エクステンションを入れる

- Ganacheの「Accounts」タブの上の方にあるニーモニックをコピー。
- Metamaskの「Restore from seed phrase」をクリック
- 「Wallet seed」欄に入力し、パスワードも入力
- 左上のセレクトボックスで「Custom RPC」を選択。Ganacheで立ち上げたサーバの「http://127.0.0.1:7545/ 」を入力してSave。(末尾に「/」を付けないと、再起動時に「Connecting to Unknown Private Network」で待たされる)
- 左矢印ボタンで戻ると、Ganacheの一番上にあるアカウントの残高がMetamask側にも反映されている。

## lite-serverの起動

- ```npm run dev```で、サーバが立ち上がり、自動的にブラウザが開く。
 - すでにlite-serverモジュールがnode_modulesディレクトリにインストールされている。
 - ```bs-config.json```ファイルで、htmlソースの場所や、コントラクトの場所を指定する。
 - ```package.json```ファイルで、npmのdevコマンドが定義されている。

- デフォルトでSafariが起動するので、手動でChromeからhttp://localhost:3000 を開く。

- JavaScriptコンソールを開いて、エラーが出ていないか確認する。

- Adoptボタンを押すと、Metamaskの画面がポップアップするので、トランザクション内容を確認して、SUBMITを押す。
 - Ganacheで、ブロックが進んでいるのも確認する。

# Macを再起動した場合

- Ganache起動
- ```truffle migrate --reset``` (「--reset」で、build配下のバイトコードとかのjsonをクリアしてから実行してくれる)
- Metamaskの画面を開いて、Account1を表示する。
 - 「Connecting to Unknown Private Network」のままの場合は、左上のネットワーク設定を、一旦別なネットワークを選んで、再度「http://127.0.0.1:7545/ 」を選択
 - パスワードを入れる(「Restore from seed phrase」で登録した時に使ったパスワード)
- Metamaskのアカウントをリセットする。「Account1」が表示されている状態で、右上のメニューから「Settings」で、下の方にある「Reset Account」をクリック。
- ```npm run dev``` でサーバ起動
- Chromeで http://localhost:3000 を開く

## 初回のAdoptで?、ボタンが'Success'にならない

- 起動直後に「Adopt」ボタンを押すと、ブロックチェーンに書き込まれたのがGanacheのBLOCKSから確認できますが、画面上のボタンがすぐには'Success'になりません。リロードすると反映されます。2回目以降のAdoptは即反映される場合もあれば、リロードしないと反映されない場合もあります。setIntervalで15秒待ってからApp.markAdopted();を呼ぶようにしたら反映されました。adoptionInstance.adopt()でブロックチェーンに書き込むのが非同期な気がしています。

## nonceが違うというエラーが出た場合

- Adoptをクリックした際に、「```Error: Error: [ethjs-rpc] rpc error with payload {…once. account has nonce of: 8 tx has nonce of: 46```」や「```Error: the tx doesn't have the correct nonce. account has nonce of: 4 tx has nonce of: 10```」のように、nonceが違うと出る場合は、Metamask上のアカウントをリセットする。[Resetting an Account (New UI)](https://consensys.zendesk.com/hc/en-us/articles/360004177531-Resetting-an-Account-New-UI-)
 - 

# 遭遇したエラー

- デプロイ用のスクリプトで、deployの引数に変数を渡しているが、```deployer.deploy(Adoption);```と書くべきを```deployer.deploy("Adoption");```と書いていた。

```
Running migration: 2_deploy_contracts.js
Error encountered, bailing. Network state unknown. Review successful transactions manually.
```

- テストでAssert.equal()をAssert()と書いててエラー。

```
test/TestAdoption.sol:28:5: TypeError: Exactly one argument expected for explicit type conversion.
    Assert(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
    ^--------------------------------------------------------------------^
Compilation failed. See above.
```